Add Reader Macros and SX-Activity plans to SX docs

Reader Macros: # dispatch for datum comments (#;), raw strings (#|...|),
and quote shorthand (#').

SX-Activity: ActivityPub federation with SX wire format, IPFS-backed
component registry, content-addressed media, Bitcoin-anchored provenance.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 10:12:36 +00:00
parent e112bffe5c
commit 624b08997d
3 changed files with 490 additions and 1 deletions

View File

@@ -103,7 +103,11 @@
(define plans-nav-items (list
(dict :label "Isomorphic Architecture" :href "/plans/isomorphic-architecture"
:summary "Making the server/client boundary a sliding window — per-page bundles, smart expansion, SPA routing, client IO, streaming suspense.")))
:summary "Making the server/client boundary a sliding window — per-page bundles, smart expansion, SPA routing, client IO, streaming suspense.")
(dict :label "Reader Macros" :href "/plans/reader-macros"
:summary "Extensible parse-time transformations via # dispatch — datum comments, raw strings, and quote shorthand.")
(dict :label "SX-Activity" :href "/plans/sx-activity"
:summary "ActivityPub federation with SX as wire format — IPFS-backed component registry, content-addressed media, Bitcoin-anchored provenance.")))
(define bootstrappers-nav-items (list
(dict :label "Overview" :href "/bootstrappers/")

View File

@@ -20,6 +20,489 @@
(p :class "text-sm text-stone-500 mt-1" (get item "summary")))))
plans-nav-items)))))
;; ---------------------------------------------------------------------------
;; Reader Macros
;; ---------------------------------------------------------------------------
(defcomp ~plan-reader-macros-content ()
(~doc-page :title "Reader Macros"
(~doc-section :title "Context" :id "context"
(p "SX has three hardcoded reader transformations: " (code "`") " → " (code "(quasiquote ...)") ", " (code ",") " → " (code "(unquote ...)") ", " (code ",@") " → " (code "(splice-unquote ...)") ". These are baked into the parser with no extensibility. The " (code "~") " prefix for components and " (code "&") " for param modifiers are just symbol characters, handled at eval time.")
(p "Reader macros add parse-time transformations triggered by a dispatch character. Motivating use case: a " (code "~md") " component that uses heredoc syntax for markdown source instead of string literals. More broadly: datum comments, raw strings, and custom literal syntax."))
(~doc-section :title "Design" :id "design"
(~doc-subsection :title "Dispatch Character: #"
(p "Lisp tradition. " (code "#") " is NOT in " (code "ident-start") " or " (code "ident-char") " — completely free. Pattern:")
(~doc-code :code (highlight "#;expr → (read and discard expr, return next)\n#|...| → raw string literal\n#'expr → (quote expr)" "lisp")))
(~doc-subsection :title "#; — Datum comment"
(p "Scheme/Racket standard. Reads and discards the next expression. Preserves balanced parens.")
(~doc-code :code (highlight "(list 1 #;(this is commented out) 2 3) → (list 1 2 3)" "lisp")))
(~doc-subsection :title "#|...| — Raw string"
(p "No escape processing. Everything between " (code "#|") " and " (code "|") " is literal. Enables inline markdown, regex patterns, code examples.")
(~doc-code :code (highlight "(~md #|## Title\n\nSome **bold** text with \"quotes\" and \\backslashes.|)" "lisp")))
(~doc-subsection :title "#' — Quote shorthand"
(p "Currently no single-char quote (" (code "`") " is quasiquote).")
(~doc-code :code (highlight "#'my-function → (quote my-function)" "lisp")))
(~doc-subsection :title "No user-defined reader macros (yet)"
(p "Would require multi-pass parsing or boot-phase registration. The three built-ins cover practical needs. Extensible dispatch can come later without breaking anything.")))
;; -----------------------------------------------------------------------
;; Implementation
;; -----------------------------------------------------------------------
(~doc-section :title "Implementation" :id "implementation"
(~doc-subsection :title "1. Spec: parser.sx"
(p "Add " (code "#") " dispatch to " (code "read-expr") " (after the " (code ",") "/" (code ",@") " case, before number). Add " (code "read-raw-string") " helper function.")
(~doc-code :code (highlight ";; Reader macro dispatch\n(= ch \"#\")\n (do (set! pos (inc pos))\n (if (>= pos len-src)\n (error \"Unexpected end of input after #\")\n (let ((dispatch-ch (nth source pos)))\n (cond\n ;; #; — datum comment: read and discard next expr\n (= dispatch-ch \";\")\n (do (set! pos (inc pos))\n (read-expr) ;; read and discard\n (read-expr)) ;; return the NEXT expr\n\n ;; #| — raw string\n (= dispatch-ch \"|\")\n (do (set! pos (inc pos))\n (read-raw-string))\n\n ;; #' — quote shorthand\n (= dispatch-ch \"'\")\n (do (set! pos (inc pos))\n (list (make-symbol \"quote\") (read-expr)))\n\n :else\n (error (str \"Unknown reader macro: #\" dispatch-ch))))))" "lisp"))
(p "The " (code "read-raw-string") " helper:")
(~doc-code :code (highlight "(define read-raw-string\n (fn ()\n (let ((buf \"\"))\n (define raw-loop\n (fn ()\n (if (>= pos len-src)\n (error \"Unterminated raw string\")\n (let ((ch (nth source pos)))\n (if (= ch \"|\")\n (do (set! pos (inc pos)) nil) ;; done\n (do (set! buf (str buf ch))\n (set! pos (inc pos))\n (raw-loop)))))))\n (raw-loop)\n buf)))" "lisp")))
(~doc-subsection :title "2. Python: parser.py"
(p "Add " (code "#") " dispatch to " (code "_parse_expr()") " (after " (code ",") "/" (code ",@") " handling ~line 252). Add " (code "_read_raw_string()") " method to Tokenizer.")
(~doc-code :code (highlight "# In _parse_expr(), after the comma/splice-unquote block:\nif raw == \"#\":\n tok._advance(1) # consume the #\n if tok.pos >= len(tok.text):\n raise ParseError(\"Unexpected end of input after #\",\n tok.pos, tok.line, tok.col)\n dispatch = tok.text[tok.pos]\n if dispatch == \";\":\n tok._advance(1)\n _parse_expr(tok) # read and discard\n return _parse_expr(tok) # return next\n if dispatch == \"|\":\n tok._advance(1)\n return tok._read_raw_string()\n if dispatch == \"'\":\n tok._advance(1)\n return [Symbol(\"quote\"), _parse_expr(tok)]\n raise ParseError(f\"Unknown reader macro: #{dispatch}\",\n tok.pos, tok.line, tok.col)" "python"))
(p "The " (code "_read_raw_string()") " method on Tokenizer:")
(~doc-code :code (highlight "def _read_raw_string(self) -> str:\n buf = []\n while self.pos < len(self.text):\n ch = self.text[self.pos]\n if ch == \"|\":\n self._advance(1)\n return \"\".join(buf)\n buf.append(ch)\n self._advance(1)\n raise ParseError(\"Unterminated raw string\",\n self.pos, self.line, self.col)" "python")))
(~doc-subsection :title "3. JS: auto-transpiled"
(p "JS parser comes from bootstrap of parser.sx — spec change handles it automatically."))
(~doc-subsection :title "4. Rebootstrap both targets"
(p "Run " (code "bootstrap_js.py") " and " (code "bootstrap_py.py") " to regenerate " (code "sx-ref.js") " and " (code "sx_ref.py") " from the updated parser.sx spec."))
(~doc-subsection :title "5. Grammar update"
(p "Add reader macro syntax to the grammar comment at the top of parser.sx:")
(~doc-code :code (highlight ";; reader → '#;' expr (datum comment)\n;; | '#|' raw-chars '|' (raw string)\n;; | \"#'\" expr (quote shorthand)" "lisp"))))
;; -----------------------------------------------------------------------
;; Files
;; -----------------------------------------------------------------------
(~doc-section :title "Files" :id "files"
(div :class "overflow-x-auto rounded border border-stone-200"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-100"
(th :class "px-3 py-2 font-medium text-stone-600" "File")
(th :class "px-3 py-2 font-medium text-stone-600" "Change")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/parser.sx")
(td :class "px-3 py-2 text-stone-700" "# dispatch in read-expr, read-raw-string helper, grammar comment"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/parser.py")
(td :class "px-3 py-2 text-stone-700" "# dispatch in _parse_expr(), _read_raw_string()"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/sx_ref.py")
(td :class "px-3 py-2 text-stone-700" "Rebootstrap"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/static/scripts/sx-ref.js")
(td :class "px-3 py-2 text-stone-700" "Rebootstrap"))))))
;; -----------------------------------------------------------------------
;; Verification
;; -----------------------------------------------------------------------
(~doc-section :title "Verification" :id "verification"
(~doc-subsection :title "Parse tests"
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "#;(ignored) 42 → 42")
(li "(list 1 #;2 3) → (list 1 3)")
(li "#|hello \"world\" \\n| → string: hello \"world\" \\n (literal, no escaping)")
(li "#|multi\\nline| → string with actual newline")
(li "#'foo → (quote foo)")
(li "# at EOF → error")
(li "#x unknown → error")))
(~doc-subsection :title "Regression"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "All existing parser tests pass after changes")
(li "Rebootstrapped JS and Python pass their test suites")
(li "JS parity: SX.parse('#|hello|') returns [\"hello\"]"))))))
;; ---------------------------------------------------------------------------
;; SX-Activity: Federated SX over ActivityPub
;; ---------------------------------------------------------------------------
(defcomp ~plan-sx-activity-content ()
(~doc-page :title "SX-Activity"
(~doc-section :title "Context" :id "context"
(p "ActivityPub federates social actions using JSON-LD over HTTP. It works, but JSON-LD is verbose, requires context resolution, and carries no computation — it's inert data that every consumer must interpret from scratch. Meanwhile SX already has: a compact wire format, a safe evaluable language, content-addressed DAG execution (artdag), IPFS storage with CIDs, and OpenTimestamps Bitcoin anchoring.")
(p "SX-Activity replaces JSON-LD with s-expressions as the federation wire format. Activities become " (strong "evaluable programs") " rather than inert data. Components travel with content, stored on IPFS, resolved by content address. Media is content-addressed. Provenance is anchored in the Bitcoin chain. The result: a federation protocol where receiving a post means receiving the ability to " (em "render") " it — not just the data, but the UI.")
(p "The key insight: " (strong "if the wire format is already an evaluable language, federation becomes code distribution.") " A Create activity carrying a Note isn't just data — it's a renderable document with its own component definitions, stylesheets, and media references, all content-addressed and independently verifiable."))
(~doc-section :title "Current State" :id "current-state"
(ul :class "space-y-2 text-stone-700 list-disc pl-5"
(li (strong "ActivityPub: ") "Full implementation — virtual per-app actors, HTTP signatures, webfinger, inbox/outbox, followers/following, delivery with idempotent logging.")
(li (strong "Activity bus: ") "Unified event bus with NOTIFY/LISTEN wakeup, at-least-once delivery, handler registry keyed by (activity_type, object_type).")
(li (strong "Content addressing: ") "artdag nodes use SHA3-256 hashing. Cache layer tracks IPFS CIDs. IPFSPin model tracks pinned content across domains.")
(li (strong "Bitcoin anchoring: ") "APAnchor model — Merkle tree of activities, OpenTimestamps proof CID, Bitcoin txid. Infrastructure exists but isn't wired to all activity types.")
(li (strong "SX wire format: ") "Server serializes to SX source via _aser, client parses and renders. Component caching via localStorage + content hashes.")
(li (strong "Boundary enforcement: ") "SX_BOUNDARY_STRICT=1 validates all primitives at registration. Pure components can't do IO — safe to load from untrusted sources.")))
;; -----------------------------------------------------------------------
;; Phase 1: SX Wire Format for Activities
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 1: SX Wire Format for Activities" :id "phase-1"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What it enables")
(p :class "text-violet-800" "Activities expressed as s-expressions instead of JSON-LD. Same semantics as ActivityStreams, but compact, parseable, and directly evaluable. Dual-format support for backward compatibility with existing AP servers."))
(~doc-subsection :title "The Problem"
(p "JSON-LD activities are verbose and require context resolution:")
(~doc-code :code (highlight "{\"@context\": \"https://www.w3.org/ns/activitystreams\",\n \"type\": \"Create\",\n \"actor\": \"https://example.com/users/alice\",\n \"object\": {\n \"type\": \"Note\",\n \"content\": \"<p>Hello world</p>\",\n \"attributedTo\": \"https://example.com/users/alice\"\n }}" "json"))
(p "Every consumer parses JSON, resolves @context, extracts fields, then builds their own UI around the raw data. The content is HTML embedded in a JSON string — two formats nested, neither evaluable."))
(~doc-subsection :title "SX Activity Format"
(p "The same activity as SX:")
(~doc-code :code (highlight "(Create\n :actor \"https://example.com/users/alice\"\n :published \"2026-03-06T12:00:00Z\"\n :object (Note\n :attributed-to \"https://example.com/users/alice\"\n :content (p \"Hello world\")\n :media-type \"text/sx\"))" "lisp"))
(p "The content isn't a string containing markup — it " (em "is") " markup. The receiving server can evaluate it directly. The Note's content is a renderable SX expression."))
(~doc-subsection :title "Approach"
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Activity vocabulary in SX")
(p "Map ActivityStreams types to SX symbols. Activities are lists with a type head and keyword properties:")
(~doc-code :code (highlight ";; Core activity types\n(Create :actor ... :object ...)\n(Update :actor ... :object ...)\n(Delete :actor ... :object ...)\n(Follow :actor ... :object ...)\n(Like :actor ... :object ...)\n(Announce :actor ... :object ...)\n\n;; Object types\n(Note :content ... :attributed-to ...)\n(Article :name ... :content ... :summary ...)\n(Image :url ... :media-type ... :cid ...)\n(Collection :total-items ... :items ...)" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "2. Content negotiation")
(p "Inbox accepts both formats. " (code "Accept: text/sx") " gets SX, " (code "Accept: application/activity+json") " gets JSON-LD. Outbox serves both. SX-native servers negotiate SX; legacy Mastodon/Pleroma servers get JSON-LD as today."))
(div
(h4 :class "font-semibold text-stone-700" "3. Bidirectional translation")
(p "Lossless mapping between JSON-LD and SX activity formats. Translate at the boundary — internal processing always uses SX. The existing " (code "APActivity") " model gains an " (code "sx_source") " column storing the canonical SX representation."))
(div
(h4 :class "font-semibold text-stone-700" "4. HTTP Signatures over SX")
(p "Same RSA signature mechanism. Digest header computed over the SX body. Existing keypair infrastructure unchanged."))))
(~doc-subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Round-trip: SX → JSON-LD → SX produces identical output")
(li "Legacy AP servers receive valid JSON-LD (Mastodon can display the post)")
(li "SX-native servers receive evaluable SX (client can render directly)")
(li "HTTP signatures verify over SX bodies"))))
;; -----------------------------------------------------------------------
;; Phase 2: Content-Addressed Components on IPFS
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 2: Content-Addressed Components on IPFS" :id "phase-2"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What it enables")
(p :class "text-violet-800" "Component definitions stored on IPFS, referenced by CID. Any server can publish components. Any browser can fetch them. No central registry — content addressing IS the registry."))
(~doc-subsection :title "The Insight"
(p "SX components are pure functions — they take data and return markup. They can't do IO (boundary enforcement guarantees this). That means they're " (strong "safe to load from any source") ". And if they're content-addressed, the CID " (em "is") " the identity — you don't need to trust the source, you just verify the hash.")
(p "Currently, component definitions travel with each page via " (code "<script type=\"text/sx\" data-components>") ". Each server bundles its own. With IPFS, components become shared infrastructure — define once, use everywhere."))
(~doc-subsection :title "Approach"
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Component CID computation")
(p "Each " (code "defcomp") " definition gets a content address:")
(~doc-code :code (highlight ";; Component source\n(defcomp ~card (&key title &rest children)\n (div :class \"border rounded p-4\"\n (h2 title) children))\n\n;; CID = SHA3-256 of canonical serialized form\n;; → bafy...abc123\n;; Stored: ipfs://bafy...abc123 → component source" "lisp"))
(p "Canonical form: normalize whitespace, sort keyword args alphabetically, strip comments. Same component always produces same CID regardless of formatting."))
(div
(h4 :class "font-semibold text-stone-700" "2. Component references in activities")
(p "Activities declare which components they need by CID:")
(~doc-code :code (highlight "(Create\n :actor \"https://rose-ash.com/users/alice\"\n :requires (list\n \"bafy...card\" ;; ~card component\n \"bafy...avatar\") ;; ~avatar component\n :object (Note\n :content (~card :title \"Hello\"\n (~avatar :src \"ipfs://bafy...photo\")\n (p \"This renders with the card component.\"))))" "lisp"))
(p "The receiving browser fetches missing components from IPFS, verifies CIDs, registers them, then renders the content."))
(div
(h4 :class "font-semibold text-stone-700" "3. IPFS component resolution")
(p "Client-side resolution pipeline:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Check localStorage cache (keyed by CID — cache-forever semantics)")
(li "Check local IPFS node if running (ipfs cat)")
(li "Fetch from IPFS gateway (configurable, default: dweb.link)")
(li "Verify SHA3-256 matches CID")
(li "Parse, register in component env, render")))
(div
(h4 :class "font-semibold text-stone-700" "4. Component publication")
(p "Server-side: on component registration, compute CID and pin to IPFS. Track in " (code "IPFSPin") " model (already exists). Publish component availability via AP outbox:")
(~doc-code :code (highlight "(Create\n :actor \"https://rose-ash.com/apps/market\"\n :object (SxComponent\n :name \"~product-card\"\n :cid \"bafy...productcard\"\n :version \"1.0.0\"\n :deps (list \"bafy...card\" \"bafy...price-tag\")))" "lisp")))))
(~doc-subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Component pinned to IPFS → fetchable via gateway → CID verifies")
(li "Browser renders federated post using IPFS-fetched components")
(li "Modified component → different CID → old content still renders with old version")
(li "Boundary enforcement: IPFS-loaded component cannot call IO primitives"))))
;; -----------------------------------------------------------------------
;; Phase 3: Federated Media & Content Store
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 3: Federated Media & Content Store" :id "phase-3"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What it enables")
(p :class "text-violet-800" "All media (images, video, audio, DAG outputs) stored content-addressed on IPFS. Activities reference media by CID. No hotlinking, no broken links, no dependence on the origin server staying online."))
(~doc-subsection :title "Current Mechanism"
(p "artdag already content-addresses all DAG outputs with SHA3-256 and tracks IPFS CIDs in " (code "IPFSPin") ". But media in the web platform (blog images, product photos, event banners) is stored as regular files on the origin server. Federated posts include " (code "url") " fields pointing to the origin — if the server goes down, the media is gone."))
(~doc-subsection :title "Approach"
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Media CID pipeline")
(p "On upload: hash content → pin to IPFS → store CID in database. Activities reference media by CID alongside URL fallback:")
(~doc-code :code (highlight "(Image\n :cid \"bafy...photo123\"\n :url \"https://rose-ash.com/media/photo.jpg\" ;; fallback\n :media-type \"image/jpeg\"\n :width 1200 :height 800)" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "2. DAG output federation")
(p "artdag processing results (rendered video, processed images) already have CIDs. Federate them as activities:")
(~doc-code :code (highlight "(Create\n :actor \"https://rose-ash.com/users/alice\"\n :object (Artwork\n :name \"Sunset Remix\"\n :cid \"bafy...artwork\"\n :dag-cid \"bafy...dag\" ;; full DAG for reproduction\n :media-type \"video/mp4\"\n :sources (list\n (Image :cid \"bafy...src1\" :attribution \"...\")\n (Image :cid \"bafy...src2\" :attribution \"...\"))))" "lisp"))
(p "The " (code ":dag-cid") " lets anyone re-execute the processing pipeline. The artwork is both a result and a reproducible recipe."))
(div
(h4 :class "font-semibold text-stone-700" "3. Shared SX content store")
(p "Not just components and media — full page content can be content-addressed. An Article's body is SX, pinned to IPFS:")
(~doc-code :code (highlight "(Article\n :name \"Why S-Expressions\"\n :content-cid \"bafy...article-body\" ;; SX source on IPFS\n :requires (list \"bafy...doc-page\" \"bafy...code-block\")\n :summary \"Why SX uses s-expressions instead of HTML.\")" "lisp"))
(p "The content outlives the server. Anyone with the CID can fetch, parse, and render the article with its original components."))
(div
(h4 :class "font-semibold text-stone-700" "4. Progressive resolution")
(p "Client resolves content progressively:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Inline content renders immediately")
(li "CID-referenced content shows placeholder → fetches from IPFS → renders")
(li "Large media uses IPFS streaming (chunked CIDs)")
(li "Integrates with Phase 5 of isomorphic plan (streaming/suspense)")))))
(~doc-subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Origin server offline → content still resolvable via IPFS gateway")
(li "DAG CID → re-executing DAG produces identical output")
(li "Media CID verifies → tampered content rejected"))))
;; -----------------------------------------------------------------------
;; Phase 4: Component Registry & Discovery
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 4: Component Registry & Discovery" :id "phase-4"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What it enables")
(p :class "text-violet-800" "Federated component discovery. Servers publish component collections. Other servers follow component feeds. Like npm, but federated, content-addressed, and the packages are safe to run (pure functions, no IO)."))
(~doc-subsection :title "Approach"
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Component collections as AP actors")
(p "Each server exposes a component registry actor:")
(~doc-code :code (highlight "(Service\n :id \"https://rose-ash.com/sx-registry\"\n :type \"SxComponentRegistry\"\n :name \"Rose Ash Components\"\n :outbox \"https://rose-ash.com/sx-registry/outbox\"\n :followers \"https://rose-ash.com/sx-registry/followers\")" "lisp"))
(p "Follow the registry to receive component updates. The outbox is a chronological feed of Create/Update/Delete activities for components."))
(div
(h4 :class "font-semibold text-stone-700" "2. Component metadata")
(~doc-code :code (highlight "(SxComponent\n :name \"~data-table\"\n :cid \"bafy...datatable\"\n :version \"2.1.0\"\n :deps (list \"bafy...sortable\" \"bafy...paginator\")\n :params (list\n (dict :name \"rows\" :type \"list\" :required true)\n (dict :name \"columns\" :type \"list\" :required true)\n (dict :name \"sortable\" :type \"boolean\" :default false))\n :css-atoms (list :border :rounded :p-4 :text-sm)\n :preview-cid \"bafy...screenshot\"\n :license \"MIT\")" "lisp"))
(p "Dependencies are transitive CID references. CSS atoms declare which CSSX rules the component needs. Preview CID is a screenshot for registry browsing."))
(div
(h4 :class "font-semibold text-stone-700" "3. Discovery protocol")
(p "Webfinger-style lookup for components by name:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Local registry: check own component env first")
(li "Followed registries: check cached feeds from followed registries")
(li "Global search: query known registries by component name")
(li "CID resolution: if you have a CID, skip discovery — fetch directly from IPFS")))
(div
(h4 :class "font-semibold text-stone-700" "4. Version resolution")
(p "Components are immutable (CID = identity). \"Updating\" a component publishes a new CID. Activities reference specific CIDs, so old content always renders correctly. The registry tracks version history:")
(~doc-code :code (highlight "(Update\n :actor \"https://rose-ash.com/sx-registry\"\n :object (SxComponent\n :name \"~card\"\n :cid \"bafy...card-v2\" ;; new version\n :replaces \"bafy...card-v1\" ;; previous version\n :changelog \"Added subtitle slot\"))" "lisp")))))
(~doc-subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Follow registry → receive component Create activities → components available locally")
(li "Render post using component from foreign registry → works")
(li "Old post referencing old CID → still renders correctly with old version"))))
;; -----------------------------------------------------------------------
;; Phase 5: Bitcoin-Anchored Provenance
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 5: Bitcoin-Anchored Provenance" :id "phase-5"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What it enables")
(p :class "text-violet-800" "Cryptographic proof that content existed at a specific time, authored by a specific actor. Leverages the existing APAnchor/OpenTimestamps infrastructure. Unforgeable, independently verifiable, survives server shutdown."))
(~doc-subsection :title "Current Mechanism"
(p "The " (code "APAnchor") " model already batches activities into Merkle trees, stores the tree on IPFS, creates an OpenTimestamps proof, and records the Bitcoin txid. This runs but isn't surfaced to users or integrated with the full activity lifecycle."))
(~doc-subsection :title "Approach"
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Automatic anchoring pipeline")
(p "Every SX activity gets queued for anchoring. Batch processor runs periodically:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Collect pending activities (content CIDs + actor signatures)")
(li "Build Merkle tree of activity hashes")
(li "Pin Merkle tree to IPFS → tree CID")
(li "Submit tree root to OpenTimestamps calendar servers")
(li "When Bitcoin confirmation arrives: store txid, update activities with anchor reference")))
(div
(h4 :class "font-semibold text-stone-700" "2. Provenance chain in activities")
(~doc-code :code (highlight "(Create\n :actor \"https://rose-ash.com/users/alice\"\n :object (Note :content (p \"Hello\") :cid \"bafy...note\")\n :provenance (Anchor\n :tree-cid \"bafy...merkle-tree\"\n :leaf-index 42\n :ots-cid \"bafy...ots-proof\"\n :btc-txid \"abc123...def\"\n :btc-block 890123\n :anchored-at \"2026-03-06T12:00:00Z\"))" "lisp"))
(p "Any party can verify: fetch the OTS proof from IPFS, check the Merkle path from the activity's CID to the tree root, confirm the tree root is committed in the Bitcoin block."))
(div
(h4 :class "font-semibold text-stone-700" "3. Component provenance")
(p "Components published to the registry also get anchored. This proves authorship and publication time:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "\"This component was published by rose-ash.com at block 890123\"")
(li "Prevents backdating — can't claim you published a component before you actually did")
(li "License disputes resolvable by checking anchor timestamps")))
(div
(h4 :class "font-semibold text-stone-700" "4. Verification UI")
(p "Client-side provenance badge on federated content:")
(~doc-code :code (highlight "(defcomp ~provenance-badge (&key anchor)\n (when anchor\n (details :class \"inline text-xs text-stone-400\"\n (summary \"✓ Anchored\")\n (dl :class \"mt-1 space-y-1\"\n (dt \"Bitcoin block\") (dd (get anchor \"btc-block\"))\n (dt \"Timestamp\") (dd (get anchor \"anchored-at\"))\n (dt \"Proof\") (dd (a :href (str \"ipfs://\" (get anchor \"ots-cid\"))\n \"OTS proof\"))))))" "lisp")))))
(~doc-subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Activity anchored → OTS proof fetchable from IPFS → Merkle path validates → txid confirms in Bitcoin")
(li "Tampered activity → Merkle proof fails → provenance badge shows ✗")
(li "Server goes offline → provenance still verifiable (all proofs on IPFS + Bitcoin)"))))
;; -----------------------------------------------------------------------
;; Phase 6: The Evaluable Web
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 6: The Evaluable Web" :id "phase-6"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What it enables")
(p :class "text-violet-800" "The full vision: federation where content carries its own rendering, components are shared infrastructure, media is permanent, and authorship is cryptographically proven. A web that evaluates rather than merely displays."))
(~doc-subsection :title "Synthesis"
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Self-rendering posts")
(p "A federated post is a complete rendering program. It carries content (SX), references components (by CID), references media (by CID), and includes provenance (Bitcoin anchor). The receiving browser needs nothing beyond the SX evaluator to render it.")
(p "No framework lock-in. No CSS framework dependency. No build step. The post is its own application."))
(div
(h4 :class "font-semibold text-stone-700" "2. Collaborative component ecosystem")
(p "Multiple servers contribute components to the shared IPFS pool. A blogging platform publishes ~article-layout. A commerce platform publishes ~product-card. A social network publishes ~thread-view. Any post can compose components from different origins — they're all just pure functions over data."))
(div
(h4 :class "font-semibold text-stone-700" "3. Permanent content")
(p "Every artifact — content, components, media, provenance proofs — is content-addressed and available on IPFS. The " (em "origin server is optional") " after publication. Content survives server shutdown, domain expiry, and platform abandonment."))
(div
(h4 :class "font-semibold text-stone-700" "4. Client as universal renderer")
(p "The SX client becomes a universal content renderer. It doesn't need to know about specific servers or APIs. Given a CID, it can fetch content, resolve components, verify provenance, and render — all from IPFS. The browser becomes a " (em "federation peer") " rather than a passive consumer.")))))
;; -----------------------------------------------------------------------
;; Cross-Cutting Concerns
;; -----------------------------------------------------------------------
(~doc-section :title "Cross-Cutting Concerns" :id "cross-cutting"
(~doc-subsection :title "Security"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "Boundary enforcement is the foundation. ") "IPFS-fetched components are parsed and registered like any other component. SX_BOUNDARY_STRICT ensures they can't call IO primitives. A malicious component can produce ugly markup but can't exfiltrate data or make network requests.")
(li (strong "CID verification: ") "Content fetched from IPFS is hashed and compared to the expected CID before use. Tampered content is rejected.")
(li (strong "Signature chain: ") "Actor signatures (RSA/HTTP Signatures) prove authorship. Bitcoin anchors prove timing. Together they establish non-repudiable provenance.")
(li (strong "Resource limits: ") "Evaluation of untrusted components runs with step limits (max eval steps, max recursion depth). Infinite loops are caught and terminated.")))
(~doc-subsection :title "Backward Compatibility"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Content negotiation ensures legacy AP servers always receive valid JSON-LD")
(li "SX-Activity is strictly opt-in — servers that don't understand it get standard AP")
(li "Existing internal activity bus unchanged — SX format is for federation, not internal events")
(li "URL fallbacks on all media references — CID is preferred, URL is fallback")))
(~doc-subsection :title "Performance"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Component CIDs cached in localStorage forever (content-addressed = immutable)")
(li "IPFS gateway responses cached with long TTL (content can't change)")
(li "Local IPFS node (if present) eliminates gateway latency")
(li "Provenance verification is lazy — badge shows unverified until user clicks to verify")))
(~doc-subsection :title "Integration with Isomorphic Architecture"
(p "SX-Activity builds on the isomorphic architecture plan:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Phase 1 (component distribution) → IPFS replaces per-server bundles")
(li "Phase 2 (IO detection) → pure components safe for IPFS publication")
(li "Phase 3 (client routing) → client can resolve federated content without server")
(li "Phase 5 (streaming/suspense) → progressive IPFS resolution uses same infrastructure"))))
;; -----------------------------------------------------------------------
;; Critical Files
;; -----------------------------------------------------------------------
(~doc-section :title "Critical Files" :id "critical-files"
(div :class "overflow-x-auto rounded border border-stone-200"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-100"
(th :class "px-3 py-2 font-medium text-stone-600" "File")
(th :class "px-3 py-2 font-medium text-stone-600" "Role")
(th :class "px-3 py-2 font-medium text-stone-600" "Phases")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/infrastructure/activitypub.py")
(td :class "px-3 py-2 text-stone-700" "AP blueprint — add SX content negotiation")
(td :class "px-3 py-2 text-stone-600" "1"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/events/bus.py")
(td :class "px-3 py-2 text-stone-700" "Activity bus — add sx_source column, SX serialization")
(td :class "px-3 py-2 text-stone-600" "1"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/activity.py")
(td :class "px-3 py-2 text-stone-700" "SX ↔ JSON-LD bidirectional translation (new)")
(td :class "px-3 py-2 text-stone-600" "1"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ipfs.py")
(td :class "px-3 py-2 text-stone-700" "Component CID computation, IPFS pinning (new)")
(td :class "px-3 py-2 text-stone-600" "2, 3"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/ipfs-resolve.sx")
(td :class "px-3 py-2 text-stone-700" "Client-side IPFS resolution spec (new)")
(td :class "px-3 py-2 text-stone-600" "2, 3"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/models/federation.py")
(td :class "px-3 py-2 text-stone-700" "IPFSPin, APAnchor models — extend for components")
(td :class "px-3 py-2 text-stone-600" "2, 3, 5"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/registry.py")
(td :class "px-3 py-2 text-stone-700" "Component registry actor, discovery protocol (new)")
(td :class "px-3 py-2 text-stone-600" "4"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/anchor.py")
(td :class "px-3 py-2 text-stone-700" "Anchoring pipeline — wire to activity lifecycle (new)")
(td :class "px-3 py-2 text-stone-600" "5"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/boot.sx")
(td :class "px-3 py-2 text-stone-700" "Client boot — IPFS component loading")
(td :class "px-3 py-2 text-stone-600" "2, 6"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/events/handlers/ap_delivery_handler.py")
(td :class "px-3 py-2 text-stone-700" "Federation delivery — SX format support")
(td :class "px-3 py-2 text-stone-600" "1"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "artdag/core/artdag/cache.py")
(td :class "px-3 py-2 text-stone-700" "Content addressing — shared with component CIDs")
(td :class "px-3 py-2 text-stone-600" "2, 3"))))))))
;; ---------------------------------------------------------------------------
;; Isomorphic Architecture Roadmap
;; ---------------------------------------------------------------------------

View File

@@ -414,4 +414,6 @@
:selected (or (find-current plans-nav-items slug) ""))
:content (case slug
"isomorphic-architecture" (~plan-isomorphic-content)
"reader-macros" (~plan-reader-macros-content)
"sx-activity" (~plan-sx-activity-content)
:else (~plans-index-content)))