Add content-addressed components plan to sx-docs

7-phase plan: canonical serialization, CID computation, component
manifests, IPFS storage & resolution cascade, security model (purity
verification, content verification, eval limits, trust tiers),
wire format integration with prefetch system, and federated sharing
via AP component registry actors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 22:55:13 +00:00
parent e38534a898
commit 7229335d22
3 changed files with 420 additions and 1 deletions

View File

@@ -114,7 +114,9 @@
(dict :label "SX-Activity" :href "/plans/sx-activity"
:summary "A new web built on SX — executable content, shared components, parsers, and logic on IPFS, provenance on Bitcoin, all running within your own security context.")
(dict :label "Predictive Prefetching" :href "/plans/predictive-prefetch"
:summary "Prefetch missing component definitions before the user clicks — hover a link, fetch its deps, navigate client-side.")))
:summary "Prefetch missing component definitions before the user clicks — hover a link, fetch its deps, navigate client-side.")
(dict :label "Content-Addressed Components" :href "/plans/content-addressed-components"
:summary "Components identified by CID, stored on IPFS, fetched from anywhere. Canonical serialization, content verification, federated sharing.")))
(define bootstrappers-nav-items (list
(dict :label "Overview" :href "/bootstrappers/")

View File

@@ -594,6 +594,422 @@
(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 ":css-atoms") " — CSSX class names the component uses. The consumer can pre-resolve CSS rules without parsing the source")
(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
;; ---------------------------------------------------------------------------

View File

@@ -478,4 +478,5 @@
"reader-macros" (~plan-reader-macros-content)
"sx-activity" (~plan-sx-activity-content)
"predictive-prefetch" (~plan-predictive-prefetch-content)
"content-addressed-components" (~plan-content-addressed-components-content)
:else (~plans-index-content)))