Fix stepper client-side [object Object] flash and missing CSSX styles
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m54s

Three issues in the stepper island's client-side rendering:

1. do-step used eval-expr with empty env for ~cssx/tw spreads — component
   not found, result leaked as [object Object]. Fixed: call ~cssx/tw
   directly (in scope from island env) with trampoline.

2. steps-to-preview excluded spreads — SSR preview had no styling.
   Fixed: include spreads in the tree so both SSR and client render
   with CSSX classes.

3. build-children used named let (let loop ...) which produces
   unresolved Thunks in render mode due to the named-let compiler
   desugaring interacting with the render/eval boundary. Fixed:
   rewrote as plain recursive function bc-loop avoiding named let.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 00:11:06 +00:00
parent f3f70cc00b
commit 7d7de86034
6 changed files with 322 additions and 53 deletions

View File

@@ -2027,8 +2027,8 @@ PLATFORM_DOM_JS = """
// If lambda takes 0 params, call without event arg (convenience for on-click handlers) // If lambda takes 0 params, call without event arg (convenience for on-click handlers)
var wrapped = isLambda(handler) var wrapped = isLambda(handler)
? (lambdaParams(handler).length === 0 ? (lambdaParams(handler).length === 0
? function(e) { try { cekCall(handler, NIL); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } finally { runPostRenderHooks(); } } ? function(e) { try { cekCall(handler, NIL); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } }
: function(e) { try { cekCall(handler, [e]); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } finally { runPostRenderHooks(); } }) : function(e) { try { cekCall(handler, [e]); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } })
: handler; : handler;
if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler)); if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler));
var passiveEvents = { touchstart: 1, touchmove: 1, wheel: 1, scroll: 1 }; var passiveEvents = { touchstart: 1, touchmove: 1, wheel: 1, scroll: 1 };

View File

@@ -14,7 +14,7 @@
// ========================================================================= // =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-24T22:56:21Z"; var SX_VERSION = "2026-03-25T00:09:53Z";
function isNil(x) { return x === NIL || x === null || x === undefined; } function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); } function isSxTruthy(x) { return x !== false && !isNil(x); }

View File

@@ -103,9 +103,7 @@
(let ((pos (dict "i" 0)) (let ((pos (dict "i" 0))
(max-i (min target (len all-steps)))) (max-i (min target (len all-steps))))
(letrec (letrec
((build-children (fn () ((bc-loop (fn (children)
(let ((children (list)))
(let loop ()
(if (>= (get pos "i") max-i) (if (>= (get pos "i") max-i)
children children
(let ((step (nth all-steps (get pos "i"))) (let ((step (nth all-steps (get pos "i")))
@@ -117,25 +115,26 @@
(let ((tag (get step "tag")) (let ((tag (get step "tag"))
(attrs (or (get step "attrs") (list))) (attrs (or (get step "attrs") (list)))
(spreads (or (get step "spreads") (list))) (spreads (or (get step "spreads") (list)))
(inner (build-children))) (inner (bc-loop (list))))
(append! children (append! children
(concat (list (make-symbol tag)) spreads attrs inner))) (concat (list (make-symbol tag)) spreads attrs inner)))
(loop)) (bc-loop children))
(= stype "close") (= stype "close")
(do (dict-set! pos "i" (+ (get pos "i") 1)) (do (dict-set! pos "i" (+ (get pos "i") 1))
children) children)
(= stype "leaf") (= stype "leaf")
(do (dict-set! pos "i" (+ (get pos "i") 1)) (do (dict-set! pos "i" (+ (get pos "i") 1))
(append! children (get step "expr")) (append! children (get step "expr"))
(loop)) (bc-loop children))
(= stype "expr") (= stype "expr")
(do (dict-set! pos "i" (+ (get pos "i") 1)) (do (dict-set! pos "i" (+ (get pos "i") 1))
(append! children (get step "expr")) (append! children (get step "expr"))
(loop)) (bc-loop children))
:else :else
(do (dict-set! pos "i" (+ (get pos "i") 1)) (do (dict-set! pos "i" (+ (get pos "i") 1))
(loop)))))))))) (bc-loop children))))))))
(let ((root (build-children)))
(let ((root (bc-loop (list))))
(cond (cond
(= (len root) 1) (first root) (= (len root) 1) (first root)
(empty? root) nil (empty? root) nil
@@ -194,16 +193,21 @@
(when (< i (len attrs)) (when (< i (len attrs))
(dom-set-attr el (keyword-name (nth attrs i)) (nth attrs (+ i 1))) (dom-set-attr el (keyword-name (nth attrs i)) (nth attrs (+ i 1)))
(loop (+ i 2)))) (loop (+ i 2))))
;; Evaluate spreads via ~cssx/tw (in scope from island env)
(for-each (fn (sp) (for-each (fn (sp)
(let ((result (eval-expr sp (make-env)))) (when (and (list? sp) (>= (len sp) 3)
(when (and result (spread? result)) (= (type-of (nth sp 1)) "keyword")
(= (keyword-name (nth sp 1)) "tokens")
(string? (nth sp 2)))
(let ((result (trampoline (~cssx/tw :tokens (nth sp 2)))))
(when (spread? result)
(let ((sattrs (spread-attrs result))) (let ((sattrs (spread-attrs result)))
(for-each (fn (k) (for-each (fn (k)
(if (= k "class") (if (= k "class")
(dom-set-attr el "class" (dom-set-attr el "class"
(str (or (dom-get-attr el "class") "") " " (get sattrs k))) (str (or (dom-get-attr el "class") "") " " (get sattrs k)))
(dom-set-attr el k (get sattrs k)))) (dom-set-attr el k (get sattrs k))))
(keys sattrs)))))) (keys sattrs)))))))
spreads) spreads)
(when parent (dom-append parent el)) (when parent (dom-append parent el))
(push-stack el)) (push-stack el))
@@ -214,9 +218,8 @@
(let ((val (get step "expr"))) (let ((val (get step "expr")))
(dom-append parent (create-text-node (if (string? val) val (str val)))))) (dom-append parent (create-text-node (if (string? val) val (str val))))))
(= step-type "expr") (= step-type "expr")
(let ((rendered (render-to-dom (get step "expr") (make-env) nil))) ;; Component expressions handled by lake's reactive render
(when (and parent rendered) nil))
(dom-append parent rendered)))))
(swap! step-idx inc) (swap! step-idx inc)
(update-code-highlight) (update-code-highlight)
(set-cookie "sx-home-stepper" (freeze-to-sx "home-stepper"))))) (set-cookie "sx-home-stepper" (freeze-to-sx "home-stepper")))))
@@ -255,10 +258,11 @@
(build-code-dom) (build-code-dom)
(let ((preview (get-preview))) (let ((preview (get-preview)))
(when preview (dom-set-prop preview "innerHTML" ""))) (when preview (dom-set-prop preview "innerHTML" "")))
(batch (fn ()
(let ((target (deref step-idx))) (let ((target (deref step-idx)))
(reset! step-idx 0) (reset! step-idx 0)
(set-stack (list (get-preview))) (set-stack (list (get-preview)))
(for-each (fn (_) (do-step)) (slice (deref steps) 0 target))) (for-each (fn (_) (do-step)) (slice (deref steps) 0 target)))))
(update-code-highlight) (update-code-highlight)
(run-post-render-hooks))))))) (run-post-render-hooks)))))))
(div :class "space-y-4" (div :class "space-y-4"

View File

@@ -443,7 +443,8 @@
:children (list :children (list
{:label "SX URLs" :href "/sx/(applications.(sx-urls))"} {:label "SX URLs" :href "/sx/(applications.(sx-urls))"}
{:label "CSSX" :href "/sx/(applications.(cssx))" :children cssx-nav-items} {:label "CSSX" :href "/sx/(applications.(cssx))" :children cssx-nav-items}
{:label "Protocols" :href "/sx/(applications.(protocol))" :children protocols-nav-items})} {:label "Protocols" :href "/sx/(applications.(protocol))" :children protocols-nav-items}
{:label "sx-pub" :href "/sx/(applications.(sx-pub))"})}
{:label "Etc" :href "/sx/(etc)" {:label "Etc" :href "/sx/(etc)"
:children (list :children (list
{:label "Essays" :href "/sx/(etc.(essay))" :children essays-nav-items} {:label "Essays" :href "/sx/(etc.(essay))" :children essays-nav-items}

View File

@@ -461,6 +461,13 @@
(define protocol (define protocol
(make-page-fn "~protocols/wire-format-content" "~protocols/" nil "-content")) (make-page-fn "~protocols/wire-format-content" "~protocols/" nil "-content"))
;; sx-pub (under applications)
(define sx-pub
(fn (slug)
(if (nil? slug)
'(~sx-pub/overview-content)
nil)))
;; Essays (under etc) ;; Essays (under etc)
;; Convention: ~essays/{slug}/essay-{slug} ;; Convention: ~essays/{slug}/essay-{slug}
(define essay (define essay

257
sx/sx/plans/sx-pub.sx Normal file
View File

@@ -0,0 +1,257 @@
;; ---------------------------------------------------------------------------
;; sx-pub: Federated SX Publishing Protocol
;; ---------------------------------------------------------------------------
(defcomp ~sx-pub/overview-content ()
(~docs/page :title "sx-pub"
(~docs/section :title "What It Is" :id "what"
(p "A federated content publishing protocol built entirely in SX. No JSON. Content pinned to IPFS, referenced by CID. Blockchain-anchored provenance.")
(p "One actor — " (code "sx") " — publishes SX specs, platform libraries, components, and documentation as content-addressed packages. Other sx-pub servers can follow, mirror, and refer to content by CID. Human-readable paths map to immutable IPFS addresses.")
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mt-4"
(p :class "text-violet-900 font-medium" "The key insight")
(p :class "text-violet-800" "IPFS is the database. SX is the wire format. defhandlers are the endpoints. The wire format is the programming language is the component system is the package manager.")))
;; -----------------------------------------------------------------------
;; Why ActivityPub Failed — and Why LISP Fixes It
;; -----------------------------------------------------------------------
(~docs/section :title "Why ActivityPub Failed" :id "why"
(p "ActivityPub's problems aren't bugs — they're consequences of a fundamental design error: " (strong "the wire format is dead data") ". JSON-LD activities describe what happened but can't " (em "do") " anything. Every consumer must independently interpret, render, and extend. The fediverse is N incompatible applications showing the same inert data differently.")
(~docs/subsection :title "1. The Rendering Gap"
(p "An ActivityPub Note looks like this:")
(~docs/code :code "{\"type\": \"Note\",\n \"content\": \"<p>Hello <strong>world</strong></p>\",\n \"attributedTo\": \"https://example.com/users/alice\"}")
(p "The content is an HTML string embedded in a JSON string. Two formats nested, neither evaluable. Every client — Mastodon, Pleroma, Misskey, Lemmy — parses this independently and wraps it in their own UI. A poll renders differently everywhere. An event looks different everywhere. There is no shared component model because the protocol has no concept of components.")
(p "In sx-pub, the content " (em "is") " the component:")
(~docs/code :code "(Note\n :attributed-to \"https://pub.sx-web.org/pub/actor\"\n :content (p \"Hello \" (strong \"world\")))")
(p "No HTML-in-JSON. No parsing step. The receiver evaluates the s-expression and gets rendered output. The content carries its own rendering semantics."))
(~docs/subsection :title "2. Inert Data vs. Evaluable Programs"
(p "A JSON-LD activity is a description. It says \"Alice created a Note with this HTML.\" The receiving server must then decide: how do I display a Note? What if it has attachments? What about custom emoji? What about a poll? Every new feature requires every implementation to add rendering code.")
(p "An SX activity is a " (em "program") ". It evaluates to its own UI. A poll isn't a JSON blob that each client renders differently — it's a " (code "(defcomp ~poll ...)") " that evaluates identically everywhere because the CEK machine has deterministic semantics. " (strong "The wire format is the renderer.")))
(~docs/subsection :title "3. Extensions Are Impossible"
(p "ActivityPub extensions (FEPs) add new JSON fields that most implementations ignore. Want to federate a recipe, a calendar event, a code snippet with syntax highlighting? Write a spec, wait years for implementations to support it, accept that most never will.")
(p "In sx-pub, extensions are just components. Publish " (code "(defcomp ~recipe ...)") " to IPFS. Anyone who follows you gets the component. Their server can evaluate it immediately — " (strong "no implementation changes needed") ". The extension mechanism is the same as the content mechanism: pin an s-expression, reference it by CID."))
(~docs/subsection :title "4. No Content Integrity"
(p "ActivityPub has no content addressing. A server can silently edit a federated post. HTTP signatures verify the " (em "transport") " but not the " (em "content") ". There's no way to prove that what you see is what was originally published.")
(p "sx-pub content is IPFS-pinned and referenced by CID (content hash). The CID " (em "is") " the identity. Mutate a single character and the CID changes. Merkle trees of CIDs get anchored to Bitcoin via OpenTimestamps — cryptographic proof of what was published and when."))
(~docs/subsection :title "5. The N-Client Problem"
(p "Because AP content is inert, every client must build its own renderer, interaction model, and extension system. The result: Mastodon is one application, Pleroma is another, Misskey is another. They can exchange data, but the " (em "experience") " is entirely determined by the client. Rich content degrades to plain text when the client doesn't understand it.")
(p "sx-pub eliminates this entirely. Content carries its rendering logic. A server that receives " (code "(defcomp ~interactive-chart ...)") " can render it correctly without any prior knowledge of charts — because the component is self-contained, sandboxed (boundary enforcement prevents IO in pure components), and deterministic. " (strong "Every server renders the same content the same way.")))
(~docs/subsection :title "6. No Dependency System"
(p "AP has no concept of shared code. If ten ActivityPub projects all need a Markdown parser, ten teams write ten Markdown parsers. There's no mechanism to publish reusable logic that travels between servers.")
(p "sx-pub's " (code ":requires") " field on published documents declares CID dependencies. A component that uses a chart library says " (code ":requires (list \"bafy...chart-lib\")") ". Pin the dependency tree and everything works offline. " (strong "This is a package manager built into the federation protocol.") " No npm, no crates.io, no central registry — just content-addressed s-expressions on IPFS."))
(~docs/subsection :title "7. The @context Illusion"
(p "JSON-LD @context was supposed to provide semantic interoperability. In practice it's a constant source of bugs. Implementations disagree on which contexts to support, which extensions to recognise, how to resolve conflicts. The W3C ActivityStreams context document is a single point of failure that the entire fediverse depends on.")
(p "SX needs no context resolution. " (code "(Publish :actor \"...\" :object (SxDocument :cid \"...\"))") " is self-describing. The s-expression " (em "is") " the meaning. Symbols are symbols. Keywords are keywords. No indirection, no remote JSON documents, no semantic web machinery."))
(div :class "rounded border border-rose-200 bg-rose-50 p-4 mt-6"
(p :class "text-rose-900 font-medium" "The fundamental difference")
(p :class "text-rose-800" "ActivityPub federates " (em "descriptions") " of content. Consumers must independently figure out how to present them. sx-pub federates " (em "programs") " — self-contained, evaluable, deterministic, content-addressed. The gap between \"data\" and \"application\" disappears because in LISP, they were never separate to begin with.")))
;; -----------------------------------------------------------------------
;; Wire Format
;; -----------------------------------------------------------------------
(~docs/section :title "Wire Format" :id "wire-format"
(p "Everything is s-expressions. No JSON-LD, no @context resolution, no nested HTML-in-JSON strings.")
(~docs/subsection :title "Actor"
(~docs/code :code "(SxActor\n :id \"https://pub.sx-web.org/pub/actor\"\n :type \"SxPublisher\"\n :name \"sx\"\n :summary \"SX language — specs, platforms, components\"\n :public-key-pem \"-----BEGIN PUBLIC KEY-----\\n...\"\n :inbox \"/pub/inbox\"\n :outbox \"/pub/outbox\"\n :followers \"/pub/followers\"\n :following \"/pub/following\"\n :collections (list\n (SxCollection :name \"core-specs\" :href \"/pub/core-specs\")\n (SxCollection :name \"platforms\" :href \"/pub/platforms\")\n (SxCollection :name \"components\" :href \"/pub/components\")))"))
(~docs/subsection :title "Publish Activity"
(p "When content is published, it's pinned to IPFS and announced to followers:")
(~docs/code :code "(Publish\n :actor \"https://pub.sx-web.org/pub/actor\"\n :published \"2026-03-24T12:00:00Z\"\n :object (SxDocument\n :name \"evaluator\"\n :path \"/pub/core-specs/evaluator\"\n :cid \"bafy...eval123\"\n :content-type \"text/sx\"\n :size 48000\n :hash \"sha3-256:abc123...\"\n :requires (list \"bafy...parser\" \"bafy...primitives\")\n :summary \"CEK machine evaluator — frames, utils, step function\"))")
(p "The " (code ":requires") " field declares CID dependencies — a component that imports the parser declares that dependency. Receivers can pin the full dependency graph."))
(~docs/subsection :title "Follow — Subscribe to a Remote Server"
(~docs/code :code "(Follow\n :actor \"https://pub.sx-web.org/pub/actor\"\n :object \"https://other.example.com/pub/actor\")")
(p "When accepted, the remote server delivers " (code "(Publish ...)") " activities to our inbox. We pin their CIDs locally."))
(~docs/subsection :title "Announce — Mirror Remote Content"
(~docs/code :code "(Announce\n :actor \"https://pub.sx-web.org/pub/actor\"\n :object \"bafy...remote-cid\")")
(p "Re-publish content from a followed server to our own followers. The CID is already pinned — we're just signalling that we endorse it."))
(~docs/subsection :title "Anchor — Blockchain Provenance Record"
(~docs/code :code "(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-24T12:00:00Z\")")
(p "Embedded in every " (code "(Publish ...)") " activity. Receivers can verify: the Merkle tree was anchored in that Bitcoin block, and the CID is a leaf in that tree. Inclusion proof is verifiable offline.")))
;; -----------------------------------------------------------------------
;; Endpoints
;; -----------------------------------------------------------------------
(~docs/section :title "Endpoints" :id "endpoints"
(p "All endpoints are " (code "defhandler") " definitions in " (code ".sx") " files. Python provides only IO primitives behind the boundary.")
(table :class "w-full text-sm"
(thead
(tr :class "border-b border-stone-200 text-left"
(th :class "py-2 pr-4" "Method")
(th :class "py-2 pr-4" "Route")
(th :class "py-2" "Purpose")))
(tbody :class "text-stone-600"
(tr :class "border-b border-stone-100"
(td :class "py-2 pr-4 font-mono text-xs" "GET") (td :class "py-2 pr-4 font-mono text-xs" "/pub/actor") (td :class "py-2" "Actor profile"))
(tr :class "border-b border-stone-100"
(td :class "py-2 pr-4 font-mono text-xs" "GET") (td :class "py-2 pr-4 font-mono text-xs" "/pub/outbox") (td :class "py-2" "Published activity feed"))
(tr :class "border-b border-stone-100"
(td :class "py-2 pr-4 font-mono text-xs" "POST") (td :class "py-2 pr-4 font-mono text-xs" "/pub/inbox") (td :class "py-2" "Receive activities from remote servers"))
(tr :class "border-b border-stone-100"
(td :class "py-2 pr-4 font-mono text-xs" "GET") (td :class "py-2 pr-4 font-mono text-xs" "/pub/⟨collection⟩") (td :class "py-2" "List items in a collection"))
(tr :class "border-b border-stone-100"
(td :class "py-2 pr-4 font-mono text-xs" "GET") (td :class "py-2 pr-4 font-mono text-xs" "/pub/⟨collection⟩/⟨slug⟩") (td :class "py-2" "Document by path — resolves CID, fetches from IPFS"))
(tr :class "border-b border-stone-100"
(td :class "py-2 pr-4 font-mono text-xs" "GET") (td :class "py-2 pr-4 font-mono text-xs" "/pub/cid/⟨cid⟩") (td :class "py-2" "Direct CID fetch (immutable, cache forever)"))
(tr :class "border-b border-stone-100"
(td :class "py-2 pr-4 font-mono text-xs" "POST") (td :class "py-2 pr-4 font-mono text-xs" "/pub/publish") (td :class "py-2" "Pin to IPFS + create Publish activity + announce"))
(tr :class "border-b border-stone-100"
(td :class "py-2 pr-4 font-mono text-xs" "POST") (td :class "py-2 pr-4 font-mono text-xs" "/pub/follow") (td :class "py-2" "Follow a remote sx-pub server"))
(tr :class "border-b border-stone-100"
(td :class "py-2 pr-4 font-mono text-xs" "GET") (td :class "py-2 pr-4 font-mono text-xs" "/pub/followers") (td :class "py-2" "Who follows us"))
(tr :class "border-b border-stone-100"
(td :class "py-2 pr-4 font-mono text-xs" "GET") (td :class "py-2 pr-4 font-mono text-xs" "/pub/following") (td :class "py-2" "Who we follow"))
(tr :class "border-b border-stone-100"
(td :class "py-2 pr-4 font-mono text-xs" "GET") (td :class "py-2 pr-4 font-mono text-xs" "/.well-known/webfinger") (td :class "py-2" "Discovery"))
(tr
(td :class "py-2 pr-4 font-mono text-xs" "GET") (td :class "py-2 pr-4 font-mono text-xs" "/pub/anchor/⟨tree-cid⟩") (td :class "py-2" "Merkle tree verification")))))
;; -----------------------------------------------------------------------
;; Storage
;; -----------------------------------------------------------------------
(~docs/section :title "Storage" :id "storage"
(p "IPFS is the canonical content store. PostgreSQL is the local index.")
(table :class "w-full text-sm"
(thead
(tr :class "border-b border-stone-200 text-left"
(th :class "py-2 pr-4" "What")
(th :class "py-2" "Where")))
(tbody :class "text-stone-600"
(tr :class "border-b border-stone-100"
(td :class "py-2 pr-4" "Content (SX source)") (td :class "py-2" "IPFS — pinned, referenced by CID"))
(tr :class "border-b border-stone-100"
(td :class "py-2 pr-4" "Path → CID index") (td :class "py-2" "PostgreSQL"))
(tr :class "border-b border-stone-100"
(td :class "py-2 pr-4" "Outbox activities") (td :class "py-2" "PostgreSQL"))
(tr :class "border-b border-stone-100"
(td :class "py-2 pr-4" "Followers / following") (td :class "py-2" "PostgreSQL"))
(tr :class "border-b border-stone-100"
(td :class "py-2 pr-4" "Anchor records") (td :class "py-2" "PostgreSQL"))
(tr :class "border-b border-stone-100"
(td :class "py-2 pr-4" "Remote mirrored content") (td :class "py-2" "PostgreSQL + IPFS (pinned locally)"))
(tr :class "border-b border-stone-100"
(td :class "py-2 pr-4" "Content cache") (td :class "py-2" "Redis"))
(tr
(td :class "py-2 pr-4" "Actor keypair") (td :class "py-2" "PostgreSQL")))))
;; -----------------------------------------------------------------------
;; SX / Python Split
;; -----------------------------------------------------------------------
(~docs/section :title "SX / Python Split" :id "split"
(p "SX handles all composition, logic, and rendering. Python provides IO primitives behind the boundary.")
(div :class "grid grid-cols-2 gap-6"
(div
(h4 :class "font-semibold text-stone-700 mb-2" "Pure SX (defhandler + defcomp)")
(ul :class "text-sm text-stone-600 space-y-1 list-disc pl-5"
(li "All HTTP routing and request dispatch")
(li "Content negotiation (Accept header)")
(li "Response construction (SX actor docs, collections)")
(li "UI components for browsing")
(li "Activity serialization")
(li "Nav data for /pub/ sections")))
(div
(h4 :class "font-semibold text-stone-700 mb-2" "Python Helpers (IO boundary)")
(ul :class "text-sm text-stone-600 space-y-1 list-disc pl-5"
(li (code "pub-ipfs-add") " / " (code "pub-ipfs-get") " / " (code "pub-ipfs-pin") " — IPFS ops")
(li (code "pub-db-query") " / " (code "pub-db-insert") " — database")
(li (code "pub-sign") " / " (code "pub-verify-signature") " — HTTP signatures")
(li (code "pub-hash") " — SHA3-256")
(li (code "pub-anchor-batch") " — OpenTimestamps")
(li (code "pub-deliver") " — POST to follower inboxes")))))
;; -----------------------------------------------------------------------
;; Flows
;; -----------------------------------------------------------------------
(~docs/section :title "Flows" :id "flows"
(~docs/subsection :title "Publish"
(p :class "text-sm text-stone-600 mb-2" "Publishing is " (strong "anchor-then-announce") " — content is provenance-sealed before anyone sees it. This prevents a malicious follower from seeing content, anchoring it first, and claiming provenance.")
(ol :class "text-sm text-stone-600 space-y-2 list-decimal pl-5"
(li (code "POST /pub/publish") " with collection, slug, SX content")
(li "Pin to IPFS → get CID (content is now addressable but not announced)")
(li "Store path→CID in DB (status: " (code "anchoring") ")")
(li "Build Merkle tree, submit to OpenTimestamps, wait for Bitcoin confirmation")
(li "On confirmation: update status to " (code "anchored") ", record proof")
(li "Construct " (code "(Publish ...)") " with anchor proof embedded")
(li "Deliver to follower inboxes — " (strong "only now") " is it public"))
(p :class "text-sm text-stone-500 mt-2 italic" "This means publishing isn't instant — there's a delay while the anchor confirms. That's a feature: it forces a deliberate pace and makes provenance airtight.")
(~docs/code :code "(Publish\n :actor \"https://pub.sx-web.org/pub/actor\"\n :published \"2026-03-24T12:00:00Z\"\n :object (SxDocument\n :name \"evaluator\"\n :cid \"bafy...eval123\"\n :anchor (Anchor\n :tree-cid \"bafy...merkle\"\n :ots-cid \"bafy...proof\"\n :btc-txid \"abc123...\"\n :btc-block 890123)))")
(p :class "text-sm text-stone-600" "Receivers verify: the CID was anchored in that Bitcoin block " (em "before") " the Publish was sent. No way to backdate."))
(~docs/subsection :title "Follow"
(ol :class "text-sm text-stone-600 space-y-2 list-decimal pl-5"
(li (code "POST /pub/follow") " with remote actor URL")
(li "Fetch remote actor document (" (code "text/sx") ")")
(li "Send " (code "(Follow ...)") " to their inbox, signed with our key")
(li "They verify signature, add us to followers, send " (code "(Accept ...)"))
(li "We store them in our following list")
(li "They start delivering " (code "(Publish ...)") " activities to our inbox")))
(~docs/subsection :title "Receive (inbox)"
(ol :class "text-sm text-stone-600 space-y-2 list-decimal pl-5"
(li "Remote server POSTs " (code "(Publish ...)") " to our inbox")
(li "Verify HTTP signature against their actor's public key")
(li "Pin the referenced CID to our local IPFS node")
(li "Index the remote content in our DB (marked as mirrored)")
(li "Now we can resolve that CID locally — " (code "/pub/cid/bafy...") " just works")))
(~docs/subsection :title "Anchoring"
(p :class "text-sm text-stone-600 mb-2" "Anchoring is integrated into the publish flow — not a separate background job. Content waits for provenance before going public.")
(ol :class "text-sm text-stone-600 space-y-2 list-decimal pl-5"
(li "Batch pending CIDs into a Merkle tree (multiple publishes can share one tree)")
(li "Pin Merkle tree to IPFS")
(li "Submit Merkle root to OpenTimestamps calendar servers")
(li "Poll for Bitcoin confirmation (typically ~1 hour)")
(li "On confirmation: store proof, transition content to " (code "anchored") " status")
(li "Trigger delivery of Publish activities for all items in the batch"))
(p :class "text-sm text-stone-500 mt-2 italic" "Batching amortises the anchor cost — one Bitcoin attestation can seal hundreds of CIDs via the Merkle tree. Each CID gets an inclusion proof linking it to the anchored root.")))
;; -----------------------------------------------------------------------
;; CID ↔ Path
;; -----------------------------------------------------------------------
(~docs/section :title "CID ↔ Path Translation" :id "cid-path"
(p "Published SX refers to dependencies by CID — the immutable, universal identifier. But humans browse by path.")
(~docs/code :code ";; Canonical SX (stored in IPFS) references by CID\n(defcomp ~my-app/dashboard (&key data)\n ;; This component depends on a chart library published at another CID\n (let ((chart-lib (require \"bafy...chart-lib-cid\")))\n (div :class \"dashboard\"\n (chart-lib/bar-chart :data data))))\n\n;; But on pub.sx-web.org you browse by path:\n;; /pub/components/dashboard\n;; /pub/core-specs/evaluator\n;; /pub/platforms/javascript\n;;\n;; The DB maps: path → CID → IPFS content\n;; Following a remote server maps: their-path → CID → our local pin")
(p "When you follow another sx-pub server and they publish a component, you get the " (code "(Publish ...)") " activity with the CID. Your server pins it. Now " (code "(require \"bafy...\")") " resolves locally — no network round-trip, no central registry, no package manager."))
;; -----------------------------------------------------------------------
;; Phases
;; -----------------------------------------------------------------------
(~docs/section :title "Implementation Phases" :id "phases"
(div :class "space-y-4"
(div :class "rounded border border-emerald-200 bg-emerald-50 p-4"
(h4 :class "font-semibold text-emerald-800" "Phase 1: Foundation")
(p :class "text-emerald-700 text-sm" "DB schema, async IPFS client, actor endpoint, webfinger, " (code "/pub/actor") " returns SX actor document."))
(div :class "rounded border border-sky-200 bg-sky-50 p-4"
(h4 :class "font-semibold text-sky-800" "Phase 2: Publishing")
(p :class "text-sky-700 text-sm" "Pin to IPFS, path→CID index, collection browsing. Publish the actual SX spec files as the first content."))
(div :class "rounded border border-violet-200 bg-violet-50 p-4"
(h4 :class "font-semibold text-violet-800" "Phase 3: Federation")
(p :class "text-violet-700 text-sm" "Inbox/outbox, follow/accept, HTTP signature verification, activity delivery, content mirroring."))
(div :class "rounded border border-amber-200 bg-amber-50 p-4"
(h4 :class "font-semibold text-amber-800" "Phase 4: Anchoring")
(p :class "text-amber-700 text-sm" "Merkle trees, OpenTimestamps, Bitcoin proof, provenance UI."))))))