Add reactive runtime demos + sx-web federation plan

7 interactive island demos for the reactive runtime layers:
- L0 Ref (timer + DOM handle), L1 Foreign (canvas via host-call),
  L2 Machine (traffic light), L3 Commands (undo/redo),
  L4 Loop (bouncing ball), L5 Keyed Lists, L6 App Shell

Fix OCaml build: add (wrapped false) to lib/dune, remove Sx. qualifiers.
Fix JS build: include dom-lib + browser-lib in adapter compilation.

New plan: sx-web federated component web — browser nodes via WebTransport,
server nodes via IPFS, in-browser authoring, AI composition layer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 00:54:23 +00:00
parent 6f96452f70
commit fea44f9fcc
7 changed files with 723 additions and 13 deletions

337
sx/sx/plans/sx-web.sx Normal file
View File

@@ -0,0 +1,337 @@
;; sx-web: Federated Component Web
;; Browser-native peer network with content-addressed SX components.
;; Builds on sx-pub (phases 1-4 complete: publishing, federation, anchoring, IPFS).
(defcomp ~plans/sx-web/plan-sx-web-content ()
(~docs/page :title "sx-web: Federated Component Web"
(p :class "text-stone-500 text-sm italic mb-8"
"A two-tier peer network where server nodes pin to IPFS and browser tabs "
"participate via WebTransport. Components, tests, and content are all "
"content-addressed SX expressions verified by CID regardless of source. "
"Builds on sx-pub phases 14 (publishing, federation, anchoring, IPFS).")
;; =====================================================================
;; Architecture
;; =====================================================================
(~docs/section :title "Architecture" :id "architecture"
(p "Two tiers, one logical network. Trust anchored on CIDs, not sources.")
(table :class "w-full mb-6 text-sm"
(thead
(tr (th :class "text-left p-2" "") (th :class "text-left p-2" "Server Node") (th :class "text-left p-2" "Browser Node")))
(tbody
(tr (td :class "p-2 font-medium" "Runtime") (td :class "p-2" "OCaml + Python") (td :class "p-2" "JavaScript (sx-browser.js)"))
(tr (td :class "p-2 font-medium" "Storage") (td :class "p-2" "IPFS daemon + PostgreSQL") (td :class "p-2" "IndexedDB + Cache API"))
(tr (td :class "p-2 font-medium" "IPFS") (td :class "p-2" "Full DHT peer, pins content") (td :class "p-2" "Verifies CIDs, caches locally"))
(tr (td :class "p-2 font-medium" "Transport") (td :class "p-2" "HTTP/3 + WebTransport server") (td :class "p-2" "WebTransport client"))
(tr (td :class "p-2 font-medium" "Availability") (td :class "p-2" "Long-lived, always on") (td :class "p-2" "Ephemeral, online when open"))
(tr (td :class "p-2 font-medium" "Trust") (td :class "p-2" "CID verification + blockchain anchor") (td :class "p-2" "CID verification (same guarantee)"))))
(p "A component fetched from IPFS, from a server, from another browser tab, "
"or from local IndexedDB cache is verified the same way: hash it, check the CID. "
"The transport is irrelevant to the trust model."))
;; =====================================================================
;; Phase 1: Browser Node Foundation
;; =====================================================================
(~docs/section :title "Phase 1: Browser Node Foundation" :id "phase-1"
(p :class "text-stone-600 italic mb-4"
"Make the browser a first-class participant, not just a viewer.")
(~docs/subsection :title "1a. Content Store (IndexedDB)"
(p "Browser-local content-addressed store. Same interface as server-side "
(code "content-put") "/" (code "content-get") " but backed by IndexedDB.")
(~docs/code :src (highlight
";; Browser content store — pure SX, platform provides IndexedDB ops\n(define browser-store (make-content-store\n {:backend :indexeddb\n :db-name \"sx-web\"\n :verify true})) ;; always verify CID on read\n\n;; Same API as server store\n(content-put browser-store sx-source) ;; → CID\n(content-get browser-store cid) ;; → SX source | nil\n(content-has? browser-store cid) ;; → bool\n(content-pin browser-store cid) ;; mark as persistent\n(content-unpin browser-store cid) ;; allow eviction"
"lisp"))
(p "Two IndexedDB object stores:")
(ul :class "list-disc pl-6 mb-4 space-y-1"
(li (strong "blobs") " — CID → SX source bytes (content-addressed, immutable)")
(li (strong "pins") " — CID → {pinned, last-accessed, size, source-node} (eviction metadata)"))
(p "Unpinned content evicted LRU when storage exceeds quota. "
"Pinned content survives until explicitly unpinned."))
(~docs/subsection :title "1b. Service Worker"
(p "Intercepts fetch requests to resolve CIDs locally before hitting the network.")
(~docs/code :src (highlight
";; Service Worker routes\n;;\n;; /sx-web/cid/<cid> → check IndexedDB first, then fetch from home node\n;; /sx-web/resolve/<path> → resolve path→CID via local index, then fetch\n;; /sx-web/eval/<cid> → fetch + evaluate, return rendered result\n;;\n;; Cache strategy:\n;; CID routes are immutable — cache forever once verified\n;; Path routes check freshness against home node"
"text"))
(p "The Service Worker makes the browser work offline for all pinned components. "
"Online, it deduplicates fetches across tabs — one IndexedDB, shared cache."))
(~docs/subsection :title "1c. CID Verification in JavaScript"
(p "SHA3-256 hashing in the browser for CID verification. "
"Uses SubtleCrypto API (hardware-accelerated on modern browsers).")
(~docs/code :src (highlight
";; New platform primitive for browser host\n(define content-hash\n (fn (source)\n (host-call (host-get (host-global \"crypto\") \"subtle\")\n \"digest\" \"SHA-256\" (encode-utf8 source))))\n\n;; Verification: hash content, compare to claimed CID\n(define verify-cid\n (fn (cid source)\n (= cid (content-hash source))))"
"lisp"))
(p "One new platform primitive: " (code "content-hash") ". "
"Everything else (verification, pinning, eviction) is pure SX."))
(~docs/subsection :title "1d. Deliverables"
(ul :class "list-disc pl-6 mb-4 space-y-1"
(li (code "web/lib/content-store.sx") " — browser content store (~120 LOC)")
(li (code "web/lib/sw-routes.sx") " — Service Worker CID routing (~80 LOC)")
(li "Platform primitive: " (code "content-hash") " in JS host (~15 LOC)")
(li "IndexedDB schema: blobs + pins stores"))))
;; =====================================================================
;; Phase 2: WebTransport Peer Layer
;; =====================================================================
(~docs/section :title "Phase 2: WebTransport Peer Layer" :id "phase-2"
(p :class "text-stone-600 italic mb-4"
"Bidirectional streams between browsers and servers. The protocol is SX.")
(~docs/subsection :title "2a. SX Protocol Definition"
(p "The protocol is an SX component. Nodes fetch, evaluate, and speak it.")
(~docs/code :src (highlight
"(defprotocol sx-sync \"0.1\"\n :messages {\n ;; Content exchange\n :have (fn (cids) ...) ;; I have these CIDs\n :want (fn (cids) ...) ;; I need these CIDs\n :provide (fn (cid source) ...) ;; Here's the content\n :reject (fn (cid reason) ...) ;; Can't provide\n\n ;; Discovery\n :announce (fn (cid metadata) ...) ;; New content published\n :query (fn (pattern) ...) ;; Search for components\n :results (fn (matches) ...) ;; Search results\n\n ;; Identity\n :hello (fn (node-id pubkey) ...) ;; Handshake\n :verify (fn (challenge sig) ...) ;; Prove identity\n }\n\n :flow {\n ;; Content resolution\n :want → :provide | :reject\n ;; Publishing\n :announce → :want (from interested peers)\n ;; Connection\n :hello → :verify → ready\n })"
"lisp"))
(p "The protocol definition is itself content-addressed. Nodes pin the protocol "
"version they speak. Protocol upgrades are published to sx-pub like any component."))
(~docs/subsection :title "2b. WebTransport Server (Server Nodes)"
(p "HTTP/3 WebTransport endpoint on server nodes. "
"Browsers connect and exchange SX messages over bidirectional streams.")
(~docs/code :src (highlight
";; Server-side: WebTransport handler\n;;\n;; One stream per conversation:\n;; Browser → Server: (want (list \"Qm..abc\" \"Qm..def\"))\n;; Server → Browser: (provide \"Qm..abc\" \"(defcomp ...)\")\n;; Server → Browser: (provide \"Qm..def\" \"(defisland ...)\")\n;;\n;; Push notifications:\n;; Server → Browser: (announce \"Qm..xyz\" {:author \"...\" :type :component})\n;;\n;; Peer introduction:\n;; Server → Browser: (peers (list {:id \"...\" :transport \"wt://...\"}))"
"text"))
(p "Server nodes also relay between browsers that can't connect directly. "
"A browser publishes through its home node, which delivers to followers "
"over existing sx-pub federation " (em "and") " pushes to connected browser peers."))
(~docs/subsection :title "2c. WebTransport Client (Browser Nodes)"
(p "Browser connects to home node on page load. Receives push updates. "
"Can also connect to peer nodes for direct exchange.")
(~docs/code :src (highlight
";; Browser-side: WebTransport client\n(define sx-peer\n (fn (url)\n (let ((wt (host-call (host-global \"WebTransport\") \"new\" url))\n (ready (host-get wt \"ready\")))\n (promise-then ready\n (fn ()\n ;; Send hello with identity\n (peer-send wt (sx-hello (node-id) (node-pubkey)))\n ;; Listen for incoming messages\n (peer-listen wt (fn (msg)\n (dispatch-protocol msg))))))))\n\n;; Connect to home node on boot\n(sx-peer (str \"https://\" home-node \"/sx-web/wt\"))"
"lisp"))
(p "Three new platform primitives for the browser host:")
(ul :class "list-disc pl-6 mb-4 space-y-1"
(li (code "wt-connect") " — open WebTransport session")
(li (code "wt-send") " — send SX expression on stream")
(li (code "wt-listen") " — register handler for incoming messages")))
(~docs/subsection :title "2d. Deliverables"
(ul :class "list-disc pl-6 mb-4 space-y-1"
(li (code "web/lib/sx-sync.sx") " — protocol definition (~200 LOC)")
(li (code "web/lib/peer.sx") " — browser peer client (~150 LOC)")
(li "Server: WebTransport endpoint in Quart/Hypercorn (~200 LOC Python)")
(li "Platform primitives: " (code "wt-connect, wt-send, wt-listen") " (~50 LOC JS)")
(li "Peer relay logic on server nodes (~100 LOC Python)"))))
;; =====================================================================
;; Phase 3: In-Browser Authoring
;; =====================================================================
(~docs/section :title "Phase 3: In-Browser Authoring" :id "phase-3"
(p :class "text-stone-600 italic mb-4"
"Edit, preview, test, and publish from a browser tab. No local dev environment.")
(~docs/subsection :title "3a. Live Editor"
(p "SX editor component with real-time preview. "
"Edit source on the left, rendered output on the right. "
"The editor is itself an SX island.")
(~docs/code :src (highlight
"(defisland ~sx-web/editor (&key initial-source)\n (let ((source (signal (or initial-source \"\")))\n (parsed (computed (fn () (try-parse (deref source)))))\n (preview (computed (fn () (try-render (deref parsed))))))\n (div :class \"flex h-full\"\n ;; Source editor (textarea or CodeMirror via foreign FFI)\n (div :class \"flex-1\"\n (textarea :bind source :class \"font-mono ...\"))\n ;; Live preview\n (div :class \"flex-1 border-l\"\n (if (get (deref preview) \"error\")\n (div :class \"text-red-600\" (get (deref preview) \"error\"))\n (div (get (deref preview) \"result\")))))))"
"lisp"))
(p "The evaluator runs client-side. No server round-trip for preview. "
"Syntax errors, type errors, and render errors show inline as you type."))
(~docs/subsection :title "3b. In-Browser Test Runner"
(p "Tests are SX expressions. The browser evaluator runs them directly.")
(~docs/code :src (highlight
";; Tests travel with components\n(deftest ~my-component/tests\n (test \"renders title\"\n (let ((html (render-to-html (~my-component :title \"Hello\"))))\n (assert-contains html \"Hello\")))\n\n (test \"handles empty props\"\n (let ((html (render-to-html (~my-component))))\n (assert-contains html \"Untitled\"))))\n\n;; Run in browser — same evaluator, same results\n(run-tests ~my-component/tests)\n;; => {:passed 2 :failed 0 :errors ()}"
"lisp"))
(p "Component + tests + docs are published as a single unit to sx-pub. "
"Any node can re-run the tests to verify the component works."))
(~docs/subsection :title "3c. Publish Flow"
(p "From browser, one action: edit → test → publish → federate.")
(~docs/code :src (highlight
";; Browser publish flow\n(define publish!\n (fn (source)\n (let ((cid (content-hash source))\n (tests (extract-tests source))\n (results (run-tests tests)))\n (when (all-passed? results)\n ;; Pin locally\n (content-pin browser-store cid)\n ;; Push to home node via WebTransport\n (peer-send home-peer\n (sx-publish cid source\n {:tests results\n :requires (extract-requires source)}))\n ;; Home node pins to IPFS + anchors + federates\n cid))))"
"lisp"))
(p "The browser handles editing, testing, hashing, and local pinning. "
"The home server handles IPFS pinning, blockchain anchoring, and federation delivery. "
"Clean split: browsers create, servers distribute."))
(~docs/subsection :title "3d. Deliverables"
(ul :class "list-disc pl-6 mb-4 space-y-1"
(li (code "web/lib/editor.sx") " — live editor island (~300 LOC)")
(li (code "web/lib/test-runner.sx") " — browser test runner (~150 LOC)")
(li (code "web/lib/publish.sx") " — publish flow (~100 LOC)")
(li "Test extraction from SX source (find deftest forms)"))))
;; =====================================================================
;; Phase 4: Component Discovery & Resolution
;; =====================================================================
(~docs/section :title "Phase 4: Component Discovery & Resolution" :id "phase-4"
(p :class "text-stone-600 italic mb-4"
"Find, resolve, and depend on components across the federation.")
(~docs/subsection :title "4a. Dependency Resolution"
(p "Components declare dependencies via " (code ":requires") ". "
"Resolution checks local cache first, then home node, then federation.")
(~docs/code :src (highlight
";; Component with declared dependencies\n(defcomp ~charts/bar-chart (&key data labels)\n :requires ((~cssx/tw \"Qm..css\") ;; pinned by CID\n (~charts/axis \"Qm..axis\") ;; specific version\n (~charts/tooltip :latest)) ;; resolve latest from publisher\n ...)\n\n;; Resolution order:\n;; 1. Local IndexedDB (by CID)\n;; 2. Service Worker cache\n;; 3. Home node (WebTransport :want)\n;; 4. Federation query (WebTransport :query)\n;; 5. IPFS gateway (HTTP fallback)"
"lisp"))
(p "CID dependencies are immutable — resolve once, cache forever. "
(code ":latest") " dependencies check the publisher's feed for the current CID."))
(~docs/subsection :title "4b. Component Browser"
(p "Browse, search, and preview components from the federated network. "
"Each component renders its own preview because it IS executable SX.")
(~docs/code :src (highlight
";; Search the federation\n(peer-send home-peer\n (sx-query {:type :component\n :tags [\"chart\" \"visualization\"]\n :has-tests true}))\n\n;; Results include CID, metadata, AND the component itself\n;; Click to preview — evaluates client-side\n;; Click to pin — stores in IndexedDB\n;; Click to fork — opens in editor with source"
"lisp"))
(p "The component browser is itself an SX component published to sx-pub."))
(~docs/subsection :title "4c. Deliverables"
(ul :class "list-disc pl-6 mb-4 space-y-1"
(li (code "web/lib/resolver.sx") " — dependency resolution chain (~200 LOC)")
(li (code "sx/sx/tools/component-browser.sx") " — federated browser (~400 LOC)")
(li (code "sx/sx/tools/search.sx") " — search interface (~150 LOC)")
(li "Server: search index over pinned components"))))
;; =====================================================================
;; Phase 5: Node Bootstrap
;; =====================================================================
(~docs/section :title "Phase 5: Node Bootstrap" :id "phase-5"
(p :class "text-stone-600 italic mb-4"
"Standing up a new node: one command, minutes to first render.")
(~docs/subsection :title "5a. Server Node Bootstrap"
(~docs/code :src (highlight
"# One command. Pulls Docker image, starts all services.\ndocker run -d --name sx-node \\\n -e SX_SEED_PEER=pub.sx-web.org \\\n -e SX_DOMAIN=my-node.example.com \\\n -p 443:443 \\\n ghcr.io/rose-ash/sx-node:latest\n\n# What happens:\n# 1. IPFS daemon starts, connects to network\n# 2. Fetches spec CIDs from seed peer\n# 3. Evaluates specs — now has a working SX evaluator\n# 4. Follows seed peer — receives component announcements\n# 5. Caddy auto-provisions TLS via Let's Encrypt\n# 6. WebTransport endpoint live\n# 7. Ready to serve, publish, and federate"
"bash"))
(p "The bootstrap IS the proof: if your node can fetch specs from the network, "
"evaluate them, and render components — the system works. "
"No manual configuration beyond domain name and seed peer."))
(~docs/subsection :title "5b. Browser Node Bootstrap"
(~docs/code :src (highlight
";; Visit any sx-web node in a browser\n;; → sx-browser.js loads (evaluator + signals + DOM adapter)\n;; → Service Worker installs (CID caching)\n;; → WebTransport connects to host node\n;; → Spec CIDs pinned to IndexedDB\n;; → You're a node.\n;;\n;; No install. No signup. No CLI.\n;; Close the tab → ephemeral node disappears\n;; Reopen → reconnects, IndexedDB still has your pins"
"text"))
(p "A browser becomes a node by visiting a URL. "
"Leaving keeps your pins. Returning restores your state. "
"The barrier to entry is typing an address."))
(~docs/subsection :title "5c. Deliverables"
(ul :class "list-disc pl-6 mb-4 space-y-1"
(li "Docker image: " (code "sx-node") " with OCaml host, IPFS, Caddy, PostgreSQL")
(li "Bootstrap script: fetch specs, verify CIDs, initialize DB")
(li "Seed peer protocol: advertise available specs + popular components")
(li "Browser auto-bootstrap: Service Worker install + spec pinning on first visit"))))
;; =====================================================================
;; Phase 6: AI Composition Layer
;; =====================================================================
(~docs/section :title "Phase 6: AI Composition Layer" :id "phase-6"
(p :class "text-stone-600 italic mb-4"
"The federated graph is simultaneously training data, retrieval context, and composition target.")
(~docs/subsection :title "6a. Component Retrieval"
(p "AI resolves components by CID, not by guessing. "
"The federation is a typed, tested, searchable context window.")
(~docs/code :src (highlight
";; AI composition query\n(ai-compose\n {:intent \"dashboard with sales chart and user table\"\n :constraints {:has-tests true\n :min-pins 10 ;; popularity signal\n :verified true} ;; blockchain-anchored\n :context (deref current-page)})\n\n;; AI doesn't generate from scratch — it assembles:\n;; 1. Search federation for matching components\n;; 2. Check tests pass for each candidate\n;; 3. Resolve dependencies\n;; 4. Compose into page, respecting :requires\n;; 5. Output is auditable: list of CIDs + composition logic"
"lisp"))
(p "The output is a composition of existing, tested, content-addressed components. "
"Not generated code — assembled code. Deterministic, verifiable, traceable."))
(~docs/subsection :title "6b. Self-Improving Tooling"
(p "AI tools are SX components published to sx-pub. "
"They improve as the network grows — more components to compose from, "
"more tests to validate against, more usage patterns to learn from.")
(p "The network effect works for both humans and AI:")
(ul :class "list-disc pl-6 mb-4 space-y-1"
(li "Every component published is more material for AI composition")
(li "Every test attached is more validation for AI output")
(li "Every fork is a preference signal for AI ranking")
(li "Every pin is a popularity signal for AI retrieval")))
(~docs/subsection :title "6c. Deliverables"
(ul :class "list-disc pl-6 mb-4 space-y-1"
(li (code "web/lib/ai-compose.sx") " — composition interface (~200 LOC)")
(li "Embedding index over component metadata + signatures")
(li "Retrieval API: search by intent, filter by quality signals")
(li "Composition validator: check output resolves, tests pass"))))
;; =====================================================================
;; Implementation Order
;; =====================================================================
(~docs/section :title "Implementation Order" :id "order"
(~docs/code :src (highlight
"Phase 1 Browser Node Foundation ~400 LOC SX + ~100 LOC JS\n IndexedDB store, Service Worker, CID verification\n Prerequisite: none (extends existing sx-browser.js)\n\nPhase 2 WebTransport Peer Layer ~550 LOC SX + ~250 LOC Python\n Protocol definition, server endpoint, browser client\n Prerequisite: Phase 1 (needs content store)\n\nPhase 3 In-Browser Authoring ~550 LOC SX\n Editor, test runner, publish flow\n Prerequisite: Phase 1 + 2 (needs store + peer connection)\n\nPhase 4 Component Discovery ~750 LOC SX\n Dependency resolution, component browser, search\n Prerequisite: Phase 2 (needs federation queries)\n\nPhase 5 Node Bootstrap ~300 LOC config + scripts\n Docker image, auto-bootstrap, seed protocol\n Prerequisite: Phase 14 (packages everything)\n\nPhase 6 AI Composition ~400 LOC SX + embedding index\n Retrieval, composition, validation\n Prerequisite: Phase 4 (needs searchable component graph)\n\n Total new SX: ~2650 LOC\n Total new platform: ~350 LOC (JS + Python)\n Timeline: Phases are independent workstreams after Phase 1"
"text"))
(p "Phase 1 is the foundation — everything else depends on the browser being able to "
"store, verify, and cache content-addressed SX. After that, phases 24 can proceed "
"in parallel. Phase 5 packages it all. Phase 6 builds on the populated network."))
;; =====================================================================
;; What Already Exists
;; =====================================================================
(~docs/section :title "What Already Exists (sx-pub Phases 14)" :id "existing"
(table :class "w-full mb-6 text-sm"
(thead
(tr (th :class "text-left p-2" "Capability") (th :class "text-left p-2" "Status") (th :class "text-left p-2" "Location")))
(tbody
(tr (td :class "p-2" "IPFS async client") (td :class "p-2 text-green-700" "Done") (td :class "p-2 font-mono text-xs" "shared/utils/ipfs_client.py"))
(tr (td :class "p-2" "Merkle tree + proofs") (td :class "p-2 text-green-700" "Done") (td :class "p-2 font-mono text-xs" "shared/utils/anchoring.py"))
(tr (td :class "p-2" "OpenTimestamps anchoring") (td :class "p-2 text-green-700" "Done") (td :class "p-2 font-mono text-xs" "shared/utils/anchoring.py"))
(tr (td :class "p-2" "ActivityPub federation") (td :class "p-2 text-green-700" "Done") (td :class "p-2 font-mono text-xs" "shared/infrastructure/activitypub.py"))
(tr (td :class "p-2" "sx-pub HTTP handlers") (td :class "p-2 text-green-700" "Done") (td :class "p-2 font-mono text-xs" "sx/sx/handlers/pub-api.sx"))
(tr (td :class "p-2" "Publishing + CID resolution") (td :class "p-2 text-green-700" "Done") (td :class "p-2 font-mono text-xs" "sx/sxc/pages/pub_helpers.py"))
(tr (td :class "p-2" "SX wire format (aser)") (td :class "p-2 text-green-700" "Done") (td :class "p-2 font-mono text-xs" "web/adapter-sx.sx"))
(tr (td :class "p-2" "Content store (in-memory)") (td :class "p-2 text-green-700" "Done") (td :class "p-2 font-mono text-xs" "lib/content.sx"))
(tr (td :class "p-2" "Component localStorage cache") (td :class "p-2 text-green-700" "Done") (td :class "p-2 font-mono text-xs" "web/boot.sx"))
(tr (td :class "p-2" "Docker dev environment") (td :class "p-2 text-green-700" "Done") (td :class "p-2 font-mono text-xs" "docker-compose.dev-pub.yml"))))
(p "The server-side infrastructure is complete. The plan above extends it to browsers."))))