Files
rose-ash/sx/sx/plans.sx
giles 59a8d2063d Expand SX-Activity plan: the evaluable web
Reframe Phase 6 from "ActivityPub but SX" to the full vision: a new web
where content, components, parsers, transforms, server/client logic, and
media all share one executable format on IPFS, sandboxed by boundary
enforcement, with Bitcoin-anchored provenance. Updated context section
and nav summary to match.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:17:21 +00:00

945 lines
73 KiB
Plaintext

;; Plans section — architecture roadmaps and implementation plans
;; ---------------------------------------------------------------------------
;; Plans index page
;; ---------------------------------------------------------------------------
(defcomp ~plans-index-content ()
(~doc-page :title "Plans"
(div :class "space-y-4"
(p :class "text-lg text-stone-600 mb-4"
"Architecture roadmaps and implementation plans for SX.")
(div :class "space-y-3"
(map (fn (item)
(a :href (get item "href")
:sx-get (get item "href") :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
:class "block rounded border border-stone-200 p-4 hover:border-violet-300 hover:bg-violet-50 transition-colors"
(div :class "font-semibold text-stone-800" (get item "label"))
(when (get item "summary")
(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 "The web is six incompatible formats duct-taped together: HTML for structure, CSS for style, JavaScript for behavior, JSON for data, server languages for backend logic, build tools for compilation. Moving anything between layers requires serialization, template languages, API contracts, and glue code. Federation (ActivityPub) adds a seventh — JSON-LD — which is inert data that every consumer must interpret from scratch and wrap in their own UI.")
(p "SX is already one evaluable format that does all six. A component definition is simultaneously structure, style (CSSX atoms), behavior (event handlers), data (the AST " (em "is") " data), server-renderable (Python evaluator), and client-renderable (JS evaluator). The pieces already exist: content-addressed DAG execution (artdag), IPFS storage with CIDs, OpenTimestamps Bitcoin anchoring, boundary-enforced sandboxing.")
(p "SX-Activity wires these together into a new web. Everything — content, UI components, markdown parsers, syntax highlighters, validation logic, media, processing pipelines — is the same executable format, stored on a content-addressed network, running within each participant's own security context. " (strong "The wire format is the programming language is the component system is the package manager.")))
(~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 this really is")
(p :class "text-violet-800" "Not ActivityPub-with-SX. A new web. One where everything — content, components, parsers, renderers, server logic, client logic — is the same executable format, shared on a content-addressed network, running within each participant's own security context."))
(~doc-subsection :title "The insight"
(p "The web has six layers that don't talk to each other: HTML (structure), CSS (style), JavaScript (behavior), JSON (data interchange), server frameworks (backend logic), and build tools (compilation). Each has its own syntax, its own semantics, its own ecosystem. Moving data between them requires serialization, deserialization, template languages, API contracts, type coercion, and an endless parade of glue code.")
(p "SX collapses all six into one evaluable format. A component definition is simultaneously structure, style (CSSX atoms), behavior (event handlers), data (the AST is data), server-renderable (Python evaluator), and client-renderable (JS evaluator). There is no boundary between \"data\" and \"program\" — s-expressions are both.")
(p "Once that's true, " (strong "everything becomes shareable.") " Not just UI components — markdown parsers, syntax highlighters, date formatters, validation logic, layout algorithms, color systems, animation curves. Any pure function over data. All content-addressed, all on IPFS, all executable within your own security context."))
(~doc-subsection :title "What travels on the network"
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "Content")
(p "Blog posts, product listings, event descriptions, social media posts. Not HTML strings embedded in JSON — live SX expressions that evaluate to rendered UI. The content " (em "is") " the application."))
(div
(h4 :class "font-semibold text-stone-700" "Components")
(p "UI building blocks: cards, tables, forms, navigation, media players. Published to IPFS, referenced by CID. A commerce site publishes " (code "~product-card") ". A blogging platform publishes " (code "~article-layout") ". A social network publishes " (code "~thread-view") ". Anyone can compose them. They're pure functions — safe to load from anywhere."))
(div
(h4 :class "font-semibold text-stone-700" "Parsers and transforms")
(p "A markdown parser is just an SX function: takes a string, returns an SX tree. Publish it to IPFS. Now anyone can use your markdown dialect. Same for: syntax highlighters, BBCode parsers, wiki markup, LaTeX subsets, CSV-to-table converters, JSON-to-SX adapters. " (strong "The parser ecosystem becomes shared infrastructure."))
(~doc-code :code (highlight ";; A markdown parser, published to IPFS\n;; CID: bafy...md-parser\n(define parse-markdown\n (fn (source)\n ;; tokenize → build AST → return SX tree\n ;; (parse-markdown \"# Hello\\n**bold**\")\n ;; → (h1 \"Hello\") (p (strong \"bold\"))\n ...))\n\n;; Anyone can use it in their components\n(defcomp ~blog-post (&key markdown-source)\n (div :class \"prose\"\n (parse-markdown markdown-source)))" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "Server-side and client-side logic")
(p "The same SX code runs on either side. A validation function published to IPFS runs server-side in Python for form processing and client-side in JavaScript for instant feedback. A price calculator runs server-side for order totals and client-side for live previews. " (em "The server/client split is a deployment decision, not a language boundary.")))
(div
(h4 :class "font-semibold text-stone-700" "Media and processing pipelines")
(p "Images, video, audio — all content-addressed on IPFS. But also the " (em "processing pipelines") " that created them. artdag DAGs are SX. Publish a DAG CID alongside the output CID and anyone can verify the provenance, re-render at different resolution, or fork the pipeline for their own work."))))
(~doc-subsection :title "The security model"
(p "This only works because of boundary enforcement. Every piece of SX fetched from the network runs within the receiver's security context:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "Pure functions can't do IO. ") "A component from IPFS can produce markup — it cannot read your cookies, make network requests, access localStorage, or call any IO primitive. The boundary spec (boundary.sx) is enforced at registration time. This isn't a policy — it's structural. The evaluator literally doesn't have IO primitives available when running untrusted code.")
(li (strong "IO requires explicit grant. ") "Only locally-registered IO primitives (query, frag, current-user) have access to server resources. Fetched components never see them. The host decides what capabilities to grant.")
(li (strong "Step limits cap computation. ") "Untrusted code runs with configurable eval step limits. No infinite loops, no resource exhaustion. Exceeding the limit halts evaluation and returns an error node.")
(li (strong "Content addressing prevents tampering. ") "You asked for CID X, you got CID X, the hash proves it. No MITM, no CDN poisoning, no supply chain attacks on the content itself.")
(li (strong "Provenance proves authorship. ") "Bitcoin-anchored timestamps prove who published what and when. Not \"trust me\" — independently verifiable against the Bitcoin blockchain."))
(p "This is the opposite of the npm model. npm packages run with full access to your system — a malicious package can exfiltrate secrets, install backdoors, modify the filesystem. SX components are structurally sandboxed. The worst a malicious component can do is render a " (code "(div \"haha got you\")") "."))
(~doc-subsection :title "What this replaces"
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
(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" "Current web")
(th :class "px-3 py-2 font-medium text-stone-600" "SX web")
(th :class "px-3 py-2 font-medium text-stone-600" "Why")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "npm / package registries")
(td :class "px-3 py-2 text-stone-700" "IPFS + component CIDs")
(td :class "px-3 py-2 text-stone-600" "Content-addressed, no central authority, structurally safe"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "CDNs for JS/CSS")
(td :class "px-3 py-2 text-stone-700" "IPFS gateways")
(td :class "px-3 py-2 text-stone-600" "Permanent, decentralized, self-verifying"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "REST/GraphQL APIs")
(td :class "px-3 py-2 text-stone-700" "SX activities over AP")
(td :class "px-3 py-2 text-stone-600" "Responses are evaluable, not just data"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "HTML + CSS + JS")
(td :class "px-3 py-2 text-stone-700" "SX (one format)")
(td :class "px-3 py-2 text-stone-600" "No impedance mismatch, same evaluator everywhere"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Build tools (webpack, vite)")
(td :class "px-3 py-2 text-stone-700" "Nothing")
(td :class "px-3 py-2 text-stone-600" "SX evaluates directly, no compilation step"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Template languages")
(td :class "px-3 py-2 text-stone-700" "Nothing")
(td :class "px-3 py-2 text-stone-600" "SX is the template and the language"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "JSON-LD federation")
(td :class "px-3 py-2 text-stone-700" "SX federation")
(td :class "px-3 py-2 text-stone-600" "Wire format is executable, content renders itself"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Trust-based package security")
(td :class "px-3 py-2 text-stone-700" "Structural sandboxing")
(td :class "px-3 py-2 text-stone-600" "Pure functions can't have side effects — not by policy, by construction"))))))
(~doc-subsection :title "The end state"
(p "A browser with an SX evaluator and an IPFS gateway is a complete web platform. Given a CID — for a page, a post, an application — it can:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Fetch the content from IPFS")
(li "Resolve component dependencies (also from IPFS)")
(li "Resolve media (also from IPFS)")
(li "Evaluate the content (pure computation, sandboxed)")
(li "Render to DOM")
(li "Verify provenance (Bitcoin anchor)")
(li "Cache everything forever (content-addressed = immutable)"))
(p "No server needed. No DNS. No TLS certificates. No hosting provider. No build step. No framework. Just content-addressed s-expressions evaluating in a sandbox.")
(p "The server becomes optional infrastructure for " (em "IO") " — database queries, authentication, payment processing, real-time events. Everything else lives on the content-addressed network. The web stops being a collection of servers you visit and becomes a " (strong "shared evaluable space") " you participate in.")))
;; -----------------------------------------------------------------------
;; 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
;; ---------------------------------------------------------------------------
(defcomp ~plan-isomorphic-content ()
(~doc-page :title "Isomorphic Architecture Roadmap"
(~doc-section :title "Context" :id "context"
(p "SX has a working server-client pipeline: server evaluates pages with IO (DB, fragments), serializes as SX wire format, client parses and renders to DOM. The language and primitives are already isomorphic " (em "— same spec, same semantics, both sides.") " What's missing is the " (strong "plumbing") " that makes the boundary between server and client a sliding window rather than a fixed wall.")
(p "The key insight: " (strong "s-expressions can partially unfold on the server after IO, then finish unfolding on the client.") " The system should be clever enough to know which downstream components have data fetches, resolve those server-side, and send the rest as pure SX for client rendering. Eventually, the client can also do IO (mapping server DB queries to REST calls), handle routing (SPA), and even work offline with cached data."))
(~doc-section :title "Current State" :id "current-state"
(ul :class "space-y-2 text-stone-700 list-disc pl-5"
(li (strong "Primitive parity: ") "100%. ~80 pure primitives, same names/semantics, JS and Python.")
(li (strong "eval/parse/render: ") "Complete both sides. sx-ref.js has eval, parse, render-to-html, render-to-dom, aser.")
(li (strong "Engine: ") "engine.sx (morph, swaps, triggers, history), orchestration.sx (fetch, events), boot.sx (hydration) — all transpiled.")
(li (strong "Wire format: ") "Server _aser → SX source → client parses → renders to DOM. Boundary is clean.")
(li (strong "Component caching: ") "Hash-based localStorage for component definitions and style dictionaries.")
(li (strong "CSS on-demand: ") "CSSX resolves keywords to CSS rules, injects only used rules.")
(li (strong "Boundary enforcement: ") "boundary.sx + SX_BOUNDARY_STRICT=1 validates all primitives/IO/helpers at registration.")))
;; -----------------------------------------------------------------------
;; Phase 1
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 1: Component Distribution & Dependency Analysis" :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" "Per-page component bundles instead of sending every definition to every page. Smaller payloads, faster boot, better cache hit rates."))
(~doc-subsection :title "The Problem"
(p "client_components_tag() in jinja_bridge.py serializes ALL entries in _COMPONENT_ENV. The sx_page() template sends everything or nothing based on a single global hash. No mechanism determines which components a page actually needs."))
(~doc-subsection :title "Approach"
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Transitive closure analyzer")
(p "New module shared/sx/deps.py:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Walk Component.body AST, collect all Symbol refs starting with ~")
(li "Recursively follow into their bodies")
(li "Handle control forms (if/when/cond/case) — include ALL branches")
(li "Handle macros — expand during walk using limited eval"))
(~doc-code :code (highlight "def transitive_deps(name: str, env: dict) -> set[str]:\n \"\"\"Compute transitive component dependencies.\"\"\"\n seen = set()\n def walk(n):\n if n in seen: return\n seen.add(n)\n comp = env.get(n)\n if comp:\n for dep in _scan_ast(comp.body):\n walk(dep)\n walk(name)\n return seen - {name}" "python")))
(div
(h4 :class "font-semibold text-stone-700" "2. Runtime component scanning")
(p "After _aser serializes page content, scan the SX string for (~name patterns (parallel to existing scan_classes_from_sx for CSS). Then compute transitive closure to get sub-components."))
(div
(h4 :class "font-semibold text-stone-700" "3. Per-page component block")
(p "In sx_page() — replace all-or-nothing with page-specific bundle. Hash changes per page, localStorage cache keyed by route pattern."))
(div
(h4 :class "font-semibold text-stone-700" "4. SX partial responses")
(p "components_for_request() already diffs against SX-Components header. Enhance with transitive closure so only truly needed missing components are sent."))))
(~doc-subsection :title "Files"
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "New: shared/sx/deps.py — dependency analysis")
(li "shared/sx/jinja_bridge.py — per-page bundle generation")
(li "shared/sx/helpers.py — modify sx_page() and sx_response()")
(li "shared/sx/types.py — add deps: set[str] to Component")
(li "shared/sx/ref/boot.sx — per-page component caching")))
(~doc-subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Page using 5/50 components → data-components block contains only those 5 + transitive deps")
(li "No \"Unknown component\" errors after bundle reduction")
(li "Payload size reduction measurable"))))
;; -----------------------------------------------------------------------
;; Phase 2
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 2: Smart Server/Client Boundary" :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" "Formalized partial evaluation model. Server evaluates IO, serializes pure subtrees. The system automatically knows \"this component needs server data\" vs \"this component is pure and can render anywhere.\""))
(~doc-subsection :title "Current Mechanism"
(p "_aser in async_eval.py already does partial evaluation — IO primitives are awaited and substituted, HTML tags and component calls serialize as SX. The _expand_components context var controls expansion. But this is a global toggle, not per-component."))
(~doc-subsection :title "Approach"
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Automatic IO detection")
(p "Extend Phase 1 AST walker to check for references to IO_PRIMITIVES names (frag, query, service, current-user, etc.).")
(~doc-code :code (highlight "def has_io_deps(name: str, env: dict) -> bool:\n \"\"\"True if component transitively references any IO primitive.\"\"\"\n ..." "python")))
(div
(h4 :class "font-semibold text-stone-700" "2. Component metadata")
(~doc-code :code (highlight "ComponentMeta:\n deps: set[str] # transitive component deps (Phase 1)\n io_refs: set[str] # IO primitive names referenced\n is_pure: bool # True if io_refs empty (transitively)" "python")))
(div
(h4 :class "font-semibold text-stone-700" "3. Selective expansion")
(p "Refine _aser: instead of checking a global _expand_components flag, check the component's is_pure metadata:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "IO-dependent → expand server-side (IO must resolve)")
(li "Pure → serialize for client (let client render)")
(li "Explicit override: :server true on defcomp forces server expansion")))
(div
(h4 :class "font-semibold text-stone-700" "4. Data manifest for pages")
(p "PageDef produces a declaration of what IO the page needs, enabling Phase 3 (client can prefetch data) and Phase 5 (streaming)."))))
(~doc-subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Components calling (query ...) classified IO-dependent; pure components classified pure")
(li "Existing pages produce identical output (regression)"))))
;; -----------------------------------------------------------------------
;; Phase 3
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 3: Client-Side Routing (SPA Mode)" :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" "After initial page load, client resolves routes locally using cached components + data. Only hits server for fresh data or unknown routes. Like Next.js client-side navigation."))
(~doc-subsection :title "Current Mechanism"
(p "All routing is server-side via defpage → Quart routes. Client navigates via sx-boost links doing sx-get + morphing. Every navigation = server roundtrip."))
(~doc-subsection :title "Approach"
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Client-side page registry")
(p "Serialize defpage routing info to client:")
(~doc-code :code (highlight "(script :type \"text/sx-pages\")\n;; {\"docs-page\": {\"path\": \"/docs/:slug\", \"auth\": \"public\",\n;; \"content\": \"(case slug ...)\", \"data\": null}}" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "2. Client route matcher")
(p "New spec file shared/sx/ref/router.sx — convert /docs/<slug> patterns to matchers. On boost-link click: match URL → if found and pure, evaluate locally. If IO needed: fetch data from server, evaluate content locally. No match: fall through to standard fetch."))
(div
(h4 :class "font-semibold text-stone-700" "3. Data endpoint")
(~doc-code :code (highlight "GET /internal/page-data/<page-name>?<params>\n# Returns JSON with evaluated :data expression\n# Reuses execute_page() logic, stops after :data step" "python")))
(div
(h4 :class "font-semibold text-stone-700" "4. Layout caching")
(p "Layouts depend on auth/fragments, so cache current layout and reuse across navigations. SX-Layout-Hash header tracks staleness."))
(div
(h4 :class "font-semibold text-stone-700" "5. Integration with orchestration.sx")
(p "Intercept bind-boost-link to try client-side resolution first."))))
(div :class "rounded border border-amber-200 bg-amber-50 p-3 mt-2"
(p :class "text-amber-800 text-sm" (strong "Depends on: ") "Phase 1 (client knows which components each page needs), Phase 2 (which pages are pure vs IO)."))
(~doc-subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Pure page navigation: zero server requests")
(li "IO page navigation: exactly one data request (not full page fetch)")
(li "Browser back/forward works with client-resolved routes")
(li "Disabling client registry → identical behavior to current"))))
;; -----------------------------------------------------------------------
;; Phase 4
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 4: Client Async & IO Bridge" :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" "Client evaluates IO primitives by mapping them to server REST calls. Same SX code, different transport. (query \"market\" \"products\" :ids \"1,2,3\") on server → DB; on client → fetch(\"/internal/data/products?ids=1,2,3\")."))
(~doc-subsection :title "Approach"
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Async client evaluator")
(p "Two possible mechanisms:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "Promise-based: ") "evalExpr returns value or Promise; rendering awaits")
(li (strong "Continuation-based: ") "use existing shift/reset to suspend on IO, resume when data arrives (architecturally cleaner, leverages existing spec)")))
(div
(h4 :class "font-semibold text-stone-700" "2. IO primitive bridge")
(p "Register async IO primitives in client PRIMITIVES:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "query → fetch to /internal/data/")
(li "service → fetch to target service internal endpoint")
(li "frag → fetch fragment HTML")
(li "current-user → cached from initial page load")))
(div
(h4 :class "font-semibold text-stone-700" "3. Client data cache")
(p "Keyed by (service, query, params-hash), configurable TTL, server can invalidate via SX-Invalidate header."))
(div
(h4 :class "font-semibold text-stone-700" "4. Optimistic updates")
(p "Extend existing apply-optimistic/revert-optimistic in engine.sx from DOM-level to data-level."))))
(div :class "rounded border border-amber-200 bg-amber-50 p-3 mt-2"
(p :class "text-amber-800 text-sm" (strong "Depends on: ") "Phase 2 (IO affinity), Phase 3 (routing for when to trigger IO)."))
(~doc-subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Client (query ...) returns identical data to server-side")
(li "Data cache prevents redundant fetches")
(li "Same component source → identical output on either side"))))
;; -----------------------------------------------------------------------
;; Phase 5
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 5: Streaming & Suspense" :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" "Server streams partially-evaluated SX as IO resolves. Client renders available subtrees immediately, fills in suspended parts. Like React Suspense but built on delimited continuations."))
(~doc-subsection :title "Approach"
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Continuation-based suspension")
(p "When _aser encounters IO during slot evaluation, emit a placeholder with a suspension ID, schedule async resolution:")
(~doc-code :code (highlight "(~suspense :id \"placeholder-123\"\n :fallback (div \"Loading...\"))" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "2. Chunked transfer")
(p "Quart async generator responses:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "First chunk: HTML shell + synchronous content + placeholders")
(li "Subsequent chunks: <script> tags replacing placeholders with resolved content")))
(div
(h4 :class "font-semibold text-stone-700" "3. Client suspension rendering")
(p "~suspense component renders fallback, listens for resolution via inline script or SSE (existing SSE infrastructure in orchestration.sx)."))
(div
(h4 :class "font-semibold text-stone-700" "4. Priority-based IO")
(p "Above-fold content resolves first. All IO starts concurrently (asyncio.create_task), results flushed in priority order."))))
(div :class "rounded border border-amber-200 bg-amber-50 p-3 mt-2"
(p :class "text-amber-800 text-sm" (strong "Depends on: ") "Phase 4 (client async for filling suspended subtrees), Phase 2 (IO analysis for priority).")))
;; -----------------------------------------------------------------------
;; Phase 6
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 6: Full Isomorphism" :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" "Same SX code runs on either side. Runtime chooses optimal split. Offline-first with cached data + client eval."))
(~doc-subsection :title "Approach"
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Runtime boundary optimizer")
(p "Given component tree + IO dependency graph, decide per-component: server-expand, client-render, or stream. Planning step cached at registration, recomputed on component change."))
(div
(h4 :class "font-semibold text-stone-700" "2. Affinity annotations")
(~doc-code :code (highlight "(defcomp ~product-grid (&key products)\n :affinity :client ;; interactive, prefer client\n ...)\n\n(defcomp ~auth-menu (&key user)\n :affinity :server ;; auth-sensitive, always server\n ...)" "lisp"))
(p "Default: auto (runtime decides from IO analysis)."))
(div
(h4 :class "font-semibold text-stone-700" "3. Offline data layer")
(p "Service Worker intercepts /internal/data/ requests, serves from IndexedDB when offline, syncs when back online."))
(div
(h4 :class "font-semibold text-stone-700" "4. Isomorphic testing")
(p "Evaluate same component on Python and JS, compare output. Extends existing test_sx_ref.py cross-evaluator comparison."))
(div
(h4 :class "font-semibold text-stone-700" "5. Universal page descriptor")
(p "defpage is portable: server executes via execute_page(), client executes via route match → fetch data → eval content → render DOM. Same descriptor, different execution environment."))))
(div :class "rounded border border-amber-200 bg-amber-50 p-3 mt-2"
(p :class "text-amber-800 text-sm" (strong "Depends on: ") "All previous phases.")))
;; -----------------------------------------------------------------------
;; Cross-Cutting Concerns
;; -----------------------------------------------------------------------
(~doc-section :title "Cross-Cutting Concerns" :id "cross-cutting"
(~doc-subsection :title "Error Reporting"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Phase 1: \"Unknown component\" includes which page expected it and what bundle was sent")
(li "Phase 2: Server logs which components expanded server-side vs sent to client")
(li "Phase 3: Client route failures include unmatched path and available routes")
(li "Phase 4: Client IO errors include query name, params, server response")
(li "Source location tracking in parser → propagate through eval → include in error messages")))
(~doc-subsection :title "Backward Compatibility"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Pages without annotations behave as today")
(li "SX-Request / SX-Components / SX-Css header protocol continues")
(li "Existing .sx files require no changes")
(li "_expand_components continues as override")
(li "Each phase is opt-in: disable → identical to previous behavior")))
(~doc-subsection :title "Spec Integrity"
(p "All new behavior specified in .sx files under shared/sx/ref/ before implementation. Bootstrappers transpile from spec. This ensures JS and Python stay in sync.")))
;; -----------------------------------------------------------------------
;; 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/sx/async_eval.py")
(td :class "px-3 py-2 text-stone-700" "Core evaluator, _aser, server/client boundary")
(td :class "px-3 py-2 text-stone-600" "2, 5"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/helpers.py")
(td :class "px-3 py-2 text-stone-700" "sx_page(), sx_response(), output pipeline")
(td :class "px-3 py-2 text-stone-600" "1, 3"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/jinja_bridge.py")
(td :class "px-3 py-2 text-stone-700" "_COMPONENT_ENV, component registry")
(td :class "px-3 py-2 text-stone-600" "1, 2"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/pages.py")
(td :class "px-3 py-2 text-stone-700" "defpage, execute_page(), page lifecycle")
(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/boot.sx")
(td :class "px-3 py-2 text-stone-700" "Client boot, component caching")
(td :class "px-3 py-2 text-stone-600" "1, 3, 4"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/orchestration.sx")
(td :class "px-3 py-2 text-stone-700" "Client fetch/swap/morph")
(td :class "px-3 py-2 text-stone-600" "3, 4"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/eval.sx")
(td :class "px-3 py-2 text-stone-700" "Evaluator spec")
(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/ref/engine.sx")
(td :class "px-3 py-2 text-stone-700" "Morph, swaps, triggers")
(td :class "px-3 py-2 text-stone-600" "3"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/deps.py")
(td :class "px-3 py-2 text-stone-700" "Dependency analysis (new)")
(td :class "px-3 py-2 text-stone-600" "1, 2"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/router.sx")
(td :class "px-3 py-2 text-stone-700" "Client-side routing (new)")
(td :class "px-3 py-2 text-stone-600" "3"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/io-bridge.sx")
(td :class "px-3 py-2 text-stone-700" "Client IO primitives (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/ref/suspense.sx")
(td :class "px-3 py-2 text-stone-700" "Streaming/suspension (new)")
(td :class "px-3 py-2 text-stone-600" "5"))))))))