2459 lines
221 KiB
Plaintext
2459 lines
221 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 (components apply classes and respond to data), 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 6 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 (components apply classes and respond to data), 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"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "Servers + hosting + DNS + TLS")
|
|
(td :class "px-3 py-2 text-stone-700" "IPFS CID")
|
|
(td :class "px-3 py-2 text-stone-600" "Entire applications are content-addressed, no infrastructure needed"))))))
|
|
|
|
(~doc-subsection :title "Serverless applications on IPFS"
|
|
(p "The logical conclusion: " (strong "entire web applications hosted on IPFS with no server at all."))
|
|
(p "An SX application is a tree of content-addressed artifacts: a root page definition, component dependencies, media, stylesheets, parsers, transforms. Pin the root CID to IPFS and the application is live. No server, no DNS, no hosting provider, no deployment pipeline. Someone gives you a CID, you paste it into an SX-aware browser, and the application runs.")
|
|
(~doc-code :code (highlight ";; An entire blog — one CID\n;; ipfs://bafy...my-blog\n(defpage blog-home\n :path \"/\"\n :requires (list\n \"bafy...article-layout\" ;; layout component\n \"bafy...md-parser\" ;; markdown parser\n \"bafy...syntax-highlight\" ;; code highlighting\n \"bafy...nav-component\") ;; navigation\n :content\n (~article-layout\n :title \"My Blog\"\n :nav (~nav-component\n :items (list\n (dict :label \"Post 1\" :cid \"bafy...post-1\")\n (dict :label \"Post 2\" :cid \"bafy...post-2\")))\n :body (~markdown-page\n :source-cid \"bafy...homepage-md\")))" "lisp"))
|
|
(p "What this looks like in practice:")
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
|
(li (strong "Personal sites: ") "A portfolio or blog is a handful of SX files + media. Pin to IPFS. Share the CID. No hosting costs, no domain renewal, no SSL certificates. The site is permanent.")
|
|
(li (strong "Documentation: ") "Pin your docs. They can't go offline, can't be censored, can't be altered after publication (provenance proves it). Anyone can mirror them by pinning the same CID.")
|
|
(li (strong "Collaborative applications: ") "Multiple authors contribute pages and components. Each publishes their CIDs. A root manifest composes them. Update the manifest CID to add content — old CIDs remain valid forever.")
|
|
(li (strong "Offline-first: ") "An IPFS-hosted app works the same whether you're online or have the content cached locally. The browser's SX evaluator + local IPFS node = complete offline platform.")
|
|
(li (strong "Zero-cost deployment: ") "\"Deploying\" means computing a hash. No CI/CD, no Docker, no cloud provider. Pin locally, pin to a remote IPFS node, or let others pin if they want to help host."))
|
|
(p "For applications that " (em "do") " need a server — user accounts, payments, real-time collaboration, database queries — the server provides IO primitives via the existing boundary system. The SX application fetches data from the server's IO endpoints, but the application itself (all the rendering, routing, component logic) lives on IPFS. The server is a " (em "data service") ", not an application host.")
|
|
(p "This inverts the current model. Today: server hosts the application, client is a thin renderer. SX web: IPFS hosts the application, server is an optional IO provider. " (strong "The application is the content. The content is the application. Both are just s-expressions.")))
|
|
|
|
(~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 6 (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"))))))))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Content-Addressed Components
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(defcomp ~plan-content-addressed-components-content ()
|
|
(~doc-page :title "Content-Addressed Components"
|
|
|
|
(~doc-section :title "The Premise" :id "premise"
|
|
(p "SX components are pure functions. Boundary enforcement guarantees it — a component cannot call IO primitives, make network requests, access cookies, or touch the filesystem. " (code "Component.is_pure") " is a structural property, verified at registration time by scanning the transitive closure of IO references via " (code "deps.sx") ".")
|
|
(p "Pure functions have a remarkable property: " (strong "their identity is their content.") " Two components that produce the same serialized form are the same component, regardless of who wrote them or where they're hosted. This means we can content-address them — compute a cryptographic hash of the canonical serialized form, and that hash " (em "is") " the component's identity.")
|
|
(p "Content addressing turns components into shared infrastructure. Define " (code "~card") " once, pin it to IPFS, and every SX application on the planet can use it by CID. No package registry, no npm install, no version conflicts. The CID " (em "is") " the version. The hash " (em "is") " the trust. Boundary enforcement " (em "is") " the sandbox.")
|
|
(p "This plan details how to get from the current name-based, per-server component model to a content-addressed, globally-shared one."))
|
|
|
|
;; -----------------------------------------------------------------------
|
|
;; Current State
|
|
;; -----------------------------------------------------------------------
|
|
|
|
(~doc-section :title "Current State" :id "current-state"
|
|
(p "What already exists and what's missing.")
|
|
|
|
(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" "Capability")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Status")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Where")))
|
|
(tbody
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "Deterministic serialization")
|
|
(td :class "px-3 py-2 text-stone-700" "Partial — " (code "serialize(body, pretty=True)") " from AST, but no canonical normalization")
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "parser.py:296-427"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "Component identity")
|
|
(td :class "px-3 py-2 text-stone-700" "By name (" (code "~card") ") — names are mutable, server-local")
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "types.py:157-180"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "Bundle hashing")
|
|
(td :class "px-3 py-2 text-stone-700" "SHA256 of all defs concatenated — per-bundle, not per-component")
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "jinja_bridge.py:60-86"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "Purity verification")
|
|
(td :class "px-3 py-2 text-stone-700" (span :class "text-green-700 font-medium" "Complete") " — " (code "is_pure") " via transitive IO ref analysis")
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "deps.sx, boundary.py"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "Dependency graph")
|
|
(td :class "px-3 py-2 text-stone-700" (span :class "text-green-700 font-medium" "Complete") " — " (code "Component.deps") " transitive closure")
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "deps.sx"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "IPFS infrastructure")
|
|
(td :class "px-3 py-2 text-stone-700" (span :class "text-green-700 font-medium" "Exists") " — IPFSPin model, async upload tasks, CID tracking")
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "models/federation.py, artdag/l1/tasks/"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "Client component caching")
|
|
(td :class "px-3 py-2 text-stone-700" "Hash-based localStorage — but keyed by bundle hash, not individual CID")
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "boot.sx, helpers.py"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "Content-addressed components")
|
|
(td :class "px-3 py-2 text-stone-700" (span :class "text-red-700 font-medium" "Not yet") " — no per-component CID, no IPFS resolution")
|
|
(td :class "px-3 py-2 text-stone-600" "—"))))))
|
|
|
|
;; -----------------------------------------------------------------------
|
|
;; Canonical Serialization
|
|
;; -----------------------------------------------------------------------
|
|
|
|
(~doc-section :title "Phase 1: Canonical Serialization" :id "canonical-serialization"
|
|
|
|
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
|
|
(p :class "text-violet-900 font-medium" "The foundation")
|
|
(p :class "text-violet-800" "Same component must always produce the same bytes, regardless of original formatting, whitespace, or comment placement. Without this, content addressing is meaningless."))
|
|
|
|
(~doc-subsection :title "The Problem"
|
|
(p "Currently " (code "serialize(body, pretty=True)") " produces readable SX source from the parsed AST. But serialization isn't fully canonical — it depends on the internal representation order, and there's no normalization pass. Two semantically identical components formatted differently would produce different hashes.")
|
|
(p "We need a " (strong "canonical form") " that strips all variance:"))
|
|
|
|
(~doc-subsection :title "Canonical Form Rules"
|
|
(ol :class "list-decimal pl-5 text-stone-700 space-y-2"
|
|
(li (strong "Strip comments.") " Comments are parsing artifacts, not part of the AST. The serializer already ignores them (it works from the parsed tree), but any future comment-preserving parser must not affect canonical output.")
|
|
(li (strong "Normalize whitespace.") " Single space between tokens, newline before each top-level form in a body. No trailing whitespace. No blank lines.")
|
|
(li (strong "Sort keyword arguments alphabetically.") " In component calls: " (code "(~card :class \"x\" :title \"y\")") " not " (code "(~card :title \"y\" :class \"x\")") ". In dict literals: " (code "{:a 1 :b 2}") " not " (code "{:b 2 :a 1}") ".")
|
|
(li (strong "Normalize string escapes.") " Use " (code "\\n") " not literal newlines in strings. Escape only what must be escaped.")
|
|
(li (strong "Normalize numbers.") " " (code "1.0") " not " (code "1.00") " or " (code "1.") ". " (code "42") " not " (code "042") ".")
|
|
(li (strong "Include the full definition form.") " Hash the complete " (code "(defcomp ~name (params) body)") ", not just the body. The name and parameter signature are part of the component's identity.")))
|
|
|
|
(~doc-subsection :title "Implementation"
|
|
(p "New spec function in a " (code "canonical.sx") " module:")
|
|
(~doc-code :code (highlight "(define canonical-serialize\n (fn (node)\n ;; Produce a canonical s-expression string from an AST node.\n ;; Deterministic: same AST always produces same output.\n ;; Used for CID computation — NOT for human-readable output.\n (case (type-of node)\n \"list\"\n (str \"(\" (join \" \" (map canonical-serialize node)) \")\")\n \"dict\"\n (let ((sorted-keys (sort (keys node))))\n (str \"{\" (join \" \"\n (map (fn (k)\n (str \":\" k \" \" (canonical-serialize (get node k))))\n sorted-keys)) \"}\"))\n \"string\"\n (str '\"' (escape-canonical node) '\"')\n \"number\"\n (canonical-number node)\n \"symbol\"\n (symbol-name node)\n \"keyword\"\n (str \":\" (keyword-name node))\n \"boolean\"\n (if node \"true\" \"false\")\n \"nil\"\n \"nil\")))" "lisp"))
|
|
(p "This function must be bootstrapped to both Python and JS — the server computes CIDs at registration time, the client verifies them on fetch.")
|
|
(p "The canonical serializer is distinct from " (code "serialize()") " for display. " (code "serialize(pretty=True)") " remains for human-readable output. " (code "canonical-serialize") " is for hashing only.")))
|
|
|
|
;; -----------------------------------------------------------------------
|
|
;; CID Computation
|
|
;; -----------------------------------------------------------------------
|
|
|
|
(~doc-section :title "Phase 2: CID Computation" :id "cid-computation"
|
|
|
|
(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" "Every component gets a stable, unique content identifier. Same source → same CID, always. Different source → different CID, always."))
|
|
|
|
(~doc-subsection :title "CID Format"
|
|
(p "Use " (a :href "https://github.com/multiformats/cid" :class "text-violet-700 underline" "CIDv1") " with:")
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
|
(li (strong "Hash function:") " SHA3-256 (already used by artdag for content addressing)")
|
|
(li (strong "Codec:") " raw (the content is the canonical SX source bytes, not a DAG-PB wrapper)")
|
|
(li (strong "Base encoding:") " base32lower for URL-safe representation (" (code "bafy...") " prefix)"))
|
|
(~doc-code :code (highlight ";; CID computation pipeline\n(define component-cid\n (fn (component)\n ;; 1. Reconstruct full defcomp form\n ;; 2. Canonical serialize\n ;; 3. SHA3-256 hash\n ;; 4. Wrap as CIDv1\n (let ((source (canonical-serialize\n (list 'defcomp\n (symbol (str \"~\" (component-name component)))\n (component-params-list component)\n (component-body component)))))\n (cid-v1 :sha3-256 :raw (encode-utf8 source)))))" "lisp")))
|
|
|
|
(~doc-subsection :title "Where CIDs Live"
|
|
(p "Each " (code "Component") " object gains a " (code "cid") " field, computed at registration time:")
|
|
(~doc-code :code (highlight ";; types.py extension\n@dataclass\nclass Component:\n name: str\n params: list[str]\n has_children: bool\n body: Any\n closure: dict[str, Any]\n css_classes: set[str]\n deps: set[str] # by name\n io_refs: set[str]\n cid: str | None = None # computed after registration\n dep_cids: dict[str, str] | None = None # name → CID" "python"))
|
|
(p "After " (code "compute_all_deps()") " runs, a new " (code "compute_all_cids()") " pass fills in CIDs for every component. Dependency CIDs are also recorded — when a component references " (code "~card") ", we store both the name and card's CID."))
|
|
|
|
(~doc-subsection :title "CID Stability"
|
|
(p "A component's CID changes when and only when its " (strong "semantics") " change:")
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
|
(li "Reformatting the " (code ".sx") " source file → same AST → same canonical form → " (strong "same CID"))
|
|
(li "Adding a comment → stripped by parser → same AST → " (strong "same CID"))
|
|
(li "Changing a class name in the body → different AST → " (strong "different CID"))
|
|
(li "Renaming the component → different defcomp form → " (strong "different CID") " (name is part of identity)"))
|
|
(p "This means CIDs are " (em "immutable versions") ". There's no " (code "~card@1.2.3") " — there's " (code "~card") " at CID " (code "bafy...abc") " and " (code "~card") " at CID " (code "bafy...def") ". The name is a human-friendly alias; the CID is the truth.")))
|
|
|
|
;; -----------------------------------------------------------------------
|
|
;; Component Manifest
|
|
;; -----------------------------------------------------------------------
|
|
|
|
(~doc-section :title "Phase 3: Component Manifest" :id "manifest"
|
|
|
|
(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" "Metadata that travels with a CID — what a component needs, what it provides, whether it's safe to run. Enough information to resolve, validate, and render without fetching the source first."))
|
|
|
|
(~doc-subsection :title "Manifest Structure"
|
|
(~doc-code :code (highlight ";; Component manifest — published alongside the source\n(SxComponent\n :name \"~product-card\"\n :cid \"bafy...productcard\"\n :source-bytes 847\n :params (:title :price :image-url)\n :has-children true\n :pure true\n :deps (\n {:name \"~card\" :cid \"bafy...card\"}\n {:name \"~price-tag\" :cid \"bafy...pricetag\"}\n {:name \"~lazy-image\" :cid \"bafy...lazyimg\"})\n :css-atoms (:border :rounded :p-4 :text-sm :font-bold\n :text-green-700 :line-through :text-stone-400)\n :author \"https://rose-ash.com/apps/market\"\n :published \"2026-03-06T14:30:00Z\")" "lisp"))
|
|
(p "Key fields:")
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
|
(li (code ":cid") " — content address of the canonical serialized source")
|
|
(li (code ":deps") " — dependency CIDs, not just names. A consumer can recursively resolve the entire tree by CID without name ambiguity")
|
|
(li (code ":pure") " — pre-computed purity flag. The consumer " (em "re-verifies") " this after fetching (never trust the manifest alone), but it enables fast rejection of IO-dependent components before downloading")
|
|
(li (code ":deps") " includes style component CIDs. No separate " (code ":css-atoms") " field needed — styling is just more components")
|
|
(li (code ":params") " — parameter signature for tooling, documentation, IDE support")
|
|
(li (code ":author") " — who published this. AP actor URL, verifiable via HTTP Signatures")))
|
|
|
|
(~doc-subsection :title "Manifest CID"
|
|
(p "The manifest itself is content-addressed. But the manifest CID is " (em "not") " the component CID — they're separate objects. The component CID is derived from the source alone (pure content). The manifest CID includes metadata that could change (author, publication date) without changing the component.")
|
|
(p "Resolution order: manifest CID → manifest → component CID → component source. Or shortcut: component CID → source directly, if you already know what you need.")))
|
|
|
|
;; -----------------------------------------------------------------------
|
|
;; IPFS Storage & Resolution
|
|
;; -----------------------------------------------------------------------
|
|
|
|
(~doc-section :title "Phase 4: IPFS Storage & Resolution" :id "ipfs"
|
|
|
|
(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" "Components live on IPFS. Any browser can fetch them by CID. No origin server needed. No CDN. No DNS. The content network IS the distribution network."))
|
|
|
|
(~doc-subsection :title "Server-Side: Publication"
|
|
(p "On component registration (startup or hot-reload), the server:")
|
|
(ol :class "list-decimal pl-5 text-stone-700 space-y-1"
|
|
(li "Computes canonical form and CID")
|
|
(li "Checks " (code "IPFSPin") " — if CID already pinned, skip (content can't have changed)")
|
|
(li "Pins canonical source to IPFS (async Celery task, same pattern as artdag)")
|
|
(li "Creates/updates " (code "IPFSPin") " record with " (code "pin_type=\"component\""))
|
|
(li "Publishes manifest to IPFS (separate CID)")
|
|
(li "Optionally announces via AP outbox for federated discovery"))
|
|
(~doc-code :code (highlight ";; IPFSPin usage for components\nIPFSPin(\n content_hash=\"sha3-256:abcdef...\",\n ipfs_cid=\"bafy...productcard\",\n pin_type=\"component\",\n source_type=\"market\", # which service defined it\n metadata={\n \"name\": \"~product-card\",\n \"manifest_cid\": \"bafy...manifest\",\n \"deps\": [\"bafy...card\", \"bafy...pricetag\"],\n \"pure\": True\n }\n)" "python")))
|
|
|
|
(~doc-subsection :title "Client-Side: Resolution"
|
|
(p "New spec module " (code "resolve.sx") " — the client-side component resolution pipeline:")
|
|
(~doc-code :code (highlight "(define resolve-component-by-cid\n (fn (cid callback)\n ;; Resolution cascade:\n ;; 1. Check component env (already loaded?)\n ;; 2. Check localStorage (keyed by CID = cache-forever)\n ;; 3. Check origin server (/sx/components?cid=bafy...)\n ;; 4. Fetch from IPFS gateway\n ;; 5. Verify hash matches CID\n ;; 6. Parse, validate purity, register, callback\n (let ((cached (local-storage-get (str \"sx-cid:\" cid))))\n (if cached\n (do\n (register-component-source cached)\n (callback true))\n (fetch-component-by-cid cid\n (fn (source)\n (if (verify-cid cid source)\n (do\n (local-storage-set (str \"sx-cid:\" cid) source)\n (register-component-source source)\n (callback true))\n (do\n (log-warn (str \"sx:cid verification failed \" cid))\n (callback false)))))))))" "lisp"))
|
|
(p "The cache-forever semantics are the key insight: because CIDs are content-addressed, a cached component " (strong "can never be stale") ". If the source changes, it gets a new CID. Old CIDs remain valid forever. There is no cache invalidation problem."))
|
|
|
|
(~doc-subsection :title "Resolution Cascade"
|
|
(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" "Layer")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Lookup")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Latency")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "When")))
|
|
(tbody
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "1. Component env")
|
|
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "(env-has? env cid)")
|
|
(td :class "px-3 py-2 text-stone-600" "0ms")
|
|
(td :class "px-3 py-2 text-stone-600" "Already loaded this session"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "2. localStorage")
|
|
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "localStorage[\"sx-cid:\" + cid]")
|
|
(td :class "px-3 py-2 text-stone-600" "<1ms")
|
|
(td :class "px-3 py-2 text-stone-600" "Previously fetched, persists across sessions"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "3. Origin server")
|
|
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "GET /sx/components?cid=bafy...")
|
|
(td :class "px-3 py-2 text-stone-600" "~20ms")
|
|
(td :class "px-3 py-2 text-stone-600" "Same-origin component, not yet cached"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "4. IPFS gateway")
|
|
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "GET https://gateway/ipfs/{cid}")
|
|
(td :class "px-3 py-2 text-stone-600" "~200ms")
|
|
(td :class "px-3 py-2 text-stone-600" "Foreign component, federated content"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "5. Local IPFS node")
|
|
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "ipfs cat {cid}")
|
|
(td :class "px-3 py-2 text-stone-600" "~5ms")
|
|
(td :class "px-3 py-2 text-stone-600" "User runs own IPFS node (power users)")))))
|
|
(p "Layer 5 is optional — checked between 2 and 3 if " (code "window.ipfs") " or a local gateway is detected. For most users, layers 1-4 cover all cases.")))
|
|
|
|
;; -----------------------------------------------------------------------
|
|
;; Security Model
|
|
;; -----------------------------------------------------------------------
|
|
|
|
(~doc-section :title "Phase 5: Security Model" :id "security"
|
|
|
|
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
|
|
(p :class "text-violet-900 font-medium" "The hard part")
|
|
(p :class "text-violet-800" "Loading code from the network is the web's original sin. Content-addressed components are safe because of three structural guarantees — not policies, not trust, not sandboxes that can be escaped."))
|
|
|
|
(~doc-subsection :title "Guarantee 1: Purity is Structural"
|
|
(p "SX boundary enforcement isn't a runtime sandbox — it's a registration-time structural check. When a component is loaded from IPFS and parsed, " (code "compute_all_io_refs()") " walks its entire AST and transitive dependencies. If " (em "any") " node references an IO primitive, the component is classified as IO-dependent and " (strong "rejected for untrusted registration."))
|
|
(p "This means the evaluator literally doesn't have IO primitives in scope when running an IPFS-loaded component. It's not that we catch IO calls — the names don't resolve. There's nothing to catch.")
|
|
(~doc-code :code (highlight "(define register-untrusted-component\n (fn (source origin)\n ;; Parse the defcomp from source\n ;; Run compute-all-io-refs on the parsed component\n ;; If io_refs is non-empty → REJECT\n ;; If pure → register in env with :origin metadata\n (let ((comp (parse-component source)))\n (if (not (component-pure? comp))\n (do\n (log-warn (str \"sx:reject IO component from \" origin))\n nil)\n (do\n (register-component comp)\n (log-info (str \"sx:registered \" (component-name comp)\n \" from \" origin))\n comp)))))" "lisp")))
|
|
|
|
(~doc-subsection :title "Guarantee 2: Content Verification"
|
|
(p "The CID IS the hash. When you fetch " (code "bafy...abc") " from any source — IPFS gateway, origin server, peer — you hash the response and compare. If it doesn't match, you reject it. No MITM attack can alter the content without changing the CID.")
|
|
(p "This is stronger than HTTPS. HTTPS trusts the certificate authority, the DNS resolver, and the server operator. Content addressing trusts " (em "mathematics") ". The hash either matches or it doesn't."))
|
|
|
|
(~doc-subsection :title "Guarantee 3: Evaluation Limits"
|
|
(p "Pure doesn't mean terminating. A component could contain an infinite loop or exponential recursion. SX evaluators enforce step limits:")
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
|
(li (strong "Max eval steps:") " configurable per context. Untrusted components get a lower limit than local ones.")
|
|
(li (strong "Max recursion depth:") " prevents stack exhaustion.")
|
|
(li (strong "Max output size:") " prevents a component from producing gigabytes of DOM nodes."))
|
|
(p "Exceeding any limit halts evaluation and returns an error node. The worst case is wasted CPU — never data exfiltration, never unauthorized IO."))
|
|
|
|
(~doc-subsection :title "Trust Tiers"
|
|
(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" "Tier")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Source")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Allowed")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Eval limits")))
|
|
(tbody
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-semibold text-stone-800" "Local")
|
|
(td :class "px-3 py-2 text-stone-700" "Server's own " (code ".sx") " files")
|
|
(td :class "px-3 py-2 text-stone-700" "Pure + IO primitives + page helpers")
|
|
(td :class "px-3 py-2 text-stone-600" "None (trusted)"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-semibold text-stone-800" "Followed")
|
|
(td :class "px-3 py-2 text-stone-700" "Components from followed AP actors")
|
|
(td :class "px-3 py-2 text-stone-700" "Pure only (IO rejected)")
|
|
(td :class "px-3 py-2 text-stone-600" "Standard limits"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-semibold text-stone-800" "Federated")
|
|
(td :class "px-3 py-2 text-stone-700" "Components from any IPFS source")
|
|
(td :class "px-3 py-2 text-stone-700" "Pure only (IO rejected)")
|
|
(td :class "px-3 py-2 text-stone-600" "Strict limits"))))))
|
|
|
|
(~doc-subsection :title "What Can Go Wrong"
|
|
(p "Honest accounting of the attack surface:")
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
|
(li (strong "Visual spoofing:") " A malicious component could render UI that looks like a login form. Mitigation: untrusted components render inside a visually distinct container with origin attribution.")
|
|
(li (strong "CSS abuse:") " A component's CSS atoms could interfere with page layout. Mitigation: scoped CSS — untrusted components' classes are namespaced.")
|
|
(li (strong "Resource exhaustion:") " A component could be expensive to evaluate. Mitigation: step limits, timeout, lazy rendering for off-screen components.")
|
|
(li (strong "Privacy leak via CSS:") " Background-image URLs could phone home. Mitigation: CSP restrictions on untrusted component rendering contexts.")
|
|
(li (strong "Dependency confusion:") " A malicious manifest could claim deps that are different components with the same name. Mitigation: deps are referenced by CID, not name. Name is informational only."))))
|
|
|
|
;; -----------------------------------------------------------------------
|
|
;; Wire Format & Prefetch Integration
|
|
;; -----------------------------------------------------------------------
|
|
|
|
(~doc-section :title "Phase 6: Wire Format & Prefetch Integration" :id "wire-format"
|
|
|
|
(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" "Pages and SX responses reference components by CID. The prefetch system resolves them from the most efficient source. Components become location-independent."))
|
|
|
|
(~doc-subsection :title "CID References in Page Registry"
|
|
(p "The page registry (shipped to the client as " (code "<script type=\"text/sx-pages\">") ") currently lists deps by name. Extend to include CIDs:")
|
|
(~doc-code :code (highlight "{:name \"docs-page\" :path \"/docs/<slug>\"\n :auth \"public\" :has-data false\n :deps ({:name \"~essay-foo\" :cid \"bafy...essay\"}\n {:name \"~doc-code\" :cid \"bafy...doccode\"})\n :content \"(case slug ...)\" :closure {}}" "lisp"))
|
|
(p "The " (a :href "/plans/predictive-prefetch" :class "text-violet-700 underline" "predictive prefetch system") " uses these CIDs to fetch components from the resolution cascade rather than only from the origin server's " (code "/sx/components") " endpoint."))
|
|
|
|
(~doc-subsection :title "SX Response Component Headers"
|
|
(p "Currently, " (code "SX-Components") " header lists loaded component names. Extend to support CIDs:")
|
|
(~doc-code :code (highlight "Request:\nSX-Components: ~card:bafy...card,~nav:bafy...nav\n\nResponse:\nSX-Component-CIDs: ~essay-foo:bafy...essay,~doc-code:bafy...doccode\n\n;; Response body only includes defs the client doesn't have\n(defcomp ~essay-foo ...)" "http"))
|
|
(p "The client can then verify received components match their declared CIDs. If the origin server is compromised, CID verification catches the tampered response."))
|
|
|
|
(~doc-subsection :title "Federated Content"
|
|
(p "When an ActivityPub activity arrives with SX content, it declares component requirements by CID:")
|
|
(~doc-code :code (highlight "(Create\n :actor \"https://other-instance.com/users/bob\"\n :object (Note\n :content (~product-card :title \"Bob's Widget\" :price 29.99)\n :requires (list\n {:name \"~product-card\" :cid \"bafy...prodcard\"}\n {:name \"~price-tag\" :cid \"bafy...pricetag\"})))" "lisp"))
|
|
(p "The receiving browser resolves required components through the cascade. If Bob's instance is down, the components are still fetchable from IPFS. The content is self-describing and self-resolving.")))
|
|
|
|
;; -----------------------------------------------------------------------
|
|
;; Component Sharing & Discovery
|
|
;; -----------------------------------------------------------------------
|
|
|
|
(~doc-section :title "Phase 7: Sharing & Discovery" :id "sharing"
|
|
|
|
(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" "Servers publish component collections via AP. Other servers follow them. Like npm, but federated, content-addressed, and structurally safe."))
|
|
|
|
(~doc-subsection :title "Component Registry as AP Actor"
|
|
(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. 'Update' means a new CID for the same name — consumers decide whether to adopt it."))
|
|
|
|
(~doc-subsection :title "Discovery Protocol"
|
|
(p "Webfinger-style lookup for components by name:")
|
|
(~doc-code :code (highlight "GET /.well-known/sx-component?name=~product-card\n\n{\n \"name\": \"~product-card\",\n \"cid\": \"bafy...prodcard\",\n \"manifest_cid\": \"bafy...manifest\",\n \"gateway\": \"https://rose-ash.com/ipfs/\",\n \"author\": \"https://rose-ash.com/apps/market\"\n}" "http"))
|
|
(p "This is an optional convenience — any consumer that knows the CID can skip discovery and fetch directly from IPFS. Discovery answers the question: " (em "\"what's the current version of ~product-card on rose-ash.com?\""))
|
|
)
|
|
|
|
(~doc-subsection :title "Name Resolution"
|
|
(p "Names are human-friendly aliases for CIDs. The same name on different servers can refer to different components (different CIDs). Conflict resolution is simple:")
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
|
(li (strong "Local wins:") " If the server defines " (code "~card") ", that definition takes precedence over any federated " (code "~card") ".")
|
|
(li (strong "CID pinning:") " When referencing a federated component, pin the CID. " (code "(:name \"~card\" :cid \"bafy...abc\")") " — the name is informational, the CID is authoritative.")
|
|
(li (strong "No global namespace:") " There is no \"npm\" that owns " (code "~card") ". Names are scoped to the server that defines them. CIDs are global."))))
|
|
|
|
;; -----------------------------------------------------------------------
|
|
;; Spec modules
|
|
;; -----------------------------------------------------------------------
|
|
|
|
(~doc-section :title "Spec Modules" :id "spec-modules"
|
|
(p "Per the SX host architecture principle, all content-addressing logic is specced in " (code ".sx") " files and bootstrapped:")
|
|
|
|
(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" "Spec module")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Functions")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Platform obligations")))
|
|
(tbody
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "canonical.sx")
|
|
(td :class "px-3 py-2 text-stone-700" (code "canonical-serialize") ", " (code "canonical-number") ", " (code "escape-canonical"))
|
|
(td :class "px-3 py-2 text-stone-600" "None — pure string operations"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "cid.sx")
|
|
(td :class "px-3 py-2 text-stone-700" (code "component-cid") ", " (code "verify-cid") ", " (code "cid-to-string") ", " (code "parse-cid"))
|
|
(td :class "px-3 py-2 text-stone-600" (code "sha3-256") ", " (code "encode-base32") ", " (code "encode-utf8")))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "resolve.sx")
|
|
(td :class "px-3 py-2 text-stone-700" (code "resolve-component-by-cid") ", " (code "resolve-deps-recursive") ", " (code "register-untrusted-component"))
|
|
(td :class "px-3 py-2 text-stone-600" (code "local-storage-get/set") ", " (code "fetch-cid") ", " (code "register-component-source"))))))
|
|
|
|
;; -----------------------------------------------------------------------
|
|
;; 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" "Phase")))
|
|
(tbody
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/canonical.sx")
|
|
(td :class "px-3 py-2 text-stone-700" "Canonical serialization spec (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/ref/cid.sx")
|
|
(td :class "px-3 py-2 text-stone-700" "CID computation and verification spec (new)")
|
|
(td :class "px-3 py-2 text-stone-600" "2"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/types.py")
|
|
(td :class "px-3 py-2 text-stone-700" "Add " (code "cid") " and " (code "dep_cids") " to Component")
|
|
(td :class "px-3 py-2 text-stone-600" "2"))
|
|
(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" "Add " (code "compute_all_cids()") " to registration lifecycle")
|
|
(td :class "px-3 py-2 text-stone-600" "2"))
|
|
(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 records for component CIDs")
|
|
(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/resolve.sx")
|
|
(td :class "px-3 py-2 text-stone-700" "Client-side CID resolution cascade (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/helpers.py")
|
|
(td :class "px-3 py-2 text-stone-700" "CIDs in page registry, " (code "/sx/components?cid=") " endpoint")
|
|
(td :class "px-3 py-2 text-stone-600" "6"))
|
|
(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" "CID-aware prefetch in resolution cascade")
|
|
(td :class "px-3 py-2 text-stone-600" "6"))
|
|
(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" "Component registry actor, Webfinger extension")
|
|
(td :class "px-3 py-2 text-stone-600" "7"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/boundary.py")
|
|
(td :class "px-3 py-2 text-stone-700" "Trust tier enforcement for untrusted components")
|
|
(td :class "px-3 py-2 text-stone-600" "5"))))))
|
|
|
|
;; -----------------------------------------------------------------------
|
|
;; Relationship
|
|
;; -----------------------------------------------------------------------
|
|
|
|
(~doc-section :title "Relationships" :id "relationships"
|
|
(p "This plan is the foundation for several other plans and roadmaps:")
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
|
(li (a :href "/plans/sx-activity" :class "text-violet-700 underline" "SX-Activity") " Phase 2 (content-addressed components on IPFS) is a summary of this plan. This plan supersedes that section with full detail.")
|
|
(li (a :href "/plans/predictive-prefetch" :class "text-violet-700 underline" "Predictive prefetching") " gains CID-based resolution — the " (code "/sx/components") " endpoint and IPFS gateway become alternative resolution paths in the prefetch cascade.")
|
|
(li (a :href "/plans/isomorphic-architecture" :class "text-violet-700 underline" "Isomorphic architecture") " Phase 1 (component distribution) is enhanced — CIDs make per-page bundles verifiable and cross-server shareable.")
|
|
(li "The SX-Activity vision of " (strong "serverless applications on IPFS") " depends entirely on this plan. Without content-addressed components, applications can't be pinned to IPFS as self-contained artifacts."))
|
|
(div :class "rounded border border-amber-200 bg-amber-50 p-3 mt-2"
|
|
(p :class "text-amber-800 text-sm" (strong "Depends on: ") "deps.sx (complete), boundary enforcement (complete), IPFS infrastructure (exists in artdag, needs wiring to web platform)."))))))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Predictive Component Prefetching
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(defcomp ~plan-predictive-prefetch-content ()
|
|
(~doc-page :title "Predictive Component Prefetching"
|
|
|
|
(~doc-section :title "Context" :id "context"
|
|
(p "Phase 3 of the isomorphic roadmap added client-side routing with component dependency checking. When a user clicks a link, " (code "try-client-route") " checks " (code "has-all-deps?") " — if the target page needs components not yet loaded, the client falls back to a server fetch. This works correctly but misses an opportunity: " (strong "we can prefetch those missing components before the click happens."))
|
|
(p "The page registry already carries " (code ":deps") " metadata for every page. The client already knows which components are loaded via " (code "loaded-component-names") ". The gap is a mechanism to " (em "proactively") " resolve the difference — fetching missing component definitions so that by the time the user clicks, client-side routing succeeds.")
|
|
(p "But this goes beyond just hover-to-prefetch. The full spectrum includes: bundling linked routes' components with the initial page load, batch-prefetching after idle, predicting mouse trajectory toward links, and even splitting the component/data fetch so that " (code ":data") " pages can prefetch their components and only fetch data on click. Each strategy trades bandwidth for latency, and pages should be able to declare which tradeoff they want."))
|
|
|
|
(~doc-section :title "Current State" :id "current-state"
|
|
(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" "Layer")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "What exists")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Where")))
|
|
(tbody
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "Page registry")
|
|
(td :class "px-3 py-2 text-stone-700" "Each page carries " (code ":deps (\"~card\" \"~essay-foo\" ...)"))
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "helpers.py → <script type=\"text/sx-pages\">"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "Dep check")
|
|
(td :class "px-3 py-2 text-stone-700" (code "has-all-deps?") " gates client routing")
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "orchestration.sx:546-559"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "Component bundle")
|
|
(td :class "px-3 py-2 text-stone-700" "Per-page inline " (code "<script type=\"text/sx\" data-components>"))
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "helpers.py:715, jinja_bridge.py"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "Incremental defs")
|
|
(td :class "px-3 py-2 text-stone-700" (code "components_for_request()") " sends only missing defs in SX responses")
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "helpers.py:459-509"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "Preload cache")
|
|
(td :class "px-3 py-2 text-stone-700" (code "sx-preload") " prefetches full responses on hover/mousedown")
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "orchestration.sx:686-708"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "Route matching")
|
|
(td :class "px-3 py-2 text-stone-700" (code "find-matching-route") " matches pathname to page entry")
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "router.sx"))))))
|
|
|
|
;; -----------------------------------------------------------------------
|
|
;; Prefetch strategies
|
|
;; -----------------------------------------------------------------------
|
|
|
|
(~doc-section :title "Prefetch Strategies" :id "strategies"
|
|
(p "Prefetching is a spectrum from conservative to aggressive. The system should support all of these, configured declaratively per link or per page via " (code "defpage") " metadata and " (code "sx-prefetch") " attributes.")
|
|
|
|
(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" "Strategy")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Trigger")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "What prefetches")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Latency on click")))
|
|
(tbody
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-semibold text-stone-800" "Eager bundle")
|
|
(td :class "px-3 py-2 text-stone-700" "Initial page load")
|
|
(td :class "px-3 py-2 text-stone-700" "Components for linked routes included in " (code "<script data-components>"))
|
|
(td :class "px-3 py-2 text-stone-600" "Zero — already in memory"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-semibold text-stone-800" "Idle timer")
|
|
(td :class "px-3 py-2 text-stone-700" "After page settles (requestIdleCallback or setTimeout)")
|
|
(td :class "px-3 py-2 text-stone-700" "Components for visible nav links, batched in one request")
|
|
(td :class "px-3 py-2 text-stone-600" "Zero if idle fetch completed"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-semibold text-stone-800" "Viewport")
|
|
(td :class "px-3 py-2 text-stone-700" "Link scrolls into view (IntersectionObserver)")
|
|
(td :class "px-3 py-2 text-stone-700" "Components for that link's route")
|
|
(td :class "px-3 py-2 text-stone-600" "Zero if user scrolled before clicking"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-semibold text-stone-800" "Mouse approach")
|
|
(td :class "px-3 py-2 text-stone-700" "Cursor moving toward link (trajectory prediction)")
|
|
(td :class "px-3 py-2 text-stone-700" "Components for predicted target")
|
|
(td :class "px-3 py-2 text-stone-600" "Near-zero — fetch starts ~200ms before hover"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-semibold text-stone-800" "Hover")
|
|
(td :class "px-3 py-2 text-stone-700" "mouseover (150ms debounce)")
|
|
(td :class "px-3 py-2 text-stone-700" "Components for hovered link's route")
|
|
(td :class "px-3 py-2 text-stone-600" "Low — typical hover-to-click is 300-500ms"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-semibold text-stone-800" "Mousedown")
|
|
(td :class "px-3 py-2 text-stone-700" "mousedown (0ms debounce)")
|
|
(td :class "px-3 py-2 text-stone-700" "Components for clicked link's route")
|
|
(td :class "px-3 py-2 text-stone-600" "~80ms — mousedown-to-click gap"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-semibold text-stone-800" "Components + data")
|
|
(td :class "px-3 py-2 text-stone-700" "Any of the above")
|
|
(td :class "px-3 py-2 text-stone-700" "Components " (em "and") " page data for " (code ":data") " pages")
|
|
(td :class "px-3 py-2 text-stone-600" "Zero for components; data fetch may still be in flight")))))
|
|
|
|
(~doc-subsection :title "Eager Bundle"
|
|
(p "The server already computes per-page component bundles. For key navigation paths — the main nav bar, section nav — the server can include " (em "linked routes' components") " in the initial bundle, not just the current page's.")
|
|
(~doc-code :code (highlight ";; defpage metadata declares eager prefetch targets\n(defpage docs-page\n :path \"/docs/<slug>\"\n :auth :public\n :prefetch :eager ;; bundle deps for all linked pure routes\n :content (case slug ...))" "lisp"))
|
|
(p "Implementation: " (code "components_for_page()") " already scans the page SX for component refs. Extend it to also scan for " (code "href") " attributes, match them against the page registry, and include those pages' deps in the bundle. The cost is a larger initial payload; the benefit is zero-latency navigation within a section."))
|
|
|
|
(~doc-subsection :title "Idle Timer"
|
|
(p "After page load and initial render, use " (code "requestIdleCallback") " (or a fallback " (code "setTimeout") ") to scan visible nav links and batch-prefetch their missing components in a single request.")
|
|
(~doc-code :code (highlight "(define prefetch-visible-links-on-idle\n (fn ()\n (request-idle-callback\n (fn ()\n (let ((links (dom-query-all \"a[href][sx-get]\"))\n (all-missing (list)))\n (for-each\n (fn (link)\n (let ((missing (compute-missing-deps\n (url-pathname (dom-get-attr link \"href\")))))\n (when missing\n (for-each (fn (d) (append! all-missing d))\n missing))))\n links)\n (when (not (empty? all-missing))\n (prefetch-components (dedupe all-missing))))))))" "lisp"))
|
|
(p "Called once from " (code "boot-init") " after initial processing. Batches all missing deps into one network request. Low priority — browser handles it when idle."))
|
|
|
|
(~doc-subsection :title "Mouse Approach (Trajectory Prediction)"
|
|
(p "Don't wait for the cursor to reach the link — predict where it's heading. Track the last few " (code "mousemove") " events, extrapolate the trajectory, and if it points toward a link, start prefetching before the hover event fires.")
|
|
(~doc-code :code (highlight "(define bind-approach-prefetch\n (fn (container)\n ;; Track mouse trajectory within a nav container.\n ;; On each mousemove, extrapolate position ~200ms ahead.\n ;; If projected point intersects a link's bounding box,\n ;; prefetch that link's route deps.\n (let ((last-x 0) (last-y 0) (last-t 0)\n (prefetched (dict)))\n (dom-add-listener container \"mousemove\"\n (fn (e)\n (let ((now (timestamp))\n (dt (- now last-t)))\n (when (> dt 16) ;; ~60fps throttle\n (let ((vx (/ (- (event-x e) last-x) dt))\n (vy (/ (- (event-y e) last-y) dt))\n (px (+ (event-x e) (* vx 200)))\n (py (+ (event-y e) (* vy 200)))\n (target (dom-element-at-point px py)))\n (when (and target (dom-has-attr? target \"href\")\n (not (get prefetched\n (dom-get-attr target \"href\"))))\n (let ((href (dom-get-attr target \"href\")))\n (set! prefetched\n (merge prefetched {href true}))\n (prefetch-route-deps\n (url-pathname href)))))\n (set! last-x (event-x e))\n (set! last-y (event-y e))\n (set! last-t now))))))))" "lisp"))
|
|
(p "This is the most speculative strategy — best suited for dense navigation areas (section sidebars, nav bars) where the cursor trajectory is a strong predictor. The " (code "prefetched") " dict prevents duplicate fetches within the same container interaction."))
|
|
|
|
(~doc-subsection :title "Components + Data (Hybrid Prefetch)"
|
|
(p "The most interesting strategy. For pages with " (code ":data") " dependencies, current behavior is full server fallback. But the page's " (em "components") " are still pure and prefetchable. If we prefetch components ahead of time, the click only needs to fetch " (em "data") " — a much smaller, faster response.")
|
|
(p "This creates a new rendering path:")
|
|
(ol :class "list-decimal pl-5 text-stone-700 space-y-1"
|
|
(li "Prefetch: hover/idle/viewport triggers " (code "prefetch-components") " for the target page")
|
|
(li "Click: client has components, but page has " (code ":data") " — fetch data from server")
|
|
(li "Server returns " (em "only data") " (JSON or SX bindings), not the full rendered page")
|
|
(li "Client evaluates the content expression with prefetched components + fetched data")
|
|
(li "Result: faster than full server render, no redundant component transfer"))
|
|
(~doc-code :code (highlight ";; Declarative: prefetch components, fetch data on click\n(defpage reference-page\n :path \"/reference/<slug>\"\n :auth :public\n :prefetch :components ;; prefetch components, data stays server-fetched\n :data (reference-data slug)\n :content (~reference-attrs-content :attrs attrs))\n\n;; On click, client-side flow:\n;; 1. Components already prefetched (from hover/idle)\n;; 2. GET /reference/attributes → server returns data bindings\n;; 3. Client evals (reference-data slug) result + content expr\n;; 4. Renders locally with cached components" "lisp"))
|
|
(p "This is a stepping stone toward full Phase 4 (client IO bridge) of the isomorphic roadmap — it achieves partial client rendering for data pages without needing a general-purpose client async evaluator. The server is a data service, the client is the renderer."))
|
|
|
|
(~doc-subsection :title "Declarative Configuration"
|
|
(p "All strategies configured via " (code "defpage") " metadata and " (code "sx-prefetch") " attributes on links/containers:")
|
|
(~doc-code :code (highlight ";; Page-level: what to prefetch for routes linking TO this page\n(defpage docs-page\n :path \"/docs/<slug>\"\n :prefetch :eager) ;; bundle with linking page\n\n(defpage reference-page\n :path \"/reference/<slug>\"\n :prefetch :components) ;; prefetch components, data on click\n\n;; Link-level: override per-link\n(a :href \"/docs/components\"\n :sx-prefetch \"idle\") ;; prefetch after page idle\n\n;; Container-level: approach prediction for nav areas\n(nav :sx-prefetch \"approach\"\n (a :href \"/docs/\") (a :href \"/reference/\") ...)" "lisp"))
|
|
(p "Priority cascade: explicit " (code "sx-prefetch") " on link > " (code ":prefetch") " on target defpage > default (hover). The system never prefetches the same components twice — " (code "_prefetch-pending") " and " (code "loaded-component-names") " handle dedup.")))
|
|
|
|
;; -----------------------------------------------------------------------
|
|
;; Design
|
|
;; -----------------------------------------------------------------------
|
|
|
|
(~doc-section :title "Implementation Design" :id "design"
|
|
|
|
(p "Per the SX host architecture principle: all SX-specific logic goes in " (code ".sx") " spec files and gets bootstrapped. The prefetch logic — scanning links, computing missing deps, managing the component cache — must be specced in " (code ".sx") ", not written directly in JS or Python.")
|
|
|
|
(~doc-subsection :title "Phase 1: Component Fetch Endpoint (Python)"
|
|
(p "A new " (strong "public") " endpoint (not " (code "/internal/") " — the client's browser calls it) that returns component definitions by name.")
|
|
(~doc-code :code (highlight "GET /<service-prefix>/sx/components?names=~card,~essay-foo\n\nResponse (text/sx):\n(defcomp ~card (&key title &rest children)\n (div :class \"border rounded p-4\" (h2 title) children))\n(defcomp ~essay-foo (&key id)\n (div (~card :title id)))" "http"))
|
|
(p "The server resolves transitive deps via " (code "deps.py") ", subtracts anything listed in the " (code "SX-Components") " request header (already loaded), serializes and returns. This is essentially " (code "components_for_request()") " driven by an explicit " (code "?names=") " param.")
|
|
(p "Cache-friendly: the response is a pure function of component hash + requested names. " (code "Cache-Control: public, max-age=3600") " with the component hash as ETag."))
|
|
|
|
(~doc-subsection :title "Phase 2: Client Prefetch Logic (SX spec)"
|
|
(p "New functions in " (code "orchestration.sx") " (or a new " (code "prefetch.sx") " if scope warrants):")
|
|
|
|
(div :class "space-y-4"
|
|
(div
|
|
(h4 :class "font-semibold text-stone-700" "1. compute-missing-deps")
|
|
(p "Given a pathname, find the page, return dep names not in " (code "loaded-component-names") ". Returns nil if page not found or has data (can't client-route anyway).")
|
|
(~doc-code :code (highlight "(define compute-missing-deps\n (fn (pathname)\n (let ((match (find-matching-route pathname _page-routes)))\n (when (and match (not (get match \"has-data\")))\n (let ((deps (or (get match \"deps\") (list)))\n (loaded (loaded-component-names)))\n (filter (fn (d) (not (contains? loaded d))) deps))))))" "lisp")))
|
|
|
|
(div
|
|
(h4 :class "font-semibold text-stone-700" "2. prefetch-components")
|
|
(p "Fetch component definitions from the server for a list of names. Deduplicates in-flight requests. On success, parses and registers the returned definitions into the component env.")
|
|
(~doc-code :code (highlight "(define _prefetch-pending (dict))\n\n(define prefetch-components\n (fn (names)\n (let ((key (join \",\" (sort names))))\n (when (not (get _prefetch-pending key))\n (set! _prefetch-pending\n (merge _prefetch-pending {key true}))\n (fetch-components-from-server names\n (fn (sx-text)\n (sx-process-component-text sx-text)\n (dict-remove! _prefetch-pending key)))))))" "lisp")))
|
|
|
|
(div
|
|
(h4 :class "font-semibold text-stone-700" "3. prefetch-route-deps")
|
|
(p "High-level composition: compute missing deps for a route, fetch if any.")
|
|
(~doc-code :code (highlight "(define prefetch-route-deps\n (fn (pathname)\n (let ((missing (compute-missing-deps pathname)))\n (when (and missing (not (empty? missing)))\n (log-info (str \"sx:prefetch \"\n (len missing) \" components for \" pathname))\n (prefetch-components missing)))))" "lisp")))
|
|
|
|
(div
|
|
(h4 :class "font-semibold text-stone-700" "4. Trigger: link hover")
|
|
(p "On mouseover of a boosted link, prefetch its route's missing components. Debounced 150ms to avoid fetching on quick mouse-throughs.")
|
|
(~doc-code :code (highlight "(define bind-prefetch-on-hover\n (fn (link)\n (let ((timer nil))\n (dom-add-listener link \"mouseover\"\n (fn (e)\n (clear-timeout timer)\n (set! timer (set-timeout\n (fn () (prefetch-route-deps\n (url-pathname (dom-get-attr link \"href\"))))\n 150))))\n (dom-add-listener link \"mouseout\"\n (fn (e) (clear-timeout timer))))))" "lisp")))
|
|
|
|
(div
|
|
(h4 :class "font-semibold text-stone-700" "5. Trigger: viewport intersection (opt-in)")
|
|
(p "More aggressive strategy: when a link scrolls into view, prefetch its route's deps. Opt-in via " (code "sx-prefetch=\"visible\"") " attribute.")
|
|
(~doc-code :code (highlight "(define bind-prefetch-on-visible\n (fn (link)\n (observe-intersection link\n (fn () (prefetch-route-deps\n (url-pathname (dom-get-attr link \"href\"))))\n true 0)))" "lisp")))
|
|
|
|
(div
|
|
(h4 :class "font-semibold text-stone-700" "6. Integration into process-elements")
|
|
(p "During the existing hydration pass, for each boosted link:")
|
|
(~doc-code :code (highlight ";; In process-elements, after binding boost behavior:\n(when (and (should-boost-link? link)\n (dom-get-attr link \"href\"))\n (bind-prefetch-on-hover link))\n\n;; Explicit viewport prefetch:\n(when (dom-has-attr? link \"sx-prefetch\")\n (bind-prefetch-on-visible link))" "lisp")))))
|
|
|
|
(~doc-subsection :title "Phase 3: Boundary Declaration"
|
|
(p "Two new IO primitives in " (code "boundary.sx") " (browser-only):")
|
|
(~doc-code :code (highlight ";; IO primitives (browser-only)\n(io fetch-components-from-server (names callback) -> void)\n(io sx-process-component-text (sx-text) -> void)" "lisp"))
|
|
(p "These are thin wrappers around " (code "fetch()") " + the existing component script processing logic already in the boundary adapter."))
|
|
|
|
(~doc-subsection :title "Phase 4: Bootstrap"
|
|
(p (code "bootstrap_js.py") " picks up the new functions from the spec and emits them into " (code "sx-browser.js") ". The two new boundary IO functions get implemented in the JS boundary adapter — the hand-written glue code that the bootstrapper doesn't generate.")
|
|
(~doc-code :code (highlight "// fetch-components-from-server: calls the endpoint\nfunction fetchComponentsFromServer(names, callback) {\n const url = `${routePrefix}/sx/components?names=${names.join(\",\")}`;\n const headers = {\n \"SX-Components\": loadedComponentNames().join(\",\")\n };\n fetch(url, { headers })\n .then(r => r.ok ? r.text() : \"\")\n .then(text => callback(text))\n .catch(() => {}); // silent fail — prefetch is best-effort\n}\n\n// sx-process-component-text: parse defcomp/defmacro into env\nfunction sxProcessComponentText(sxText) {\n if (!sxText) return;\n const frag = document.createElement(\"div\");\n frag.innerHTML =\n `<script type=\"text/sx\" data-components>${sxText}<\\/script>`;\n Sx.processScripts(frag);\n}" "javascript"))))
|
|
|
|
;; -----------------------------------------------------------------------
|
|
;; Request flow
|
|
;; -----------------------------------------------------------------------
|
|
|
|
(~doc-section :title "Request Flow" :id "request-flow"
|
|
(p "End-to-end example: user hovers a link, components prefetch, click goes client-side.")
|
|
(~doc-code :code (highlight "User hovers link \"/docs/sx-manifesto\"\n |\n +-- bind-prefetch-on-hover fires (150ms debounce)\n |\n +-- compute-missing-deps(\"/docs/sx-manifesto\")\n | +-- find-matching-route -> page with deps:\n | | [\"~essay-sx-manifesto\", \"~doc-code\"]\n | +-- loaded-component-names -> [\"~nav\", \"~footer\", \"~doc-code\"]\n | +-- missing: [\"~essay-sx-manifesto\"]\n |\n +-- prefetch-components([\"~essay-sx-manifesto\"])\n | +-- GET /sx/components?names=~essay-sx-manifesto\n | | Headers: SX-Components: ~nav,~footer,~doc-code\n | +-- Server resolves transitive deps\n | | (also needs ~rich-text, subtracts already-loaded)\n | +-- Response:\n | (defcomp ~essay-sx-manifesto ...) \n | (defcomp ~rich-text ...)\n |\n +-- sx-process-component-text registers defcomps in env\n |\n +-- User clicks link\n +-- try-client-route(\"/docs/sx-manifesto\")\n +-- has-all-deps? -> true (prefetched!)\n +-- eval content -> DOM\n +-- Client-side render, no server roundtrip" "text")))
|
|
|
|
;; -----------------------------------------------------------------------
|
|
;; File changes
|
|
;; -----------------------------------------------------------------------
|
|
|
|
(~doc-section :title "File Changes" :id "file-changes"
|
|
(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")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Phase")))
|
|
(tbody
|
|
(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" "New " (code "sx_components_endpoint()") " route handler")
|
|
(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/infrastructure/factory.py")
|
|
(td :class "px-3 py-2 text-stone-700" "Register " (code "/sx/components") " route on all SX apps")
|
|
(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/ref/orchestration.sx")
|
|
(td :class "px-3 py-2 text-stone-700" "Prefetch functions: compute-missing-deps, prefetch-components, prefetch-route-deps, bind-prefetch-on-hover, bind-prefetch-on-visible")
|
|
(td :class "px-3 py-2 text-stone-600" "2"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/boundary.sx")
|
|
(td :class "px-3 py-2 text-stone-700" "Declare " (code "fetch-components-from-server") ", " (code "sx-process-component-text"))
|
|
(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/bootstrap_js.py")
|
|
(td :class "px-3 py-2 text-stone-700" "Emit new spec functions, boundary adapter stubs")
|
|
(td :class "px-3 py-2 text-stone-600" "4"))))))
|
|
|
|
;; -----------------------------------------------------------------------
|
|
;; Non-goals & rollout
|
|
;; -----------------------------------------------------------------------
|
|
|
|
(~doc-section :title "Non-Goals (This Phase)" :id "non-goals"
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
|
(li (strong "Analytics-driven prediction") " — no ML models or click-frequency heuristics. Trajectory prediction uses geometry, not statistics.")
|
|
(li (strong "Cross-service prefetch") " — components are per-service. A link to a different service domain is always a server navigation.")
|
|
(li (strong "Service worker caching") " — could layer on later, but basic fetch + in-memory registration is sufficient.")
|
|
(li (strong "Full client-side data evaluation") " — the components+data strategy fetches data from the server, it doesn't replicate server IO on the client. That's Phase 4 of the isomorphic roadmap.")))
|
|
|
|
(~doc-section :title "Rollout" :id "rollout"
|
|
(p "Incremental, each step independently valuable:")
|
|
(ol :class "list-decimal pl-5 text-stone-700 space-y-2"
|
|
(li (strong "Component endpoint") " — purely additive. Refactor " (code "components_for_request()") " to accept explicit " (code "?names=") " param.")
|
|
(li (strong "Core spec functions") " — " (code "compute-missing-deps") ", " (code "prefetch-components") ", " (code "prefetch-route-deps") " in orchestration.sx. Testable in isolation.")
|
|
(li (strong "Hover prefetch") " — wire " (code "bind-prefetch-on-hover") " into " (code "process-elements") ". All boosted links get it automatically. Console logs show activity.")
|
|
(li (strong "Idle batch prefetch") " — call " (code "prefetch-visible-links-on-idle") " from " (code "boot-init") ". One request prefetches all visible nav deps after page settles.")
|
|
(li (strong "Viewport + approach") " — opt-in via " (code "sx-prefetch") " attributes. Trajectory prediction for dense nav areas.")
|
|
(li (strong "Eager bundles") " — extend " (code "components_for_page()") " to include linked routes' deps. Heavier initial payload, zero-latency nav.")
|
|
(li (strong "Components + data split") " — new server response mode returning data bindings only. Client renders with prefetched components. Bridges toward Phase 4.")))
|
|
|
|
(~doc-section :title "Relationship to Isomorphic Roadmap" :id "relationship"
|
|
(p "This plan sits between Phase 3 (client-side routing) and Phase 4 (client async & IO bridge) of the "
|
|
(a :href "/plans/isomorphic-architecture" :class "text-violet-700 underline" "isomorphic architecture roadmap")
|
|
". It extends Phase 3 by making more navigations go client-side without needing any IO bridge — purely by ensuring component definitions are available before they're needed.")
|
|
(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 3 (client-side routing with deps checking). No dependency on Phase 4.")))))
|
|
;; ---------------------------------------------------------------------------
|
|
;; Plan Status Overview
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(defcomp ~plan-status-content ()
|
|
(~doc-page :title "Plan Status"
|
|
|
|
(p :class "text-lg text-stone-600 mb-6"
|
|
"Audit of all plans across the SX language and Rose Ash platform. Last updated March 2026.")
|
|
|
|
;; -----------------------------------------------------------------------
|
|
;; Completed
|
|
;; -----------------------------------------------------------------------
|
|
|
|
(~doc-section :title "Completed" :id "completed"
|
|
|
|
(div :class "space-y-4"
|
|
|
|
(div :class "rounded border border-green-200 bg-green-50 p-4"
|
|
(div :class "flex items-center gap-2 mb-1"
|
|
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
|
|
(span :class "font-semibold text-stone-800" "Split Cart into Microservices"))
|
|
(p :class "text-sm text-stone-600" "Cart decomposed into 4 services: relations (internal, owns ContainerRelation), likes (internal, unified generic likes), orders (public, owns Order/OrderItem + SumUp checkout), and cart (thin CartItem CRUD). All three new services deployed with dedicated databases."))
|
|
|
|
(div :class "rounded border border-green-200 bg-green-50 p-4"
|
|
(div :class "flex items-center gap-2 mb-1"
|
|
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
|
|
(span :class "font-semibold text-stone-800" "Ticket Purchase Through Cart"))
|
|
(p :class "text-sm text-stone-600" "Tickets flow through the cart like products: state=pending in cart, reserved at checkout, confirmed on payment. TicketDTO, CartSummaryDTO with ticket_count/ticket_total, CalendarService protocol methods all implemented."))
|
|
|
|
(div :class "rounded border border-green-200 bg-green-50 p-4"
|
|
(div :class "flex items-center gap-2 mb-1"
|
|
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
|
|
(span :class "font-semibold text-stone-800" "Ticket UX Improvements"))
|
|
(p :class "text-sm text-stone-600" "+/- quantity buttons on entry pages and cart. Tickets grouped by event in cart display. Adjust quantity route, sold/basket counts, matching product card UX pattern."))
|
|
|
|
(div :class "rounded border border-green-200 bg-green-50 p-4"
|
|
(div :class "flex items-center gap-2 mb-1"
|
|
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
|
|
(a :href "/isomorphism/" :class "font-semibold text-green-800 underline" "Isomorphic Phase 1: Dependency Analysis"))
|
|
(p :class "text-sm text-stone-600" "Per-page component bundles via deps.sx. Transitive closure, scan-refs, components-needed, page-css-classes. 15 tests, bootstrapped to both hosts."))
|
|
|
|
(div :class "rounded border border-green-200 bg-green-50 p-4"
|
|
(div :class "flex items-center gap-2 mb-1"
|
|
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
|
|
(a :href "/isomorphism/" :class "font-semibold text-green-800 underline" "Isomorphic Phase 2: IO Detection"))
|
|
(p :class "text-sm text-stone-600" "Automatic IO classification. scan-io-refs, transitive-io-refs, compute-all-io-refs. Server expands IO components, serializes pure ones for client."))
|
|
|
|
(div :class "rounded border border-green-200 bg-green-50 p-4"
|
|
(div :class "flex items-center gap-2 mb-1"
|
|
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
|
|
(a :href "/isomorphism/" :class "font-semibold text-green-800 underline" "Isomorphic Phase 3: Client-Side Routing"))
|
|
(p :class "text-sm text-stone-600" "router.sx spec, page registry via <script type=\"text/sx-pages\">, client route matching, try-first/fallback to server. Pure pages render without server roundtrips."))
|
|
|
|
(div :class "rounded border border-green-200 bg-green-50 p-4"
|
|
(div :class "flex items-center gap-2 mb-1"
|
|
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
|
|
(a :href "/isomorphism/" :class "font-semibold text-green-800 underline" "Isomorphic Phase 4: Client Async & IO Bridge"))
|
|
(p :class "text-sm text-stone-600" "Server evaluates :data expressions, serializes as SX wire format. Client fetches pre-evaluated data, caches with 30s TTL, renders :content locally. 30 unit tests."))
|
|
|
|
(div :class "rounded border border-green-200 bg-green-50 p-4"
|
|
(div :class "flex items-center gap-2 mb-1"
|
|
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
|
|
(a :href "/isomorphism/" :class "font-semibold text-green-800 underline" "Isomorphic Phase 5: Client IO Proxy"))
|
|
(p :class "text-sm text-stone-600" "IO primitives (highlight, asset-url, etc.) proxied to server via registerIoDeps(). Async DOM renderer handles promises through the render tree. Components with IO deps render client-side via server round-trips — no continuations needed."))
|
|
|
|
(div :class "rounded border border-green-200 bg-green-50 p-4"
|
|
(div :class "flex items-center gap-2 mb-1"
|
|
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
|
|
(a :href "/testing/" :class "font-semibold text-green-800 underline" "Modular Test Architecture"))
|
|
(p :class "text-sm text-stone-600" "Per-module test specs (eval, parser, router, render) with 161 tests. Three runners: Python, Node.js, browser. 5 platform functions, everything else pure SX."))))
|
|
|
|
;; -----------------------------------------------------------------------
|
|
;; In Progress / Partial
|
|
;; -----------------------------------------------------------------------
|
|
|
|
(~doc-section :title "In Progress" :id "in-progress"
|
|
|
|
(div :class "space-y-4"
|
|
|
|
(div :class "rounded border border-amber-200 bg-amber-50 p-4"
|
|
(div :class "flex items-center gap-2 mb-1"
|
|
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-amber-600 text-white uppercase" "Partial")
|
|
(a :href "/plans/fragment-protocol" :class "font-semibold text-amber-900 underline" "Fragment Protocol"))
|
|
(p :class "text-sm text-stone-600" "Fragment GET infrastructure works. The planned POST/sexp structured protocol for transferring component definitions between services is not yet implemented. Fragment endpoints still use legacy GET + X-Fragment-Request headers."))))
|
|
|
|
;; -----------------------------------------------------------------------
|
|
;; Not Started
|
|
;; -----------------------------------------------------------------------
|
|
|
|
(~doc-section :title "Not Started" :id "not-started"
|
|
|
|
(div :class "space-y-4"
|
|
|
|
(div :class "rounded border border-stone-200 bg-stone-50 p-4"
|
|
(div :class "flex items-center gap-2 mb-1"
|
|
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-stone-500 text-white uppercase" "Not Started")
|
|
(a :href "/plans/reader-macros" :class "font-semibold text-stone-800 underline" "Reader Macros"))
|
|
(p :class "text-sm text-stone-600" "# dispatch character for datum comments (#;), raw strings (#|...|), and quote shorthand (#'). Fully designed but no implementation in parser.sx or parser.py.")
|
|
(p :class "text-sm text-stone-500 mt-1" "Remaining: spec in parser.sx, Python in parser.py, rebootstrap both targets."))
|
|
|
|
(div :class "rounded border border-stone-200 bg-stone-50 p-4"
|
|
(div :class "flex items-center gap-2 mb-1"
|
|
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-stone-500 text-white uppercase" "Not Started")
|
|
(a :href "/plans/sx-activity" :class "font-semibold text-stone-800 underline" "SX-Activity"))
|
|
(p :class "text-sm text-stone-600" "Federated SX over ActivityPub — 6 phases from SX wire format for activities to the evaluable web on IPFS. Existing AP infrastructure provides the foundation but no SX-specific federation code exists.")
|
|
(p :class "text-sm text-stone-500 mt-1" "Remaining: shared/sx/activity.py (SX<->JSON-LD), shared/sx/ipfs.py, shared/sx/ref/ipfs-resolve.sx, shared/sx/registry.py, shared/sx/anchor.py."))
|
|
|
|
(div :class "rounded border border-stone-200 bg-stone-50 p-4"
|
|
(div :class "flex items-center gap-2 mb-1"
|
|
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-stone-500 text-white uppercase" "Not Started")
|
|
(a :href "/plans/glue-decoupling" :class "font-semibold text-stone-800 underline" "Cross-App Decoupling via Glue"))
|
|
(p :class "text-sm text-stone-600" "Eliminate all cross-app model imports by routing through a glue service layer. No glue/ directory exists. Apps are currently decoupled via HTTP interfaces and DTOs instead.")
|
|
(p :class "text-sm text-stone-500 mt-1" "Remaining: glue/services/ for pages, page_config, calendars, marketplaces, cart_items, products, post_associations. 25+ cross-app imports to eliminate."))
|
|
|
|
(div :class "rounded border border-stone-200 bg-stone-50 p-4"
|
|
(div :class "flex items-center gap-2 mb-1"
|
|
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-stone-500 text-white uppercase" "Not Started")
|
|
(a :href "/plans/social-sharing" :class "font-semibold text-stone-800 underline" "Social Network Sharing"))
|
|
(p :class "text-sm text-stone-600" "OAuth-based sharing to Facebook, Instagram, Threads, Twitter/X, LinkedIn, and Mastodon via the account service. No models, blueprints, or platform clients created.")
|
|
(p :class "text-sm text-stone-500 mt-1" "Remaining: SocialConnection model, social_crypto.py, platform OAuth clients (6), account/bp/social/ blueprint, share button fragment."))
|
|
|
|
(div :class "rounded border border-green-200 bg-green-50 p-4"
|
|
(div :class "flex items-center gap-2 mb-1"
|
|
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
|
|
(a :href "/isomorphism/" :class "font-semibold text-stone-800 underline" "Isomorphic Phase 6: Streaming & Suspense"))
|
|
(p :class "text-sm text-stone-600" "Server streams partially-evaluated SX as IO resolves. ~suspense component renders fallbacks, inline resolution scripts fill in content. Concurrent IO via asyncio, chunked transfer encoding.")
|
|
(p :class "text-sm text-stone-500 mt-1" "Demo: " (a :href "/isomorphism/streaming" "/isomorphism/streaming")))
|
|
|
|
(div :class "rounded border border-stone-200 bg-stone-50 p-4"
|
|
(div :class "flex items-center gap-2 mb-1"
|
|
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-stone-500 text-white uppercase" "Not Started")
|
|
(a :href "/isomorphism/" :class "font-semibold text-stone-800 underline" "Isomorphic Phase 7: Full Isomorphism"))
|
|
(p :class "text-sm text-stone-600" "Runtime boundary optimizer, affinity annotations, offline data layer via Service Worker + IndexedDB, isomorphic testing harness.")
|
|
(p :class "text-sm text-stone-500 mt-1" "Depends on: all previous phases."))))))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Fragment Protocol
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(defcomp ~plan-fragment-protocol-content ()
|
|
(~doc-page :title "Fragment Protocol"
|
|
|
|
(~doc-section :title "Context" :id "context"
|
|
(p "Fragment endpoints return raw sexp source (e.g., " (code "(~blog-nav-wrapper :items ...)") "). The consuming service embeds this in its page sexp, which the client evaluates. But service-specific components like " (code "~blog-nav-wrapper") " are only in that service's component env — not in the consumer's. So the consumer's " (code "client_components_tag()") " never sends them to the client, causing \"Unknown component\" errors.")
|
|
(p "The fix: transfer component definitions alongside fragments. Services tell the provider what they already have; the provider sends only what's missing."))
|
|
|
|
(~doc-section :title "What exists" :id "exists"
|
|
(div :class "rounded border border-green-200 bg-green-50 p-4"
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
|
(li "Fragment GET infrastructure works (" (code "shared/infrastructure/fragments.py") ")")
|
|
(li (code "X-Fragment-Request") " header protocol for internal service calls")
|
|
(li "Content type negotiation for text/html and text/sx responses")
|
|
(li "Fragment caching and composition in page rendering"))))
|
|
|
|
(~doc-section :title "What remains" :id "remains"
|
|
(div :class "rounded border border-amber-200 bg-amber-50 p-4"
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
|
(li (strong "POST sexp protocol: ") "Switch from GET to POST with structured sexp body containing " (code ":components") " list of what consumer already has")
|
|
(li (strong "Structured response: ") (code "(fragment-response :defs (...) :content (...))") " — provider sends only missing component defs")
|
|
(li (strong (code "fragment_response()") " builder: ") "New function in helpers.py that diffs provider's component env against consumer's list")
|
|
(li (strong "Register received defs: ") "Consumer parses " (code ":defs") " from response and registers into its " (code "_COMPONENT_ENV"))
|
|
(li (strong "Shared blueprint factory: ") (code "create_fragment_blueprint(handlers)") " to deduplicate the identical fragment endpoint pattern across 8 services"))))
|
|
|
|
(~doc-section :title "Files to modify" :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/infrastructure/fragments.py")
|
|
(td :class "px-3 py-2 text-stone-700" "POST sexp body, parse response, register defs"))
|
|
(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" "fragment_response() builder"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/infrastructure/fragment_endpoint.py")
|
|
(td :class "px-3 py-2 text-stone-700" "NEW — shared blueprint factory"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "*/bp/fragments/routes.py")
|
|
(td :class "px-3 py-2 text-stone-700" "All 8 services: use create_fragment_blueprint"))))))))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Glue Decoupling
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(defcomp ~plan-glue-decoupling-content ()
|
|
(~doc-page :title "Cross-App Decoupling via Glue Services"
|
|
|
|
(~doc-section :title "Context" :id "context"
|
|
(p "All cross-domain FK constraints have been dropped (with pragmatic exceptions for OrderItem.product_id and CartItem). Cross-domain writes go through internal HTTP and activity bus. However, " (strong "25+ cross-app model imports remain") " — apps still import from each other's models/ directories. This means every app needs every other app's code on disk to start.")
|
|
(p "The goal: eliminate all cross-app model imports. Every app only imports from its own models/, from shared/, and from a new glue/ service layer."))
|
|
|
|
(~doc-section :title "Current state" :id "current"
|
|
(p "Apps are partially decoupled via HTTP interfaces (fetch_data, call_action, send_internal_activity) and DTOs. The Cart microservice split (relations, likes, orders) is complete. But direct model imports persist in:")
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
|
(li (strong "Cart") " — 9 files importing from market, events, blog")
|
|
(li (strong "Blog") " — 8 files importing from cart, events, market")
|
|
(li (strong "Events") " — 5 files importing from blog, market, cart")
|
|
(li (strong "Market") " — 1 file importing from blog")))
|
|
|
|
(~doc-section :title "What remains" :id "remains"
|
|
(div :class "space-y-3"
|
|
(div :class "rounded border border-stone-200 p-3"
|
|
(h4 :class "font-semibold text-stone-700" "1. glue/services/pages.py")
|
|
(p :class "text-sm text-stone-600" "Dict-based Post access for non-blog apps: get_page_by_slug, get_page_by_id, get_pages_by_ids, page_exists, search_posts."))
|
|
(div :class "rounded border border-stone-200 p-3"
|
|
(h4 :class "font-semibold text-stone-700" "2. glue/services/page_config.py")
|
|
(p :class "text-sm text-stone-600" "PageConfig CRUD: get_page_config, get_or_create_page_config, get_page_configs_by_ids."))
|
|
(div :class "rounded border border-stone-200 p-3"
|
|
(h4 :class "font-semibold text-stone-700" "3. glue/services/calendars.py")
|
|
(p :class "text-sm text-stone-600" "Calendar queries + entry associations (from blog): get_calendars_for_page, toggle_entry_association, get_associated_entries."))
|
|
(div :class "rounded border border-stone-200 p-3"
|
|
(h4 :class "font-semibold text-stone-700" "4. glue/services/marketplaces.py")
|
|
(p :class "text-sm text-stone-600" "MarketPlace CRUD (from blog+events): get_marketplaces_for_page, create_marketplace, soft_delete_marketplace."))
|
|
(div :class "rounded border border-stone-200 p-3"
|
|
(h4 :class "font-semibold text-stone-700" "5. glue/services/cart_items.py")
|
|
(p :class "text-sm text-stone-600" "CartItem/CalendarEntry queries for cart: get_cart_items, find_or_create_cart_item, clear_cart_for_order."))
|
|
(div :class "rounded border border-stone-200 p-3"
|
|
(h4 :class "font-semibold text-stone-700" "6. glue/services/products.py")
|
|
(p :class "text-sm text-stone-600" "Minimal Product access for cart orders: get_product."))
|
|
(div :class "rounded border border-stone-200 p-3"
|
|
(h4 :class "font-semibold text-stone-700" "7. Model registration + cleanup")
|
|
(p :class "text-sm text-stone-600" "register_models() in glue/setup.py, update all app.py files, delete moved service files."))))
|
|
|
|
(~doc-section :title "Docker consideration" :id "docker"
|
|
(p :class "text-stone-600" "For glue services to work in Docker (single app per container), model files from other apps must be importable. Recommended: try/except at import time — glue services that can't import a model raise ImportError at call time, which only happens if called from the wrong app."))))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Social Sharing
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(defcomp ~plan-social-sharing-content ()
|
|
(~doc-page :title "Social Network Sharing"
|
|
|
|
(~doc-section :title "Context" :id "context"
|
|
(p "Rose Ash already has ActivityPub for federated social sharing. This plan adds OAuth-based sharing to mainstream social networks — Facebook, Instagram, Threads, Twitter/X, LinkedIn, and Mastodon.")
|
|
(p "All social logic lives in the " (strong "account") " microservice. Content apps get a share button that opens the account share page."))
|
|
|
|
(~doc-section :title "What remains" :id "remains"
|
|
(~doc-note "Nothing has been implemented. This is the full scope of work.")
|
|
|
|
(div :class "space-y-4"
|
|
|
|
(div :class "rounded border border-stone-200 p-4"
|
|
(h4 :class "font-semibold text-stone-700 mb-2" "Phase 1: Data Model + Encryption")
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1 text-sm"
|
|
(li (code "shared/models/social_connection.py") " — SocialConnection model (user_id, platform, tokens, scopes, extra_data)")
|
|
(li (code "shared/infrastructure/social_crypto.py") " — Fernet encrypt/decrypt for tokens")
|
|
(li "Alembic migration for social_connections table")
|
|
(li "Environment variables for per-platform OAuth credentials")))
|
|
|
|
(div :class "rounded border border-stone-200 p-4"
|
|
(h4 :class "font-semibold text-stone-700 mb-2" "Phase 2: Platform OAuth Clients")
|
|
(p :class "text-sm text-stone-600 mb-2" "All in " (code "account/services/social_platforms/") ":")
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1 text-sm"
|
|
(li (code "base.py") " — SocialPlatform ABC, OAuthResult, ShareResult")
|
|
(li (code "meta.py") " — Facebook + Instagram + Threads (Graph API)")
|
|
(li (code "twitter.py") " — OAuth 2.0 with PKCE")
|
|
(li (code "linkedin.py") " — LinkedIn Posts API")
|
|
(li (code "mastodon.py") " — Dynamic app registration per instance")))
|
|
|
|
(div :class "rounded border border-stone-200 p-4"
|
|
(h4 :class "font-semibold text-stone-700 mb-2" "Phase 3: Account Blueprint")
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1 text-sm"
|
|
(li (code "account/bp/social/routes.py") " — /social/ list, /social/connect/<platform>/, /social/callback/<platform>/, /social/share/")
|
|
(li "Register before account blueprint (account has catch-all /<slug>/ route)")))
|
|
|
|
(div :class "rounded border border-stone-200 p-4"
|
|
(h4 :class "font-semibold text-stone-700 mb-2" "Phase 4: Templates")
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1 text-sm"
|
|
(li "Social panel — platform cards, connect/disconnect")
|
|
(li "Share panel — content preview, account checkboxes, share button")
|
|
(li "Share result — per-platform success/failure with links")))
|
|
|
|
(div :class "rounded border border-stone-200 p-4"
|
|
(h4 :class "font-semibold text-stone-700 mb-2" "Phase 5: Share Button in Content Apps")
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1 text-sm"
|
|
(li "share-button fragment from account service")
|
|
(li "Blog, events, market detail pages fetch and render the fragment")))
|
|
|
|
(div :class "rounded border border-stone-200 p-4"
|
|
(h4 :class "font-semibold text-stone-700 mb-2" "Phase 6: Token Refresh + Share History")
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1 text-sm"
|
|
(li "Automatic token refresh before posting")
|
|
(li "Optional social_shares table for history and duplicate prevention")))))))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; 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 knows which components have data fetches (via IO detection in " (a :href "/specs/deps" :class "text-violet-700 underline" "deps.sx") "), resolves those server-side, and sends the rest as pure SX for client rendering. The boundary slides automatically based on what each component actually needs."))
|
|
|
|
(~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.")
|
|
(li (strong "Boundary enforcement: ") "boundary.sx + SX_BOUNDARY_STRICT=1 validates all primitives/IO/helpers at registration.")
|
|
(li (strong "Dependency analysis: ") "deps.sx computes per-page component bundles — only definitions a page actually uses are sent.")
|
|
(li (strong "IO detection: ") "deps.sx classifies every component as pure or IO-dependent. Server expands IO components, serializes pure ones for client.")
|
|
(li (strong "Client-side routing: ") "router.sx matches URL patterns. Pure pages render instantly without server roundtrips. Pages with :data fall through to server transparently.")
|
|
(li (strong "Client IO proxy: ") "IO primitives registered on the client call back to the server via fetch. Components with IO deps can render client-side.")
|
|
(li (strong "Streaming/suspense: ") "defpage :stream true enables chunked HTML. ~suspense placeholders show loading skeletons; __sxResolve() fills in content as IO completes.")))
|
|
|
|
;; -----------------------------------------------------------------------
|
|
;; Phase 1
|
|
;; -----------------------------------------------------------------------
|
|
|
|
(~doc-section :title "Phase 1: Component Distribution & Dependency Analysis" :id "phase-1"
|
|
|
|
(div :class "rounded border border-green-300 bg-green-50 p-4 mb-4"
|
|
(div :class "flex items-center gap-2 mb-2"
|
|
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
|
|
(a :href "/specs/deps" :class "text-green-700 underline text-sm font-medium" "View canonical spec: deps.sx")
|
|
(a :href "/isomorphism/bundle-analyzer" :class "text-green-700 underline text-sm font-medium" "Live bundle analyzer"))
|
|
(p :class "text-green-900 font-medium" "What it enables")
|
|
(p :class "text-green-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 "The page boot payload serializes every component definition in the environment. A page that uses 5 components still receives all 50+. No mechanism determines which components a page actually needs — the boundary between \"loaded\" and \"used\" is invisible."))
|
|
|
|
(~doc-subsection :title "Implementation"
|
|
|
|
(p "The dependency analysis algorithm is defined in "
|
|
(a :href "/specs/deps" :class "text-violet-700 underline" "deps.sx")
|
|
" — a spec module bootstrapped to every host. Each host loads it via " (code "--spec-modules deps") " and provides 6 platform functions. The spec is the single source of truth; hosts are interchangeable.")
|
|
|
|
(div :class "space-y-4"
|
|
(div
|
|
(h4 :class "font-semibold text-stone-700" "1. Transitive closure (deps.sx)")
|
|
(p "9 functions that walk the component graph. The core:")
|
|
(~doc-code :code (highlight "(define (transitive-deps name env)\n (let ((key (if (starts-with? name \"~\") name\n (concat \"~\" name)))\n (seen (set-create)))\n (transitive-deps-walk key env seen)\n (set-remove seen key)))" "lisp"))
|
|
(p (code "scan-refs") " walks a component body AST collecting " (code "~") " symbols. "
|
|
(code "transitive-deps") " follows references recursively through the env. "
|
|
(code "compute-all-deps") " batch-computes and caches deps for every component. "
|
|
"Circular references terminate safely via a seen-set."))
|
|
|
|
(div
|
|
(h4 :class "font-semibold text-stone-700" "2. Page scanning")
|
|
(~doc-code :code (highlight "(define (components-needed page-source env)\n (let ((direct (scan-components-from-source page-source))\n (all-needed (set-create)))\n (for-each (fn (name) ...\n (set-add! all-needed name)\n (set-union! all-needed (component-deps comp)))\n direct)\n all-needed))" "lisp"))
|
|
(p (code "scan-components-from-source") " finds " (code "(~name") " patterns in serialized SX via regex. " (code "components-needed") " combines scanning with the cached transitive closure to produce the minimal component set for a page."))
|
|
|
|
(div
|
|
(h4 :class "font-semibold text-stone-700" "3. Per-page CSS scoping")
|
|
(p (code "page-css-classes") " unions the CSS classes from only the components in the page bundle. Pages that don't use a component never pay for its styles."))
|
|
|
|
(div
|
|
(h4 :class "font-semibold text-stone-700" "4. Platform interface")
|
|
(p "The spec declares 6 functions each host implements natively — the only host-specific code:")
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
|
(li (code "component-deps") " / " (code "component-set-deps!") " — read/write the cached deps set on a component object")
|
|
(li (code "component-css-classes") " — read pre-scanned CSS class names from a component")
|
|
(li (code "env-components") " — enumerate all component entries in an environment")
|
|
(li (code "regex-find-all") " / " (code "scan-css-classes") " — host-native regex and CSS scanning")))))
|
|
|
|
(~doc-subsection :title "Spec module"
|
|
(p "deps.sx is loaded as a " (strong "spec module") " — an optional extension to the core spec. The bootstrapper flag " (code "--spec-modules deps") " includes it in the generated output alongside the core evaluator, parser, and renderer. Phase 2 IO detection was added to the same module — same bootstrapping mechanism, no architecture changes needed.")
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
|
|
(li "shared/sx/ref/deps.sx — canonical spec (14 functions, 8 platform declarations)")
|
|
(li "Bootstrapped to all host targets via --spec-modules deps")))
|
|
|
|
(~doc-subsection :title "Verification"
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
|
(li "15 dedicated tests: scan, transitive closure, circular deps, compute-all, components-needed")
|
|
(li "Bootstrapped output verified on both host targets")
|
|
(li "Full test suite passes with zero regressions")
|
|
(li (a :href "/isomorphism/bundle-analyzer" :class "text-violet-700 underline" "Live bundle analyzer") " shows real per-page savings on this app"))))
|
|
|
|
;; -----------------------------------------------------------------------
|
|
;; Phase 2
|
|
;; -----------------------------------------------------------------------
|
|
|
|
(~doc-section :title "Phase 2: Smart Server/Client Boundary — IO Detection" :id "phase-2"
|
|
|
|
(div :class "rounded border border-green-300 bg-green-50 p-4 mb-4"
|
|
(div :class "flex items-center gap-2 mb-2"
|
|
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
|
|
(a :href "/specs/deps" :class "text-green-700 underline text-sm font-medium" "View canonical spec: deps.sx")
|
|
(a :href "/isomorphism/bundle-analyzer" :class "text-green-700 underline text-sm font-medium" "Live bundle analyzer with IO"))
|
|
(p :class "text-green-900 font-medium" "What it enables")
|
|
(p :class "text-green-800" "Automatic IO detection and selective expansion. Server expands IO-dependent components, serializes pure ones for client. Per-component intelligence replaces global toggle."))
|
|
|
|
(~doc-subsection :title "IO Detection in the Spec"
|
|
(p "Five new functions in "
|
|
(a :href "/specs/deps" :class "text-violet-700 underline" "deps.sx")
|
|
" extend the Phase 1 walker to detect IO primitive references:")
|
|
|
|
(div :class "space-y-4"
|
|
(div
|
|
(h4 :class "font-semibold text-stone-700" "1. IO scanning")
|
|
(p (code "scan-io-refs") " walks an AST node, collecting symbol names that match an IO name set. The IO set is provided by the host from boundary declarations (all three tiers: core IO, deployment IO, page helpers).")
|
|
(~doc-code :code (highlight "(define scan-io-refs\n (fn (node io-names)\n (let ((refs (list)))\n (scan-io-refs-walk node io-names refs)\n refs)))" "lisp")))
|
|
|
|
(div
|
|
(h4 :class "font-semibold text-stone-700" "2. Transitive IO closure")
|
|
(p (code "transitive-io-refs") " follows component deps recursively, unioning IO refs from all reachable components and macros. Cycle-safe via seen-set.")
|
|
(~doc-code :code (highlight "(define transitive-io-refs\n (fn (name env io-names)\n ;; Walk deps, scan each body for IO refs,\n ;; union all refs transitively.\n ...))" "lisp")))
|
|
|
|
(div
|
|
(h4 :class "font-semibold text-stone-700" "3. Batch computation")
|
|
(p (code "compute-all-io-refs") " iterates the env, computes transitive IO refs for each component, and caches the result via " (code "component-set-io-refs!") ". Called after " (code "compute-all-deps") " at component registration time."))
|
|
|
|
(div
|
|
(h4 :class "font-semibold text-stone-700" "4. Component metadata")
|
|
(p "Each component now carries " (code "io_refs") " (transitive IO primitive names) alongside " (code "deps") " and " (code "css_classes") ". The derived " (code "is_pure") " property is true when " (code "io_refs") " is empty — the component can render anywhere without server data."))))
|
|
|
|
(~doc-subsection :title "Selective Expansion"
|
|
(p "The partial evaluator " (code "_aser") " now uses per-component IO metadata instead of a global toggle:")
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
|
(li (strong "IO-dependent") " → expand server-side (IO must resolve)")
|
|
(li (strong "Pure") " → serialize for client (let client render)")
|
|
(li (strong "Layout slot context") " → all components still expand (backwards compat via " (code "_expand_components") " context var)"))
|
|
(p "A component calling " (code "(highlight ...)") " or " (code "(query ...)") " is IO-dependent. A component with only HTML tags and string ops is pure."))
|
|
|
|
(~doc-subsection :title "Platform interface additions"
|
|
(p "Two new platform functions each host implements:")
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
|
|
(li "(component-io-refs c) → cached IO ref list")
|
|
(li "(component-set-io-refs! c refs) → cache IO refs on component")))
|
|
|
|
(~doc-subsection :title "Verification"
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
|
(li "Components calling (query ...) or (highlight ...) classified IO-dependent")
|
|
(li "Pure components (HTML-only) classified pure with empty io_refs")
|
|
(li "Transitive IO detection: component calling ~other where ~other calls (current-user) → IO-dependent")
|
|
(li "Bootstrapped to both hosts (sx_ref.py + sx-ref.js)")
|
|
(li (a :href "/isomorphism/bundle-analyzer" :class "text-violet-700 underline" "Live bundle analyzer") " shows per-page IO classification"))))
|
|
|
|
;; -----------------------------------------------------------------------
|
|
;; Phase 3
|
|
;; -----------------------------------------------------------------------
|
|
|
|
(~doc-section :title "Phase 3: Client-Side Routing (SPA Mode)" :id "phase-3"
|
|
|
|
(div :class "rounded border border-green-300 bg-green-50 p-4 mb-4"
|
|
(div :class "flex items-center gap-2 mb-2"
|
|
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
|
|
(a :href "/specs/router" :class "text-green-700 underline text-sm font-medium" "View canonical spec: router.sx")
|
|
(a :href "/isomorphism/routing-analyzer" :class "text-green-700 underline text-sm font-medium" "Live routing analyzer"))
|
|
(p :class "text-green-900 font-medium" "What it enables")
|
|
(p :class "text-green-800" "After initial page load, pure pages render instantly without server roundtrips. Client matches routes locally, evaluates content expressions with cached components, and only falls back to server for pages with :data dependencies."))
|
|
|
|
(~doc-subsection :title "Architecture"
|
|
(p "Three-layer approach: spec defines pure route matching, page registry bridges server metadata to client, orchestration intercepts navigation for try-first/fallback.")
|
|
|
|
(div :class "space-y-4"
|
|
(div
|
|
(h4 :class "font-semibold text-stone-700" "1. Route matching spec (router.sx)")
|
|
(p "New spec module with pure functions for Flask-style route pattern matching:")
|
|
(~doc-code :code (highlight "(define split-path-segments ;; \"/docs/hello\" → (\"docs\" \"hello\")\n(define parse-route-pattern ;; \"/docs/<slug>\" → segment descriptors\n(define match-route-segments ;; segments + pattern → params dict or nil\n(define find-matching-route ;; path + route table → first match" "lisp"))
|
|
(p "No platform interface needed — uses only pure string and list primitives. Bootstrapped to both hosts via " (code "--spec-modules deps,router") "."))
|
|
|
|
(div
|
|
(h4 :class "font-semibold text-stone-700" "2. Page registry")
|
|
(p "Server serializes defpage metadata as SX dict literals inside " (code "<script type=\"text/sx-pages\">") ". Each entry carries name, path pattern, auth level, has-data flag, serialized content expression, and closure values.")
|
|
(~doc-code :code (highlight "{:name \"docs-page\" :path \"/docs/<slug>\"\n :auth \"public\" :has-data false\n :content \"(case slug ...)\" :closure {}}" "lisp"))
|
|
(p "boot.sx processes these at startup using the SX parser — the same " (code "parse") " function from parser.sx — building route entries with parsed patterns into the " (code "_page-routes") " table. No JSON dependency."))
|
|
|
|
(div
|
|
(h4 :class "font-semibold text-stone-700" "3. Client-side interception (orchestration.sx)")
|
|
(p (code "bind-client-route-link") " replaces " (code "bind-boost-link") " in boost processing. On click:")
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
|
(li "Extract pathname from href")
|
|
(li "Call " (code "find-matching-route") " against " (code "_page-routes"))
|
|
(li "If match found AND no :data: evaluate content expression locally with component env + URL params")
|
|
(li "If evaluation succeeds: swap into #main-panel, pushState, log " (code "\"sx:route client /path\""))
|
|
(li "If anything fails (no match, has data, eval error): transparent fallback to server fetch"))
|
|
(p (code "handle-popstate") " also tries client routing before server fetch on back/forward."))))
|
|
|
|
(~doc-subsection :title "What becomes client-routable"
|
|
(p "All pages with content expressions — most of this docs app. Pure pages render instantly; :data pages fetch data then render client-side (Phase 4):")
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
|
(li (code "/") ", " (code "/docs/") ", " (code "/docs/<slug>") " (most slugs), " (code "/protocols/") ", " (code "/protocols/<slug>"))
|
|
(li (code "/examples/") ", " (code "/examples/<slug>") ", " (code "/essays/") ", " (code "/essays/<slug>"))
|
|
(li (code "/plans/") ", " (code "/plans/<slug>") ", " (code "/isomorphism/") ", " (code "/bootstrappers/")))
|
|
(p "Pages that fall through to server:")
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
|
(li (code "/docs/primitives") " and " (code "/docs/special-forms") " (call " (code "primitives-data") " / " (code "special-forms-data") " helpers)")
|
|
(li (code "/reference/<slug>") " (has " (code ":data (reference-data slug)") ")")
|
|
(li (code "/bootstrappers/<slug>") " (has " (code ":data (bootstrapper-data slug)") ")")
|
|
(li (code "/isomorphism/bundle-analyzer") " (has " (code ":data (bundle-analyzer-data)") ")")
|
|
(li (code "/isomorphism/data-test") " (has " (code ":data (data-test-data)") " — " (a :href "/isomorphism/data-test" :class "text-violet-700 underline" "Phase 4 demo") ")")))
|
|
|
|
(~doc-subsection :title "Try-first/fallback design"
|
|
(p "Client routing uses a try-first approach: attempt local evaluation in a try/catch, fall back to server fetch on any failure. This avoids needing perfect static analysis of content expressions — if a content expression calls a page helper the client doesn't have, the eval throws, and the server handles it transparently.")
|
|
(p "Console messages provide visibility: " (code "sx:route client /essays/why-sexps") " vs " (code "sx:route server /specs/eval") "."))
|
|
|
|
(~doc-subsection :title "Files"
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
|
|
(li "shared/sx/ref/router.sx — route pattern matching spec")
|
|
(li "shared/sx/ref/boot.sx — process page registry scripts")
|
|
(li "shared/sx/ref/orchestration.sx — client route interception")
|
|
(li "shared/sx/ref/bootstrap_js.py — router spec module + platform functions")
|
|
(li "shared/sx/ref/bootstrap_py.py — router spec module (parity)")
|
|
(li "shared/sx/helpers.py — page registry SX serialization")))
|
|
|
|
(~doc-subsection :title "Verification"
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
|
(li "Pure page navigation: zero server requests, console shows \"sx:route client\"")
|
|
(li "IO/data page fallback: falls through to server fetch transparently")
|
|
(li "Browser back/forward works with client-routed pages")
|
|
(li "Disabling page registry → identical behavior to before")
|
|
(li "Bootstrap parity: sx_ref.py and sx-ref.js both contain router functions"))))
|
|
|
|
;; -----------------------------------------------------------------------
|
|
;; Phase 4
|
|
;; -----------------------------------------------------------------------
|
|
|
|
(~doc-section :title "Phase 4: Client Async & IO Bridge" :id "phase-4"
|
|
|
|
(div :class "rounded border border-green-300 bg-green-50 p-4 mb-4"
|
|
(div :class "flex items-center gap-2 mb-2"
|
|
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
|
|
(a :href "/isomorphism/data-test" :class "text-green-700 underline text-sm font-medium" "Live data test page"))
|
|
(p :class "text-green-900 font-medium" "What it enables")
|
|
(p :class "text-green-800" "Client fetches server-evaluated data and renders :data pages locally. Data cached with TTL to avoid redundant fetches on back/forward navigation. All IO stays server-side — no continuations needed."))
|
|
|
|
(~doc-subsection :title "Architecture"
|
|
(p "Separates IO from rendering. Server evaluates :data expression (async, with DB/service access), serializes result as SX wire format. Client fetches pre-evaluated data, parses it, merges into env, renders pure :content client-side.")
|
|
|
|
(div :class "space-y-4"
|
|
(div
|
|
(h4 :class "font-semibold text-stone-700" "1. Abstract resolve-page-data")
|
|
(p "Spec-level primitive in orchestration.sx. The spec says \"I need data for this page\" — platform provides transport:")
|
|
(~doc-code :code (highlight "(resolve-page-data page-name params\n (fn (data)\n ;; data is a dict — merge into env and render\n (let ((env (merge closure params data))\n (rendered (try-eval-content content-src env)))\n (swap-rendered-content target rendered pathname))))" "lisp"))
|
|
(p "Browser platform: HTTP fetch to " (code "/sx/data/<page-name>") ". Future platforms could use IPC, cache, WebSocket, etc."))
|
|
|
|
(div
|
|
(h4 :class "font-semibold text-stone-700" "2. Server data endpoint")
|
|
(p (code "evaluate_page_data()") " evaluates the :data expression, kebab-cases dict keys (Python " (code "total_count") " → SX " (code "total-count") "), serializes as SX wire format.")
|
|
(p "Response content type: " (code "text/sx; charset=utf-8") ". Per-page auth enforcement via " (code "_check_page_auth()") "."))
|
|
|
|
(div
|
|
(h4 :class "font-semibold text-stone-700" "3. Client data cache")
|
|
(p "In-memory cache in orchestration.sx, keyed by " (code "page-name:param=value") ". 30-second TTL prevents redundant fetches on back/forward navigation:")
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
|
(li "Cache miss: " (code "sx:route client+data /path") " — fetches from server, caches, renders")
|
|
(li "Cache hit: " (code "sx:route client+cache /path") " — instant render from cached data")
|
|
(li "After TTL: stale entry evicted, fresh fetch on next visit"))
|
|
(p "Try it: navigate to the " (a :href "/isomorphism/data-test" :class "text-violet-700 underline" "data test page") ", go back, return within 30s — the server-time stays the same (cached). Wait 30s+ and return — new time (fresh fetch)."))))
|
|
|
|
(~doc-subsection :title "Files"
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
|
|
(li "shared/sx/ref/orchestration.sx — resolve-page-data spec, data cache")
|
|
(li "shared/sx/ref/bootstrap_js.py — platform resolvePageData (HTTP fetch)")
|
|
(li "shared/sx/pages.py — evaluate_page_data(), auto_mount_page_data()")
|
|
(li "shared/sx/helpers.py — deps for :data pages in page registry")
|
|
(li "sx/sx/data-test.sx — test component")
|
|
(li "shared/sx/tests/test_page_data.py — 30 unit tests")))
|
|
|
|
(~doc-subsection :title "Verification"
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
|
(li "30 unit tests: serialize roundtrip, kebab-case, deps, full pipeline simulation, cache TTL")
|
|
(li "Console: " (code "sx:route client+data") " on first visit, " (code "sx:route client+cache") " on return within 30s")
|
|
(li (a :href "/isomorphism/data-test" :class "text-violet-700 underline" "Live data test page") " exercises the full pipeline with server time + pipeline steps")
|
|
(li "append! and dict-set! registered as proper primitives in spec + both hosts"))))
|
|
|
|
;; -----------------------------------------------------------------------
|
|
;; Phase 5
|
|
;; -----------------------------------------------------------------------
|
|
|
|
(~doc-section :title "Phase 5: Client IO Proxy" :id "phase-5"
|
|
|
|
(div :class "rounded border border-green-300 bg-green-50 p-4 mb-4"
|
|
(div :class "flex items-center gap-2 mb-2"
|
|
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete"))
|
|
(p :class "text-green-900 font-medium" "What it enables")
|
|
(p :class "text-green-800" "Components with IO dependencies render client-side. IO primitives are proxied to the server — the client evaluator calls them like normal functions, the proxy fetches results via HTTP, the async DOM renderer awaits the promises and continues."))
|
|
|
|
(~doc-subsection :title "How it works"
|
|
(p "Instead of async-aware continuations (originally planned), Phase 5 was solved by combining three mechanisms that emerged from Phases 3-4:")
|
|
|
|
(div :class "space-y-4"
|
|
(div
|
|
(h4 :class "font-semibold text-stone-700" "1. IO dependency detection (from Phase 2)")
|
|
(p "The component dep analyzer scans AST bodies for IO primitive names (highlight, asset-url, query, frag, etc.) and computes transitive IO refs. Pages include their IO dep list in the page registry."))
|
|
|
|
(div
|
|
(h4 :class "font-semibold text-stone-700" "2. IO proxy registration")
|
|
(p (code "registerIoDeps(names)") " in orchestration.sx registers proxy functions for each IO primitive. When the client evaluator encounters " (code "(highlight code \"sx\")") ", the proxy sends an HTTP request to the server's IO endpoint and returns a Promise."))
|
|
|
|
(div
|
|
(h4 :class "font-semibold text-stone-700" "3. Async DOM renderer")
|
|
(p (code "asyncRenderToDom") " walks the expression tree and handles Promises transparently. When a subexpression returns a Promise (from an IO proxy call), the renderer awaits it and continues building the DOM tree. No continuations needed — JavaScript's native Promise mechanism provides the suspension."))))
|
|
|
|
(~doc-subsection :title "Why continuations weren't needed"
|
|
(p "The original Phase 5 plan called for async-aware shift/reset or a CPS transform of the evaluator. In practice, JavaScript's Promise mechanism provided the same capability: the async DOM renderer naturally suspends when it encounters a Promise and resumes when it resolves.")
|
|
(p "Delimited continuations remain valuable for Phase 6 (streaming/suspense on the " (em "server") " side, where Python doesn't have native Promise-based suspension in the evaluator). But for client-side IO, Promises + async render were sufficient."))
|
|
|
|
(~doc-subsection :title "Files"
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
|
|
(li "shared/sx/ref/orchestration.sx — registerIoDeps, IO proxy registration")
|
|
(li "shared/sx/ref/bootstrap_js.py — asyncRenderToDom, IO proxy HTTP transport")
|
|
(li "shared/sx/helpers.py — io_deps in page registry entries")
|
|
(li "shared/sx/deps.py — transitive IO ref computation")))
|
|
|
|
(~doc-subsection :title "Verification"
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
|
(li "Navigate to any page with IO deps (e.g. /testing/eval) — console shows IO proxy calls")
|
|
(li "Components using " (code "highlight") " render correctly via proxy")
|
|
(li "Pages with " (code "asset-url") " resolve script paths via proxy")
|
|
(li "Async render completes without blocking — partial results appear as promises resolve"))))
|
|
|
|
;; -----------------------------------------------------------------------
|
|
;; Phase 6
|
|
;; -----------------------------------------------------------------------
|
|
|
|
(~doc-section :title "Phase 6: Streaming & Suspense" :id "phase-6"
|
|
|
|
(div :class "rounded border border-green-300 bg-green-50 p-4 mb-4"
|
|
(div :class "flex items-center gap-2 mb-2"
|
|
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
|
|
(a :href "/isomorphism/streaming" :class "text-green-700 underline text-sm font-medium" "Live streaming demo"))
|
|
(p :class "text-green-900 font-medium" "What it enables")
|
|
(p :class "text-green-800" "Server streams partially-evaluated SX as IO resolves. Client renders available subtrees immediately with loading skeletons, fills in suspended parts as data arrives."))
|
|
|
|
(~doc-subsection :title "What was built"
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
|
(li (code "~suspense") " component — renders fallback content with a stable DOM ID, replaced when resolution arrives")
|
|
(li (code "defpage :stream true") " — opts a page into streaming response mode")
|
|
(li (code "defpage :fallback expr") " — custom loading skeleton for streaming pages")
|
|
(li (code "execute_page_streaming()") " — Quart async generator response that yields HTML chunks")
|
|
(li (code "sx_page_streaming_parts()") " — splits the HTML shell into streamable parts")
|
|
(li (code "Sx.resolveSuspense(id, sx)") " — client-side function to replace suspense placeholders")
|
|
(li (code "window.__sxResolve") " bootstrap — queues resolutions that arrive before sx.js loads")
|
|
(li "Concurrent IO: data eval + header eval run in parallel via " (code "asyncio.create_task"))
|
|
(li "Completion-order streaming: whichever IO finishes first gets sent first via " (code "asyncio.wait(FIRST_COMPLETED)"))))
|
|
|
|
(~doc-subsection :title "Architecture"
|
|
|
|
(div :class "space-y-4"
|
|
(div
|
|
(h4 :class "font-semibold text-stone-700" "1. Suspense component")
|
|
(p "When streaming, the server renders the page with " (code "~suspense") " placeholders instead of awaiting IO:")
|
|
(~doc-code :code (highlight "(~app-body\n :header-rows (~suspense :id \"stream-headers\"\n :fallback (div :class \"h-12 bg-stone-200 animate-pulse\"))\n :content (~suspense :id \"stream-content\"\n :fallback (div :class \"p-8 animate-pulse\" ...)))" "lisp")))
|
|
|
|
(div
|
|
(h4 :class "font-semibold text-stone-700" "2. Chunked transfer")
|
|
(p "Quart async generator response yields chunks in order:")
|
|
(ol :class "list-decimal pl-5 text-stone-700 space-y-1"
|
|
(li "HTML shell + CSS + component defs + page registry + suspense page SX + scripts (immediate)")
|
|
(li "Resolution " (code "<script>") " tags as each IO completes")
|
|
(li "Closing " (code "</body></html>"))))
|
|
|
|
(div
|
|
(h4 :class "font-semibold text-stone-700" "3. Client resolution")
|
|
(p "Each resolution chunk is an inline script:")
|
|
(~doc-code :code (highlight "<script>\n window.__sxResolve(\"stream-content\",\n \"(~article :title \\\"Hello\\\")\")\n</script>" "html"))
|
|
(p "The client parses the SX, renders to DOM, and replaces the suspense placeholder's children."))
|
|
|
|
(div
|
|
(h4 :class "font-semibold text-stone-700" "4. Concurrent IO")
|
|
(p "Data evaluation and header construction run in parallel. " (code "asyncio.wait(FIRST_COMPLETED)") " yields resolution chunks in whatever order IO completes — no artificial sequencing."))))
|
|
|
|
(~doc-subsection :title "Continuation foundation"
|
|
(p "Delimited continuations (" (code "reset") "/" (code "shift") ") are implemented in the Python evaluator (async_eval.py lines 586-624) and available as special forms. Phase 6 uses the simpler pattern of concurrent IO + completion-order streaming, but the continuation machinery is in place for Phase 7's more sophisticated evaluation-level suspension."))
|
|
|
|
(~doc-subsection :title "Files"
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
|
|
(li "shared/sx/templates/pages.sx — ~suspense component definition")
|
|
(li "shared/sx/types.py — PageDef.stream, PageDef.fallback_expr fields")
|
|
(li "shared/sx/evaluator.py — defpage :stream/:fallback parsing")
|
|
(li "shared/sx/pages.py — execute_page_streaming(), streaming route mounting")
|
|
(li "shared/sx/helpers.py — sx_page_streaming_parts(), sx_streaming_resolve_script()")
|
|
(li "shared/sx/ref/boot.sx — resolve-suspense spec (canonical)")
|
|
(li "shared/sx/ref/bootstrap_js.py — resolveSuspense on Sx object, __sxPending/Resolve init")
|
|
(li "shared/static/scripts/sx-browser.js — bootstrapped output (DO NOT EDIT)")
|
|
(li "shared/sx/async_eval.py — reset/shift special forms (continuation foundation)")
|
|
(li "sx/sx/streaming-demo.sx — demo content component")
|
|
(li "sx/sxc/pages/docs.sx — streaming-demo defpage")
|
|
(li "sx/sxc/pages/helpers.py — streaming-demo-data page helper")))
|
|
|
|
(~doc-subsection :title "Demonstration"
|
|
(p "The " (a :href "/isomorphism/streaming" :class "text-violet-700 underline" "streaming demo page") " exercises the full pipeline:")
|
|
(ol :class "list-decimal pl-5 text-stone-700 space-y-1"
|
|
(li "Navigate to " (a :href "/isomorphism/streaming" :class "text-violet-700 underline" "/isomorphism/streaming"))
|
|
(li "The page skeleton appears " (strong "instantly") " — animated loading skeletons fill the content area")
|
|
(li "After ~1.5 seconds, the real content replaces the skeletons (streamed from server)")
|
|
(li "Open the Network tab — observe " (code "Transfer-Encoding: chunked") " on the document response")
|
|
(li "The document response shows multiple chunks arriving over time: shell first, then resolution scripts")))
|
|
|
|
(~doc-subsection :title "What to verify"
|
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
|
(li (strong "Instant shell: ") "The page HTML arrives immediately — no waiting for the 1.5s data fetch")
|
|
(li (strong "Suspense placeholders: ") "The " (code "~suspense") " component renders a " (code "data-suspense") " wrapper with animated fallback content")
|
|
(li (strong "Resolution: ") "The " (code "__sxResolve()") " inline script replaces the placeholder with real rendered content")
|
|
(li (strong "Chunked encoding: ") "Network tab shows the document as a chunked response with multiple frames")
|
|
(li (strong "Concurrent IO: ") "Header and content resolve independently — whichever finishes first appears first")
|
|
(li (strong "HTMX fallback: ") "SX/HTMX requests bypass streaming and receive a standard response"))))
|
|
|
|
;; -----------------------------------------------------------------------
|
|
;; Phase 7
|
|
;; -----------------------------------------------------------------------
|
|
|
|
(~doc-section :title "Phase 7: Full Isomorphism" :id "phase-7"
|
|
|
|
(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. Optimistic data updates")
|
|
(p "Extend existing apply-optimistic/revert-optimistic in engine.sx from DOM-level to data-level. Client updates cached data optimistically, sends mutation to server, reverts on rejection."))
|
|
|
|
(div
|
|
(h4 :class "font-semibold text-stone-700" "4. 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" "5. 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" "6. 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 data errors include page name, params, server response status")
|
|
(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"))))))))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; SX CI Pipeline
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(defcomp ~plan-sx-ci-content ()
|
|
(~doc-page :title "SX CI Pipeline"
|
|
|
|
(p :class "text-stone-500 text-sm italic mb-8"
|
|
"Build, test, and deploy Rose Ash using the same language the application is written in.")
|
|
|
|
(~doc-section :title "Context" :id "context"
|
|
(p :class "text-stone-600"
|
|
"Rose Ash currently uses shell scripts for CI: " (code "deploy.sh") " auto-detects changed services via git diff, builds Docker images, pushes to the registry, and restarts Swarm services. " (code "dev.sh") " starts the dev environment and runs tests. These work, but they are opaque imperative scripts with no reuse, no composition, and no relationship to SX.")
|
|
(p :class "text-stone-600"
|
|
"The CI pipeline is the last piece of infrastructure not expressed in s-expressions. Fixing that completes the \"one representation for everything\" claim — the same language that defines the spec, the components, the pages, the essays, and the deployment config also defines the build pipeline."))
|
|
|
|
(~doc-section :title "Design" :id "design"
|
|
(p :class "text-stone-600"
|
|
"Pipeline definitions are " (code ".sx") " files. A minimal Python CLI runner evaluates them using " (code "sx_ref.py") ". CI-specific IO primitives (shell execution, Docker, git) are boundary-declared and only available to the pipeline runner — never to web components.")
|
|
(~doc-code :code (highlight ";; pipeline/deploy.sx\n(let ((targets (if (= (length ARGS) 0)\n (~detect-changed :base \"HEAD~1\")\n (filter (fn (svc) (some (fn (a) (= a (get svc \"name\"))) ARGS))\n services))))\n (when (= (length targets) 0)\n (log-step \"No changes detected\")\n (exit 0))\n\n (log-step (str \"Deploying: \" (join \" \" (map (fn (s) (get s \"name\")) targets))))\n\n ;; Tests first\n (~unit-tests)\n (~sx-spec-tests)\n\n ;; Build, push, restart\n (for-each (fn (svc) (~build-service :service svc)) targets)\n (for-each (fn (svc) (~restart-service :service svc)) targets)\n\n (log-step \"Deploy complete\"))" "lisp"))
|
|
(p :class "text-stone-600"
|
|
"Pipeline steps are components. " (code "~unit-tests") ", " (code "~build-service") ", " (code "~detect-changed") " are " (code "defcomp") " definitions that compose by nesting — the same mechanism used for page layouts, navigation, and every other piece of the system."))
|
|
|
|
(~doc-section :title "CI Primitives" :id "primitives"
|
|
(p :class "text-stone-600"
|
|
"New IO primitives declared in " (code "boundary.sx") ", implemented only in the CI runner context:")
|
|
(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" "Primitive")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Signature")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Purpose")))
|
|
(tbody
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shell-run")
|
|
(td :class "px-3 py-2 font-mono text-xs text-stone-600" "(command) -> dict")
|
|
(td :class "px-3 py-2 text-stone-700" "Execute shell command, return " (code "{:exit N :stdout \"...\" :stderr \"...\"}") ""))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shell-run!")
|
|
(td :class "px-3 py-2 font-mono text-xs text-stone-600" "(command) -> dict")
|
|
(td :class "px-3 py-2 text-stone-700" "Execute shell command, throw on non-zero exit"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "docker-build")
|
|
(td :class "px-3 py-2 font-mono text-xs text-stone-600" "(&key file tag context) -> nil")
|
|
(td :class "px-3 py-2 text-stone-700" "Build Docker image"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "docker-push")
|
|
(td :class "px-3 py-2 font-mono text-xs text-stone-600" "(tag) -> nil")
|
|
(td :class "px-3 py-2 text-stone-700" "Push image to registry"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "docker-restart")
|
|
(td :class "px-3 py-2 font-mono text-xs text-stone-600" "(service) -> nil")
|
|
(td :class "px-3 py-2 text-stone-700" "Restart Swarm service"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "git-diff-files")
|
|
(td :class "px-3 py-2 font-mono text-xs text-stone-600" "(base head) -> list")
|
|
(td :class "px-3 py-2 text-stone-700" "List changed files between commits"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "git-branch")
|
|
(td :class "px-3 py-2 font-mono text-xs text-stone-600" "() -> string")
|
|
(td :class "px-3 py-2 text-stone-700" "Current branch name"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "log-step")
|
|
(td :class "px-3 py-2 font-mono text-xs text-stone-600" "(message) -> nil")
|
|
(td :class "px-3 py-2 text-stone-700" "Formatted pipeline output"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "fail!")
|
|
(td :class "px-3 py-2 font-mono text-xs text-stone-600" "(message) -> nil")
|
|
(td :class "px-3 py-2 text-stone-700" "Abort pipeline with error")))))
|
|
(p :class "text-stone-600"
|
|
"The boundary system ensures these primitives are " (em "only") " available in the CI context. Web components cannot call " (code "shell-run!") " — the evaluator will refuse to resolve the symbol, just as it refuses to resolve any other unregistered IO primitive. The sandbox is structural, not a convention."))
|
|
|
|
(~doc-section :title "Reusable Steps" :id "steps"
|
|
(p :class "text-stone-600"
|
|
"Pipeline steps are components — same " (code "defcomp") " as UI components, same " (code "&key") " params, same composition by nesting:")
|
|
(~doc-code :code (highlight "(defcomp ~detect-changed (&key base)\n (let ((files (git-diff-files (or base \"HEAD~1\") \"HEAD\")))\n (if (some (fn (f) (starts-with? f \"shared/\")) files)\n services\n (filter (fn (svc)\n (some (fn (f) (starts-with? f (str (get svc \"dir\") \"/\"))) files))\n services))))\n\n(defcomp ~build-service (&key service)\n (let ((name (get service \"name\"))\n (tag (str registry \"/\" name \":latest\")))\n (log-step (str \"Building \" name))\n (docker-build :file (str (get service \"dir\") \"/Dockerfile\") :tag tag :context \".\")\n (docker-push tag)))\n\n(defcomp ~bootstrap-check ()\n (log-step \"Checking bootstrapped files are up to date\")\n (shell-run! \"python shared/sx/ref/bootstrap_js.py\")\n (shell-run! \"python shared/sx/ref/bootstrap_py.py\")\n (let ((diff (shell-run \"git diff --name-only shared/static/scripts/sx-ref.js shared/sx/ref/sx_ref.py\")))\n (when (not (= (get diff \"stdout\") \"\"))\n (fail! \"Bootstrapped files are stale — rebootstrap and commit\"))))" "lisp"))
|
|
(p :class "text-stone-600"
|
|
"Compare this to GitHub Actions YAML, where \"reuse\" means composite actions with " (code "uses:") " references, input/output mappings, shell script blocks inside YAML strings, and a " (a :href "https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions" :class "text-violet-600 hover:underline" "100-page syntax reference") ". SX pipeline reuse is function composition. That is all it has ever been."))
|
|
|
|
(~doc-section :title "Pipelines" :id "pipelines"
|
|
(p :class "text-stone-600"
|
|
"Two primary pipelines, each a single " (code ".sx") " file:")
|
|
(div :class "space-y-4"
|
|
(div :class "rounded border border-stone-200 p-4"
|
|
(h4 :class "font-semibold text-stone-700 mb-2" "pipeline/test.sx")
|
|
(p :class "text-sm text-stone-600" "Unit tests, SX spec tests (Python + Node), bootstrap staleness check, Tailwind CSS check. Run locally or in CI.")
|
|
(p :class "text-sm font-mono text-violet-700 mt-1" "python -m shared.sx.ci pipeline/test.sx"))
|
|
(div :class "rounded border border-stone-200 p-4"
|
|
(h4 :class "font-semibold text-stone-700 mb-2" "pipeline/deploy.sx")
|
|
(p :class "text-sm text-stone-600" "Auto-detect changed services (or accept explicit args), run tests, build Docker images, push to registry, restart Swarm services.")
|
|
(p :class "text-sm font-mono text-violet-700 mt-1" "python -m shared.sx.ci pipeline/deploy.sx blog market"))))
|
|
|
|
(~doc-section :title "Why this matters" :id "why"
|
|
(p :class "text-stone-600"
|
|
"CI pipelines are the strongest test case for \"one representation for everything.\" GitHub Actions, GitLab CI, CircleCI — all use YAML. YAML is not a programming language. So every CI system reinvents conditionals (" (code "if:") " expressions evaluated as strings), iteration (" (code "matrix:") " strategies), composition (" (code "uses:") " references with input/output schemas), and error handling (" (code "continue-on-error:") " booleans) — all in a data format that was never designed for any of it.")
|
|
(p :class "text-stone-600"
|
|
"The result is a domain-specific language trapped inside YAML, with worse syntax than any language designed to be one. Every CI pipeline of sufficient complexity becomes a programming task performed in a notation that actively resists programming.")
|
|
(p :class "text-stone-600"
|
|
"SX pipelines use real conditionals, real functions, real composition, and real error handling — because SX is a real language. The pipeline definition and the application code are the same thing. An AI that can generate SX components can generate SX pipelines. A developer who reads SX pages can read SX deploys. The representation is universal."))
|
|
|
|
(~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" "Purpose")))
|
|
(tbody
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ci.py")
|
|
(td :class "px-3 py-2 text-stone-700" "Pipeline runner CLI (~150 lines)"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ci_primitives.py")
|
|
(td :class "px-3 py-2 text-stone-700" "CI IO primitive implementations"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "pipeline/services.sx")
|
|
(td :class "px-3 py-2 text-stone-700" "Service registry (data)"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "pipeline/steps.sx")
|
|
(td :class "px-3 py-2 text-stone-700" "Reusable step components"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "pipeline/test.sx")
|
|
(td :class "px-3 py-2 text-stone-700" "Test pipeline"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "pipeline/deploy.sx")
|
|
(td :class "px-3 py-2 text-stone-700" "Deploy pipeline"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/boundary.sx")
|
|
(td :class "px-3 py-2 text-stone-700" "Add CI primitive declarations"))))))))
|
|
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; CSSX Components
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(defcomp ~plan-cssx-components-content ()
|
|
(~doc-page :title "CSSX Components"
|
|
|
|
(~doc-section :title "Context" :id "context"
|
|
(p "SX currently has a parallel CSS system: a style dictionary (JSON blob of atom-to-declaration mappings), a " (code "StyleValue") " type threaded through the evaluator and renderer, content-addressed hash class names (" (code "sx-a3f2b1") "), runtime CSS injection into " (code "<style id=\"sx-css\">") ", and a separate caching pipeline (" (code "<script type=\"text/sx-styles\">") ", localStorage, cookies).")
|
|
(p "This is ~300 lines of spec code (cssx.sx) plus platform interface (hash, regex, injection), plus server-side infrastructure (css_registry.py, tw.css parsing). All to solve one problem: " (em "resolving keyword atoms like ") (code ":flex :gap-4 :hover:bg-sky-200") (em " into CSS at render time."))
|
|
(p "The result: elements in the DOM get opaque class names like " (code "class=\"sx-a3f2b1\"") ". DevTools becomes useless. You can't inspect an element and understand its styling. " (strong "This is a deal breaker.")))
|
|
|
|
(~doc-section :title "The Idea" :id "idea"
|
|
(p (strong "Styling is just components.") " A CSSX component is a regular " (code "defcomp") " that decides how to style its children. It might apply Tailwind classes, or hand-written CSS classes, or inline styles, or generate rules at runtime. The implementation is the component's private business. The consumer just calls " (code "(~btn :variant \"primary\" \"Submit\")") " and doesn't care.")
|
|
(p "Because it's " (code "defcomp") ", you get everything for free: caching, bundling, dependency scanning, server/client rendering, composition. No parallel infrastructure.")
|
|
(p "Key advantages:")
|
|
(ul :class "list-disc pl-5 space-y-1 text-stone-700"
|
|
(li (strong "Readable DOM: ") "Elements have real class names, not content-addressed hashes. DevTools works.")
|
|
(li (strong "Data-driven styling: ") "Components receive data and decide styling. " (code "(~metric :value 150)") " renders red because " (code "value > 100") " — logic lives in the component, not a CSS preprocessor.")
|
|
(li (strong "One system: ") "No separate " (code "StyleValue") " type, no style dictionary JSON, no " (code "<script type=\"text/sx-styles\">") ", no " (code "sx-css") " injection. Components ARE the styling abstraction.")
|
|
(li (strong "One cache: ") "Component hash/localStorage handles everything. No separate style dict caching.")
|
|
(li (strong "Composable: ") (code "(~card :elevated true (~metric :value v))") " — styling composes like any other component.")
|
|
(li (strong "Strategy-agnostic: ") "A component can apply Tailwind classes, emit " (code "<style>") " blocks, use inline styles, generate CSS custom properties, or any combination. The consumer never knows or cares. Swap strategies without touching call sites.")))
|
|
|
|
(~doc-section :title "Examples" :id "examples"
|
|
(~doc-subsection :title "Simple class mapping"
|
|
(p "A button component that maps variant keywords to class strings:")
|
|
(highlight
|
|
"(defcomp ~btn (&key variant disabled &rest children)\n (button\n :class (str \"px-4 py-2 rounded font-medium transition \"\n (case variant\n \"primary\" \"bg-blue-600 text-white hover:bg-blue-700\"\n \"danger\" \"bg-red-600 text-white hover:bg-red-700\"\n \"ghost\" \"bg-transparent hover:bg-stone-100\"\n \"bg-stone-200 hover:bg-stone-300\")\n (when disabled \" opacity-50 cursor-not-allowed\"))\n :disabled disabled\n children))"
|
|
"lisp"))
|
|
|
|
(~doc-subsection :title "Data-driven styling"
|
|
(p "Styling that responds to data values — impossible with static CSS:")
|
|
(highlight
|
|
"(defcomp ~metric (&key value label threshold)\n (let ((t (or threshold 10)))\n (div :class (str \"p-3 rounded font-bold \"\n (cond\n ((> value (* t 10)) \"bg-red-500 text-white\")\n ((> value t) \"bg-amber-200 text-amber-900\")\n (:else \"bg-green-100 text-green-800\")))\n (span :class \"text-sm\" label) \": \" (span (str value)))))"
|
|
"lisp"))
|
|
|
|
(~doc-subsection :title "Style functions"
|
|
(p "Reusable style logic without wrapping — returns class strings:")
|
|
(highlight
|
|
"(define card-classes\n (fn (&key elevated bordered)\n (str \"rounded-lg p-4 \"\n (if elevated \"shadow-lg\" \"shadow-sm\")\n (when bordered \" border border-stone-200\"))))\n\n;; Usage: (div :class (card-classes :elevated true) ...)"
|
|
"lisp"))
|
|
|
|
(~doc-subsection :title "Responsive and interactive"
|
|
(p "Components can encode responsive breakpoints and interactive states as class strings — the same way you'd write Tailwind, but wrapped in a semantic component:")
|
|
(highlight
|
|
"(defcomp ~responsive-grid (&key cols &rest children)\n (div :class (str \"grid gap-4 \"\n (case (or cols 3)\n 1 \"grid-cols-1\"\n 2 \"grid-cols-1 md:grid-cols-2\"\n 3 \"grid-cols-1 md:grid-cols-2 lg:grid-cols-3\"\n 4 \"grid-cols-2 md:grid-cols-3 lg:grid-cols-4\"))\n children))"
|
|
"lisp"))
|
|
|
|
(~doc-subsection :title "Emitting CSS directly"
|
|
(p "Components are not limited to referencing existing classes. They can generate CSS — " (code "<style>") " tags, keyframes, custom properties — as part of their output:")
|
|
(highlight
|
|
"(defcomp ~pulse (&key color duration &rest children)\n (<>\n (style (str \"@keyframes sx-pulse {\"\n \"0%,100% { opacity:1 } 50% { opacity:.5 } }\"))\n (div :style (str \"animation: sx-pulse \" (or duration \"2s\") \" infinite;\"\n \"color:\" (or color \"inherit\"))\n children)))\n\n(defcomp ~theme (&key primary surface &rest children)\n (<>\n (style (str \":root {\"\n \"--color-primary:\" (or primary \"#7c3aed\") \";\"\n \"--color-surface:\" (or surface \"#fafaf9\") \"}\"))\n children))"
|
|
"lisp")
|
|
(p "The CSS strategy is the component's private implementation detail. Consumers call " (code "(~pulse :color \"red\" \"Loading...\")") " or " (code "(~theme :primary \"#2563eb\" ...)") " without knowing or caring whether the component uses classes, inline styles, generated rules, or all three.")))
|
|
|
|
(~doc-section :title "What Changes" :id "changes"
|
|
|
|
(~doc-subsection :title "Remove"
|
|
(ul :class "list-disc pl-5 space-y-1 text-stone-700"
|
|
(li (code "StyleValue") " type and all plumbing (type checks in eval, render, serialize)")
|
|
(li (code "cssx.sx") " spec module (~300 lines: resolve-style, resolve-atom, split-variant, hash, injection)")
|
|
(li "Style dictionary JSON format, loading, caching (" (code "<script type=\"text/sx-styles\">") ", " (code "initStyleDict") ", " (code "parseAndLoadStyleDict") ")")
|
|
(li (code "<style id=\"sx-css\">") " runtime CSS injection system")
|
|
(li (code "css_registry.py") " server-side (builds style dictionary from tw.css)")
|
|
(li "Style dict cookies (" (code "sx-styles-hash") "), localStorage keys (" (code "sx-styles-src") ")")
|
|
(li "Platform interface: " (code "fnv1a-hash") ", " (code "compile-regex") ", " (code "regex-match") ", " (code "regex-replace-groups") ", " (code "make-style-value") ", " (code "inject-style-value"))))
|
|
|
|
(~doc-subsection :title "Keep"
|
|
(ul :class "list-disc pl-5 space-y-1 text-stone-700"
|
|
(li (code "defstyle") " — already just " (code "(defstyle name expr)") " which binds name to a value. Stays as sugar for defining reusable style values/functions. No " (code "StyleValue") " type needed — the value can be a string, a function, anything.")
|
|
(li (code "defkeyframes") " — could stay if we want declarative keyframe definitions. Or could become a component/function too.")
|
|
(li (code "tw.css") " — the compiled Tailwind stylesheet. Components reference its classes directly. No runtime resolution needed.")
|
|
(li (code ":class") " attribute — just takes strings now, no " (code "StyleValue") " special-casing.")))
|
|
|
|
(~doc-subsection :title "Add"
|
|
(p "Nothing new to the spec. CSSX components are just " (code "defcomp") ". The only new thing is a convention: components whose primary purpose is styling. They live in the same component files, cache the same way, bundle the same way.")))
|
|
|
|
(~doc-section :title "Migration" :id "migration"
|
|
(p "The existing codebase uses " (code ":class") " with plain Tailwind strings everywhere already. The CSSX style dictionary was an alternative path that was never widely adopted. Migration is mostly deletion:")
|
|
(ol :class "list-decimal pl-5 space-y-2 text-stone-700"
|
|
(li "Remove " (code "StyleValue") " type from " (code "types.py") ", " (code "render.sx") ", " (code "eval.sx") ", bootstrappers")
|
|
(li "Remove " (code "cssx.sx") " from spec modules and bootstrapper")
|
|
(li "Remove " (code "css_registry.py") " and style dict generation pipeline")
|
|
(li "Remove style dict loading from " (code "boot.sx") " (" (code "initStyleDict") ", " (code "queryStyleScripts") ")")
|
|
(li "Remove style-related cookies and localStorage from " (code "boot.sx") " platform interface")
|
|
(li "Remove " (code "StyleValue") " special-casing from " (code "render-attrs") " in " (code "render.sx") " and DOM adapter")
|
|
(li "Simplify " (code ":class") " / " (code ":style") " attribute handling — just strings")
|
|
(li "Convert any existing " (code "defstyle") " uses to return plain class strings instead of " (code "StyleValue") " objects"))
|
|
(p :class "mt-4 text-stone-600 italic" "Net effect: hundreds of lines of spec and infrastructure removed, zero new lines added. The component system already does everything CSSX was trying to do."))
|
|
|
|
(~doc-section :title "Relationship to Other Plans" :id "relationships"
|
|
(ul :class "list-disc pl-5 space-y-1 text-stone-700"
|
|
(li (strong "Content-Addressed Components: ") "CSSX components get CIDs like any other component. A " (code "~btn") " from one site can be shared to another via IPFS. No " (code ":css-atoms") " manifest field needed — the component carries its own styling logic.")
|
|
(li (strong "Isomorphic Rendering: ") "Components render the same on server and client. No style injection timing issues, no FOUC from late CSS loading.")
|
|
(li (strong "Component Bundling: ") "deps.sx already handles transitive component deps. Style components are just more components in the bundle — no separate style bundling.")))
|
|
|
|
(~doc-section :title "Comparison with CSS Technologies" :id "comparison"
|
|
(p "CSSX components share DNA with several existing approaches but avoid the problems that make each one painful at scale.")
|
|
|
|
(~doc-subsection :title "styled-components / Emotion"
|
|
(p (a :href "https://styled-components.com" :class "text-violet-600 hover:underline" "styled-components") " pioneered the idea that styling belongs in components. But it generates CSS at runtime, injects " (code "<style>") " tags, and produces opaque hashed class names (" (code "class=\"sc-bdfBwQ fNMpVx\"") "). Open DevTools and you see gibberish. It also carries significant runtime cost — parsing CSS template literals, hashing, deduplicating — and needs a separate SSR extraction step (" (code "ServerStyleSheet") ").")
|
|
(p "CSSX components share the core insight (" (em "styling is a component concern") ") but without the runtime machinery. When a component applies Tailwind classes, there's zero CSS generation overhead. When it does emit " (code "<style>") " blocks, it's explicit — not hidden behind a tagged template literal. And the DOM is always readable."))
|
|
|
|
(~doc-subsection :title "CSS Modules"
|
|
(p (a :href "https://github.com/css-modules/css-modules" :class "text-violet-600 hover:underline" "CSS Modules") " scope class names to avoid collisions by rewriting them at build time: " (code ".button") " becomes " (code ".button_abc123") ". This solves the global namespace problem but creates the same opacity issue — hashed names in the DOM that you can't grep for or reason about.")
|
|
(p "CSSX components don't need scoping because component boundaries already provide isolation. A " (code "~btn") " owns its markup. There's nothing to collide with."))
|
|
|
|
(~doc-subsection :title "Tailwind CSS"
|
|
(p "Tailwind is " (em "complementary") ", not competitive. CSSX components are the semantic layer on top. Raw Tailwind in markup — " (code ":class \"px-4 py-2 bg-blue-600 text-white font-medium rounded hover:bg-blue-700\"") " — is powerful but verbose and duplicated across call sites.")
|
|
(p "A CSSX component wraps that string once: " (code "(~btn :variant \"primary\" \"Submit\")") ". The Tailwind classes are still there, readable in DevTools, but consumers don't repeat them. This is the same pattern Tailwind's own docs recommend (" (em "\"extracting components\"") ") — CSSX components are just SX's native way of doing it."))
|
|
|
|
(~doc-subsection :title "Vanilla Extract"
|
|
(p (a :href "https://vanilla-extract.style" :class "text-violet-600 hover:underline" "Vanilla Extract") " is zero-runtime CSS-in-JS: styles are written in TypeScript, compiled to static CSS at build time, and referenced by generated class names. It avoids the runtime cost of styled-components but still requires a build step, a bundler plugin, and TypeScript. The generated class names are again opaque.")
|
|
(p "CSSX components need no build step for styling — they're evaluated at render time like any other component. And since the component chooses its own strategy, it can reference pre-built classes (zero runtime) " (em "or") " generate CSS on the fly — same API either way."))
|
|
|
|
(~doc-subsection :title "Design Tokens / Style Dictionary"
|
|
(p "The " (a :href "https://amzn.github.io/style-dictionary/" :class "text-violet-600 hover:underline" "Style Dictionary") " pattern — a JSON/YAML file mapping token names to values, compiled to platform-specific output — is essentially what the old CSSX was. It's the industry standard for design systems.")
|
|
(p "The problem is that it's a parallel system: separate file format, separate build pipeline, separate caching, separate tooling. CSSX components eliminate all of that by expressing tokens as component parameters: " (code "(~theme :primary \"#7c3aed\")") " instead of " (code "{\"color\": {\"primary\": {\"value\": \"#7c3aed\"}}}") ". Same result, no parallel infrastructure.")))
|
|
|
|
(~doc-section :title "Philosophy" :id "philosophy"
|
|
(p "The web has spent two decades building increasingly complex CSS tooling: preprocessors, CSS-in-JS, atomic CSS, utility frameworks, design tokens, style dictionaries. Each solves a real problem but adds a new system with its own caching, bundling, and mental model.")
|
|
(p "CSSX components collapse all of this back to the simplest possible thing: " (strong "a function that takes data and returns markup with classes.") " That's what a component already is. There is no separate styling system because there doesn't need to be."))))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Live Streaming — SSE & WebSocket
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(defcomp ~plan-live-streaming-content ()
|
|
(~doc-page :title "Live Streaming"
|
|
|
|
(~doc-section :title "Context" :id "context"
|
|
(p "SX streaming currently uses chunked transfer encoding: the server sends an HTML shell with "
|
|
(code "~suspense") " placeholders, then resolves each one via inline "
|
|
(code "<script>__sxResolve(id, sx)</script>") " chunks as IO completes. "
|
|
"Once the response finishes, the connection closes. Each slot resolves exactly once.")
|
|
(p "This is powerful for initial page load but doesn't support live updates "
|
|
"— dashboard metrics, chat messages, collaborative editing, real-time notifications. "
|
|
"For that we need a persistent transport: " (strong "SSE") " (Server-Sent Events) or " (strong "WebSockets") ".")
|
|
(p "The key insight: the client already has " (code "Sx.resolveSuspense(id, sxSource)") " which replaces "
|
|
"DOM content by suspense ID. A persistent connection just needs to keep calling it."))
|
|
|
|
(~doc-section :title "Design" :id "design"
|
|
|
|
(~doc-subsection :title "Transport Hierarchy"
|
|
(p "Three tiers, progressively more capable:")
|
|
(ol :class "list-decimal list-inside space-y-2 text-stone-700 text-sm"
|
|
(li (strong "Chunked streaming") " (done) — single HTTP response, each suspense resolves once. "
|
|
"Best for: initial page load with slow IO.")
|
|
(li (strong "SSE") " — persistent one-way connection, server pushes resolve events. "
|
|
"Best for: dashboards, notifications, progress bars, any read-only live data.")
|
|
(li (strong "WebSocket") " — bidirectional, client can send events back. "
|
|
"Best for: chat, collaborative editing, interactive applications.")))
|
|
|
|
(~doc-subsection :title "SSE Protocol"
|
|
(p "A " (code "~live") " component declares a persistent connection to an SSE endpoint:")
|
|
(~doc-code :code (highlight "(~live :src \"/api/stream/dashboard\"\n (~suspense :id \"cpu\" :fallback (span \"Loading...\"))\n (~suspense :id \"memory\" :fallback (span \"Loading...\"))\n (~suspense :id \"requests\" :fallback (span \"Loading...\")))" "lisp"))
|
|
(p "The server SSE endpoint yields SX resolve events:")
|
|
(~doc-code :code (highlight "async def dashboard_stream():\n while True:\n stats = await get_system_stats()\n yield sx_sse_event(\"cpu\", f'(~stat-badge :value \"{stats.cpu}%\")')\n yield sx_sse_event(\"memory\", f'(~stat-badge :value \"{stats.mem}%\")')\n await asyncio.sleep(1)" "python"))
|
|
(p "SSE wire format — each event is a suspense resolve:")
|
|
(~doc-code :code (highlight "event: sx-resolve\ndata: {\"id\": \"cpu\", \"sx\": \"(~stat-badge :value \\\"42%\\\")\"}\n\nevent: sx-resolve\ndata: {\"id\": \"memory\", \"sx\": \"(~stat-badge :value \\\"68%\\\")\"}" "text")))
|
|
|
|
(~doc-subsection :title "WebSocket Protocol"
|
|
(p "A " (code "~ws") " component establishes a bidirectional channel:")
|
|
(~doc-code :code (highlight "(~ws :src \"/ws/chat\"\n :on-message handle-chat-message\n (~suspense :id \"messages\" :fallback (div \"Connecting...\"))\n (~suspense :id \"typing\" :fallback (span)))" "lisp"))
|
|
(p "Client can send SX expressions back:")
|
|
(~doc-code :code (highlight ";; Client sends:\n(sx-send ws-conn '(chat-message :text \"hello\" :user \"alice\"))\n\n;; Server receives, broadcasts to all connected clients:\n;; event: sx-resolve for \"messages\" suspense" "lisp")))
|
|
|
|
(~doc-subsection :title "Shared Resolution Mechanism"
|
|
(p "All three transports use the same client-side resolution:")
|
|
(ul :class "list-disc list-inside space-y-1 text-stone-600 text-sm"
|
|
(li (code "Sx.resolveSuspense(id, sxSource)") " — already exists, parses SX and renders to DOM")
|
|
(li "SSE: " (code "EventSource") " → " (code "onmessage") " → " (code "resolveSuspense()"))
|
|
(li "WS: " (code "WebSocket") " → " (code "onmessage") " → " (code "resolveSuspense()"))
|
|
(li "The component env (defs needed for rendering) can be sent once on connection open")
|
|
(li "Subsequent events only need the SX expression — lightweight wire format"))))
|
|
|
|
(~doc-section :title "Implementation" :id "implementation"
|
|
|
|
(~doc-subsection :title "Phase 1: SSE Infrastructure"
|
|
(ol :class "list-decimal list-inside space-y-2 text-stone-700 text-sm"
|
|
(li "Add " (code "~live") " component to " (code "shared/sx/templates/") " — renders child suspense placeholders, "
|
|
"emits " (code "data-sx-live") " attribute with SSE endpoint URL")
|
|
(li "Add " (code "sx-live.js") " client module — on boot, finds " (code "[data-sx-live]") " elements, "
|
|
"opens EventSource, routes events to " (code "resolveSuspense()"))
|
|
(li "Add " (code "sx_sse_event(id, sx)") " helper for Python SSE endpoints — formats SSE wire protocol")
|
|
(li "Add " (code "sse_stream()") " Quart helper — returns async generator Response with correct headers")))
|
|
|
|
(~doc-subsection :title "Phase 2: Defpage Integration"
|
|
(ol :class "list-decimal list-inside space-y-2 text-stone-700 text-sm"
|
|
(li "New " (code ":live") " defpage slot — declares SSE endpoint + suspense bindings")
|
|
(li "Auto-mount SSE endpoint alongside the page route")
|
|
(li "Component defs sent as first SSE event on connection open")
|
|
(li "Automatic reconnection with exponential backoff")))
|
|
|
|
(~doc-subsection :title "Phase 3: WebSocket"
|
|
(ol :class "list-decimal list-inside space-y-2 text-stone-700 text-sm"
|
|
(li "Add " (code "~ws") " component — bidirectional channel with send/receive")
|
|
(li "Add " (code "sx-ws.js") " client module — WebSocket management, message routing")
|
|
(li "Server-side: Quart WebSocket handlers that receive and broadcast SX events")
|
|
(li "Client-side: " (code "sx-send") " primitive for sending SX expressions to server")))
|
|
|
|
(~doc-subsection :title "Phase 4: Spec & Boundary"
|
|
(ol :class "list-decimal list-inside space-y-2 text-stone-700 text-sm"
|
|
(li "Spec " (code "~live") " and " (code "~ws") " in " (code "render.sx") " (how they render in each mode)")
|
|
(li "Add SSE/WS IO primitives to " (code "boundary.sx"))
|
|
(li "Bootstrap SSE/WS connection management into " (code "sx-ref.js"))
|
|
(li "Spec-level tests for resolve, reconnection, and message routing"))))
|
|
|
|
(~doc-section :title "Files" :id "files"
|
|
(table :class "w-full text-left border-collapse"
|
|
(thead
|
|
(tr :class "border-b border-stone-200"
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "File")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Purpose")))
|
|
(tbody
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/templates/live.sx")
|
|
(td :class "px-3 py-2 text-stone-700" "~live component definition"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/static/scripts/sx-live.js")
|
|
(td :class "px-3 py-2 text-stone-700" "SSE client — EventSource → resolveSuspense"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/sse.py")
|
|
(td :class "px-3 py-2 text-stone-700" "SSE helpers — event formatting, stream response"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/static/scripts/sx-ws.js")
|
|
(td :class "px-3 py-2 text-stone-700" "WebSocket client — bidirectional SX channel"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/render.sx")
|
|
(td :class "px-3 py-2 text-stone-700" "Spec: ~live and ~ws rendering in all modes"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/boundary.sx")
|
|
(td :class "px-3 py-2 text-stone-700" "SSE/WS IO primitive declarations")))))))
|
|
|