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
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:
@@ -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 };
|
||||||
|
|||||||
@@ -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); }
|
||||||
|
|||||||
@@ -103,39 +103,38 @@
|
|||||||
(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)))
|
(if (>= (get pos "i") max-i)
|
||||||
(let loop ()
|
children
|
||||||
(if (>= (get pos "i") max-i)
|
(let ((step (nth all-steps (get pos "i")))
|
||||||
children
|
(stype (get step "type")))
|
||||||
(let ((step (nth all-steps (get pos "i")))
|
(cond
|
||||||
(stype (get step "type")))
|
(= stype "open")
|
||||||
(cond
|
(do
|
||||||
(= stype "open")
|
(dict-set! pos "i" (+ (get pos "i") 1))
|
||||||
(do
|
(let ((tag (get step "tag"))
|
||||||
(dict-set! pos "i" (+ (get pos "i") 1))
|
(attrs (or (get step "attrs") (list)))
|
||||||
(let ((tag (get step "tag"))
|
(spreads (or (get step "spreads") (list)))
|
||||||
(attrs (or (get step "attrs") (list)))
|
(inner (bc-loop (list))))
|
||||||
(spreads (or (get step "spreads") (list)))
|
(append! children
|
||||||
(inner (build-children)))
|
(concat (list (make-symbol tag)) spreads attrs inner)))
|
||||||
(append! children
|
(bc-loop children))
|
||||||
(concat (list (make-symbol tag)) spreads attrs inner)))
|
(= stype "close")
|
||||||
(loop))
|
(do (dict-set! pos "i" (+ (get pos "i") 1))
|
||||||
(= stype "close")
|
children)
|
||||||
(do (dict-set! pos "i" (+ (get pos "i") 1))
|
(= stype "leaf")
|
||||||
children)
|
(do (dict-set! pos "i" (+ (get pos "i") 1))
|
||||||
(= stype "leaf")
|
(append! children (get step "expr"))
|
||||||
(do (dict-set! pos "i" (+ (get pos "i") 1))
|
(bc-loop children))
|
||||||
(append! children (get step "expr"))
|
(= stype "expr")
|
||||||
(loop))
|
(do (dict-set! pos "i" (+ (get pos "i") 1))
|
||||||
(= stype "expr")
|
(append! children (get step "expr"))
|
||||||
(do (dict-set! pos "i" (+ (get pos "i") 1))
|
(bc-loop children))
|
||||||
(append! children (get step "expr"))
|
:else
|
||||||
(loop))
|
(do (dict-set! pos "i" (+ (get pos "i") 1))
|
||||||
:else
|
(bc-loop children))))))))
|
||||||
(do (dict-set! pos "i" (+ (get pos "i") 1))
|
|
||||||
(loop))))))))))
|
(let ((root (bc-loop (list))))
|
||||||
(let ((root (build-children)))
|
|
||||||
(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")
|
||||||
(let ((sattrs (spread-attrs result)))
|
(= (keyword-name (nth sp 1)) "tokens")
|
||||||
(for-each (fn (k)
|
(string? (nth sp 2)))
|
||||||
(if (= k "class")
|
(let ((result (trampoline (~cssx/tw :tokens (nth sp 2)))))
|
||||||
(dom-set-attr el "class"
|
(when (spread? result)
|
||||||
(str (or (dom-get-attr el "class") "") " " (get sattrs k)))
|
(let ((sattrs (spread-attrs result)))
|
||||||
(dom-set-attr el k (get sattrs k))))
|
(for-each (fn (k)
|
||||||
(keys sattrs))))))
|
(if (= k "class")
|
||||||
|
(dom-set-attr el "class"
|
||||||
|
(str (or (dom-get-attr el "class") "") " " (get sattrs k)))
|
||||||
|
(dom-set-attr el k (get sattrs k))))
|
||||||
|
(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" "")))
|
||||||
(let ((target (deref step-idx)))
|
(batch (fn ()
|
||||||
(reset! step-idx 0)
|
(let ((target (deref step-idx)))
|
||||||
(set-stack (list (get-preview)))
|
(reset! step-idx 0)
|
||||||
(for-each (fn (_) (do-step)) (slice (deref steps) 0 target)))
|
(set-stack (list (get-preview)))
|
||||||
|
(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"
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
257
sx/sx/plans/sx-pub.sx
Normal 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."))))))
|
||||||
Reference in New Issue
Block a user