Nav redesign: embedded breadcrumb navigation with recursive depth
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m25s

Replace menu bar navigation with in-page nav embedded in content area.
Each page shows logo/tagline/copyright, then a sibling row per trail
level (← prev | Current | next →), then children as button links.

- resolve-nav-path: recursive walk with no depth limit
- find-nav-index: rewritten with recursion (set! broken across closures)
- Walk stops on exact href match (prevents /cssx/ drilling into Overview)
- Unicode chars (©, ←, →) inline instead of \u escapes (SX parser doesn't support them)
- All 38 defpages wrapped in (~sx-doc :path ...) for in-page nav
- Layout returns only root header (nav moved out of blue menu bar)
- Standalone layout variants for sx-web.org (return nil)
- New plans: environment-images, runtime-slicing, typed-sx, nav-redesign, sx-web-platform

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 14:37:37 +00:00
parent cad65bcdf1
commit ec1093d372
10 changed files with 1887 additions and 696 deletions

View File

@@ -1,143 +1,118 @@
;; SX docs layout defcomps — fully self-contained via IO primitives.
;; Registered via register_sx_layout in __init__.py.
;; --- Main nav defcomp: static nav items from MAIN_NAV ---
;; @css aria-selected:bg-violet-200 aria-selected:text-violet-900
(defcomp ~sx-main-nav (&key section)
(let* ((sc "aria-selected:bg-violet-200 aria-selected:text-violet-900")
(items (list
(dict :label "Docs" :href "/docs/introduction")
(dict :label "CSSX" :href "/cssx/")
(dict :label "Reference" :href "/reference/")
(dict :label "Protocols" :href "/protocols/wire-format")
(dict :label "Examples" :href "/examples/click-to-load")
(dict :label "Essays" :href "/essays/")
(dict :label "Philosophy" :href "/philosophy/")
(dict :label "Specs" :href "/specs/")
(dict :label "Bootstrappers" :href "/bootstrappers/")
(dict :label "Testing" :href "/testing/")
(dict :label "Isomorphism" :href "/isomorphism/")
(dict :label "Plans" :href "/plans/")
(dict :label "Reactive Islands" :href "/reactive-islands/"))))
(<> (map (lambda (item)
(~nav-link
:href (get item "href")
:label (get item "label")
:is-selected (when (= (get item "label") section) "true")
:select-colours sc))
items))))
;; --- SX header row ---
(defcomp ~sx-header-row (&key nav child oob)
(~menu-row-sx :id "sx-row" :level 1 :colour "violet"
:link-href "/" :link-label "sx"
:link-label-content (~sx-docs-label)
:nav nav
:child-id "sx-header-child"
:child child
:oob oob))
;; --- Sub-row for section pages ---
(defcomp ~sx-sub-row (&key sub-label sub-href sub-nav selected oob)
(~menu-row-sx :id "sx-sub-row" :level 2 :colour "violet"
:link-href sub-href :link-label sub-label
:selected selected
:nav sub-nav
:oob oob))
;; SX docs layout defcomps + in-page navigation.
;; Layout = root header only. Nav is in-page via ~sx-doc wrapper.
;; ---------------------------------------------------------------------------
;; SX home layout (root + sx header)
;; Nav components — logo header, sibling arrows, children links
;; ---------------------------------------------------------------------------
(defcomp ~sx-layout-full (&key section)
(<> (~root-header-auto)
(~sx-header-row :nav (~sx-main-nav :section section))))
;; @css text-violet-700 text-violet-600 text-violet-500 text-stone-400 text-stone-500 text-stone-600
;; @css hover:text-violet-600 hover:text-violet-700 hover:bg-violet-50
;; @css bg-violet-50 border-violet-200 border
(defcomp ~sx-layout-oob (&key section)
(<> (~sx-header-row
:nav (~sx-main-nav :section section)
:oob true)
(~clear-oob-div :id "sx-header-child")
(~root-header-auto true)))
;; Logo + tagline + copyright — always shown at top of page area.
(defcomp ~sx-header ()
(div :class "max-w-3xl mx-auto px-4 pt-8 pb-4 text-center"
(a :href "/"
:sx-get "/" :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
:class "block mb-2"
(span :class "text-4xl font-bold font-mono text-violet-700" "(<sx>)"))
(p :class "text-lg text-stone-500 mb-1"
"Framework free reactive hypermedia")
(p :class "text-xs text-stone-400"
"© Giles Bradshaw 2026")))
(defcomp ~sx-layout-mobile (&key section)
(<> (~mobile-menu-section
:label "sx" :href "/" :level 1 :colour "violet"
:items (~sx-main-nav :section section))
(~root-mobile-auto)))
;; Current section with annotated prev/next siblings.
;; Desktop: prev ← Current → next (horizontal)
;; Mobile: stacked vertically
(defcomp ~nav-sibling-row (&key node siblings)
(let* ((idx (find-nav-index siblings node))
(count (len siblings))
(prev-idx (mod (+ (- idx 1) count) count))
(next-idx (mod (+ idx 1) count))
(prev-node (nth siblings prev-idx))
(next-node (nth siblings next-idx)))
(div :class "max-w-3xl mx-auto px-4 py-2 flex items-center justify-center gap-4"
(a :href (get prev-node "href")
:sx-get (get prev-node "href") :sx-target "#main-panel"
:sx-select "#main-panel" :sx-swap "outerHTML"
:sx-push-url "true"
:class "text-sm text-stone-500 hover:text-violet-600"
(str "← " (get prev-node "label")))
(a :href (get node "href")
:sx-get (get node "href") :sx-target "#main-panel"
:sx-select "#main-panel" :sx-swap "outerHTML"
:sx-push-url "true"
:class "text-lg font-semibold text-violet-700 px-4"
(get node "label"))
(a :href (get next-node "href")
:sx-get (get next-node "href") :sx-target "#main-panel"
:sx-select "#main-panel" :sx-swap "outerHTML"
:sx-push-url "true"
:class "text-sm text-stone-500 hover:text-violet-600"
(str (get next-node "label") " →")))))
;; Children links — shown as clearly clickable buttons.
(defcomp ~nav-children (&key items)
(div :class "max-w-3xl mx-auto px-4 py-3"
(div :class "flex flex-wrap justify-center gap-2"
(map (fn (item)
(a :href (get item "href")
:sx-get (get item "href") :sx-target "#main-panel"
:sx-select "#main-panel" :sx-swap "outerHTML"
:sx-push-url "true"
:class "px-3 py-1.5 text-sm rounded border border-violet-200 text-violet-700 hover:bg-violet-50 transition-colors"
(get item "label")))
items))))
;; ---------------------------------------------------------------------------
;; SX section layout (root + sx header + sub-row)
;; ~sx-doc — in-page content wrapper with nav
;; Used by every defpage :content to embed nav inside the page content area.
;; ---------------------------------------------------------------------------
(defcomp ~sx-section-layout-full (&key section sub-label sub-href sub-nav selected)
(<> (~root-header-auto)
(~sx-header-row
:nav (~sx-main-nav :section section)
:child (~sx-sub-row :sub-label sub-label :sub-href sub-href
:sub-nav sub-nav :selected selected))))
(defcomp ~sx-section-layout-oob (&key section sub-label sub-href sub-nav selected)
(<> (~oob-header-sx :parent-id "sx-header-child"
:row (~sx-sub-row :sub-label sub-label :sub-href sub-href
:sub-nav sub-nav :selected selected))
(~sx-header-row
:nav (~sx-main-nav :section section)
:oob true)
(~root-header-auto true)))
(defcomp ~sx-section-layout-mobile (&key section sub-label sub-href sub-nav)
(<>
(when sub-nav
(~mobile-menu-section
:label (or sub-label section) :href sub-href :level 2 :colour "violet"
:items sub-nav))
(~mobile-menu-section
:label "sx" :href "/" :level 1 :colour "violet"
:items (~sx-main-nav :section section))
(~root-mobile-auto)))
(defcomp ~sx-doc (&key path &rest children)
(let ((nav-state (resolve-nav-path sx-nav-tree (or path "/"))))
(<>
(div :id "sx-nav" :class "mb-6"
(~sx-header)
;; Sibling arrows for EVERY level in the trail
(map (fn (crumb)
(~nav-sibling-row
:node (get crumb "node")
:siblings (get crumb "siblings")))
(get nav-state "trail"))
;; Children as button links
(when (get nav-state "children")
(~nav-children :items (get nav-state "children"))))
;; Page content follows
children)))
;; ---------------------------------------------------------------------------
;; Standalone layouts (no root header, no auth — for sx-web.org)
;; SX docs layouts root header only (nav is in page content via ~sx-doc)
;; ---------------------------------------------------------------------------
(defcomp ~sx-standalone-layout-full (&key section)
(~sx-header-row :nav (~sx-main-nav :section section)))
(defcomp ~sx-docs-layout-full ()
(~root-header-auto))
(defcomp ~sx-standalone-layout-oob (&key section)
(<> (~sx-header-row
:nav (~sx-main-nav :section section)
:oob true)
(~clear-oob-div :id "sx-header-child")))
;; OOB: just update root header. Nav is in content via ~sx-doc.
(defcomp ~sx-docs-layout-oob ()
(~root-header-auto true))
(defcomp ~sx-standalone-layout-mobile (&key section)
(~mobile-menu-section
:label "sx" :href "/" :level 1 :colour "violet"
:items (~sx-main-nav :section section)))
;; Mobile: just root mobile nav. In-page nav is in content.
(defcomp ~sx-docs-layout-mobile ()
(~root-mobile-auto))
(defcomp ~sx-standalone-section-layout-full (&key section sub-label sub-href sub-nav selected)
(~sx-header-row
:nav (~sx-main-nav :section section)
:child (~sx-sub-row :sub-label sub-label :sub-href sub-href
:sub-nav sub-nav :selected selected)))
;; ---------------------------------------------------------------------------
;; Standalone layouts (no root header — for sx-web.org)
;; ---------------------------------------------------------------------------
(defcomp ~sx-standalone-section-layout-oob (&key section sub-label sub-href sub-nav selected)
(<> (~oob-header-sx :parent-id "sx-header-child"
:row (~sx-sub-row :sub-label sub-label :sub-href sub-href
:sub-nav sub-nav :selected selected))
(~sx-header-row
:nav (~sx-main-nav :section section)
:oob true)))
(defcomp ~sx-standalone-docs-layout-full ()
nil)
(defcomp ~sx-standalone-section-layout-mobile (&key section sub-label sub-href sub-nav)
(<>
(when sub-nav
(~mobile-menu-section
:label (or sub-label section) :href sub-href :level 2 :colour "violet"
:items sub-nav))
(~mobile-menu-section
:label "sx" :href "/" :level 1 :colour "violet"
:items (~sx-main-nav :section section))))
;; Standalone OOB: nothing needed — nav is in content.
(defcomp ~sx-standalone-docs-layout-oob ()
nil)
;; Standalone mobile: nothing — nav is in content.
(defcomp ~sx-standalone-docs-layout-mobile ()
nil)

View File

@@ -169,6 +169,14 @@
: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.")
(dict :label "Environment Images" :href "/plans/environment-images"
:summary "Serialize evaluated environments as content-addressed images. Spec CID → image CID → every endpoint is fully executable and verifiable.")
(dict :label "Runtime Slicing" :href "/plans/runtime-slicing"
:summary "Tier the client runtime by need: L0 hypermedia (~5KB), L1 DOM ops (~8KB), L2 islands (~15KB), L3 full eval (~44KB). Sliced by slice.sx, translated by js.sx.")
(dict :label "Typed SX" :href "/plans/typed-sx"
:summary "Gradual type system for SX. Optional annotations, checked at registration time, zero runtime cost. types.sx — specced, bootstrapped, catches composition errors.")
(dict :label "Nav Redesign" :href "/plans/nav-redesign"
:summary "Replace menu bars with vertical breadcrumb navigation. Logo → section → page, arrows for siblings, children below. No dropdowns, no hamburger, infinite depth.")
(dict :label "Fragment Protocol" :href "/plans/fragment-protocol"
:summary "Structured sexp request/response for cross-service component transfer.")
(dict :label "Glue Decoupling" :href "/plans/glue-decoupling"
@@ -178,7 +186,9 @@
(dict :label "SX CI Pipeline" :href "/plans/sx-ci"
:summary "Build, test, and deploy in s-expressions — CI pipelines as SX components.")
(dict :label "Live Streaming" :href "/plans/live-streaming"
:summary "SSE and WebSocket transports for re-resolving suspense slots after initial page load — live data, real-time collaboration.")))
:summary "SSE and WebSocket transports for re-resolving suspense slots after initial page load — live data, real-time collaboration.")
(dict :label "sx-web Platform" :href "/plans/sx-web-platform"
:summary "sx-web.org as online development platform — embedded Claude Code, IPFS storage, sx-activity publishing, sx-ci testing. Author, stage, test, deploy from the browser.")))
(define reactive-islands-nav-items (list
(dict :label "Overview" :href "/reactive-islands/"
@@ -281,6 +291,7 @@
;; Generic section nav — builds nav links from a list of items.
;; Replaces _nav_items_sx() and all section-specific nav builders in utils.py.
;; KEPT for backward compat with other apps — SX docs uses ~nav-list instead.
(defcomp ~section-nav (&key items current)
(<> (map (fn (item)
(~nav-link
@@ -289,3 +300,88 @@
:is-selected (when (= (get item "label") current) "true")
:select-colours "aria-selected:bg-violet-200 aria-selected:text-violet-900"))
items)))
;; ---------------------------------------------------------------------------
;; Nav tree — hierarchical navigation for SX docs
;; ---------------------------------------------------------------------------
(define sx-nav-tree
{:label "sx" :href "/"
:children (list
{:label "Docs" :href "/docs/" :children docs-nav-items}
{:label "CSSX" :href "/cssx/" :children cssx-nav-items}
{:label "Reference" :href "/reference/" :children reference-nav-items}
{:label "Protocols" :href "/protocols/" :children protocols-nav-items}
{:label "Examples" :href "/examples/" :children examples-nav-items}
{:label "Essays" :href "/essays/" :children essays-nav-items}
{:label "Philosophy" :href "/philosophy/" :children philosophy-nav-items}
{:label "Specs" :href "/specs/" :children specs-nav-items}
{:label "Bootstrappers" :href "/bootstrappers/" :children bootstrappers-nav-items}
{:label "Testing" :href "/testing/" :children testing-nav-items}
{:label "Isomorphism" :href "/isomorphism/" :children isomorphism-nav-items}
{:label "Plans" :href "/plans/" :children plans-nav-items}
{:label "Reactive Islands" :href "/reactive-islands/" :children reactive-islands-nav-items})})
;; Resolve a URL path to a nav trail + children.
;; Returns {:trail [{:node N :siblings S} ...] :children [...] :depth N}
;; Trail is from outermost selected ancestor to deepest.
(define resolve-nav-path
(fn (tree path)
(let ((trail (list)))
(define walk
(fn (node)
(let ((children (get node "children")))
(when children
(let ((match (find-nav-match children path)))
(when match
(append! trail {:node match :siblings children})
;; Only recurse deeper if this wasn't an exact match
;; (exact match = we found our target, stop)
(when (not (= (get match "href") path))
(walk match))))))))
(walk tree)
(let ((depth (len trail)))
(if (= depth 0)
{:trail trail :children (get tree "children") :depth 0}
(let ((deepest (nth trail (- depth 1))))
{:trail trail
:children (get (get deepest "node") "children")
:depth depth}))))))
;; Find a nav item whose href matches the given path (or path prefix).
(define find-nav-match
(fn (items path)
;; Exact match first
(or (some (fn (item)
(when (= (get item "href") path) item))
items)
;; Prefix match: path starts with item href (for /plans/typed-sx matching /plans/)
(some (fn (item)
(let ((href (get item "href")))
(when (and (ends-with? href "/")
(starts-with? path href))
item)))
items)
;; Path contains section: /plans/typed-sx matches section with /plans/ children
(some (fn (item)
(let ((children (get item "children")))
(when children
(when (some (fn (child)
(= (get child "href") path))
children)
item))))
items))))
;; Find the index of a nav item in a list by matching href.
(define find-nav-index
(fn (items node)
(let ((target-href (get node "href"))
(count (len items)))
(define find-loop
(fn (i)
(if (>= i count)
0
(if (= (get (nth items i) "href") target-href)
i
(find-loop (+ i 1))))))
(find-loop 0))))

View File

@@ -0,0 +1,304 @@
;; ---------------------------------------------------------------------------
;; Content-Addressed Environment Images
;; ---------------------------------------------------------------------------
(defcomp ~plan-environment-images-content ()
(~doc-page :title "Content-Addressed Environment Images"
(~doc-section :title "The Idea" :id "idea"
(p "Every served SX endpoint should point back to its spec. The spec CIDs identify the exact evaluator, renderer, parser, and primitives that produced the output. This makes every endpoint " (strong "fully executable") " — anyone with the CIDs can independently reproduce the result.")
(p "But evaluating spec files from source on every cold start is wasteful. The specs are pure — same source always produces the same evaluated environment. So we can serialize the " (em "evaluated") " environment as a content-addressed image: all defcomps, defmacros, bound symbols, resolved closures frozen into a single artifact. The image CID is a function of its contents. Load the image, skip evaluation, get the same result.")
(p "The chain becomes:")
(ol :class "list-decimal pl-5 text-stone-700 space-y-2"
(li (strong "Served page") " → CID of the spec that defines its semantics")
(li (strong "Spec CID") " → the evaluator, renderer, parser, primitives that any conforming host can execute")
(li (strong "Image CID") " → the pre-evaluated environment, a cache of (2) that any conforming host can deserialize"))
(p "The spec is the truth. The image is a verified cache. The bootstrapper that compiled the spec for a particular host is an implementation detail — irrelevant to the content address.")
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mt-4"
(p :class "text-violet-900 font-medium" "Prior art")
(p :class "text-violet-800" (a :href "https://github.com/KinaKnowledge/juno-lang" :class "underline" "Juno") " — a self-hosted Lisp-to-JS compiler — implements image persistence: serialize a running environment, restore it later, even bundle it as a standalone HTML document. Their Seedling IDE saves/restores entire development sessions as images. SX can do this more rigorously because our images are content-addressed (Juno's are not) and our components are boundary-enforced pure (Juno's are not).")))
;; -----------------------------------------------------------------------
;; What gets serialized
;; -----------------------------------------------------------------------
(~doc-section :title "What Gets Serialized" :id "what"
(p "An environment image is a snapshot of everything produced by evaluating the spec files. Not the source — the result.")
(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" "Category")
(th :class "px-3 py-2 font-medium text-stone-600" "Contents")
(th :class "px-3 py-2 font-medium text-stone-600" "Source")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Components")
(td :class "px-3 py-2 text-stone-700" "All " (code "defcomp") " definitions — name, params, body AST, closure bindings, CID, deps, css_classes")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "Service .sx files + shared/sx/templates/"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Macros")
(td :class "px-3 py-2 text-stone-700" "All " (code "defmacro") " definitions — name, params, body AST, closure")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "Spec files"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Bindings")
(td :class "px-3 py-2 text-stone-700" "Top-level " (code "define") " values — constants, lookup tables, configuration")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "Spec files + service .sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Primitives")
(td :class "px-3 py-2 text-stone-700" "Registry of pure primitive names (not implementations — those are host-specific)")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "primitives.sx, boundary.sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Spec provenance")
(td :class "px-3 py-2 text-stone-700" "CIDs of the spec files that produced this image")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "eval.sx, render.sx, parser.sx, ...")))))
(p "Notably " (strong "absent") " from the image:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "IO primitive implementations") " — these are host-specific. The image records their " (em "names") " (for boundary enforcement) but not their code.")
(li (strong "Page helpers") " — same reason. " (code "fetch-data") ", " (code "app-url") " etc. are registered by the host app at startup.")
(li (strong "Runtime state") " — no request context, no DB connections, no session data. The image is a pure function's result, not a running process snapshot.")))
;; -----------------------------------------------------------------------
;; Image format
;; -----------------------------------------------------------------------
(~doc-section :title "Image Format" :id "format"
(p "The image is itself an s-expression — the same format the spec is written in. This means the image can be parsed by the same parser, inspected by the same tools, and content-addressed by the same canonical serializer.")
(~doc-code :code (highlight "(sx-image\n :version 1\n :spec-cids {:eval \"bafy...eval\"\n :render \"bafy...render\"\n :parser \"bafy...parser\"\n :primitives \"bafy...prims\"\n :boundary \"bafy...boundary\"\n :signals \"bafy...signals\"}\n\n :components (\n (defcomp ~card (&key title subtitle &rest children)\n (div :class \"card\" (h2 title) (when subtitle (p subtitle)) children))\n (defcomp ~nav (&key items current)\n (nav :class \"nav\" (map (fn (item) ...) items)))\n ;; ... all registered components\n )\n\n :macros (\n (defmacro when (test &rest body)\n (list 'if test (cons 'begin body) nil))\n ;; ... all macros\n )\n\n :bindings (\n (define void-elements (list \"area\" \"base\" \"br\" \"col\" ...))\n (define boolean-attrs (list \"checked\" \"disabled\" ...))\n ;; ... all top-level defines\n )\n\n :primitive-names (\"str\" \"+\" \"-\" \"*\" \"/\" \"=\" \"<\" \">\" ...)\n :io-names (\"fetch-data\" \"call-action\" \"app-url\" ...))" "lisp"))
(p "The " (code ":spec-cids") " field is the key. It links this image back to the exact spec that produced it. Anyone can verify the image by:")
(ol :class "list-decimal pl-5 text-stone-700 space-y-1"
(li "Fetch the spec files by CID")
(li "Evaluate them with a conforming evaluator")
(li "Serialize the resulting environment")
(li "Compare — it must produce the same image")))
;; -----------------------------------------------------------------------
;; Image CID
;; -----------------------------------------------------------------------
(~doc-section :title "Image CID" :id "image-cid"
(p "The image CID is computed by canonical-serializing the entire " (code "(sx-image ...)") " form and hashing it. Same process as component CIDs, just applied to the whole environment.")
(p "The relationship between spec CIDs and image CID is deterministic:")
(~doc-code :code (highlight ";; The image CID is a pure function of the spec CIDs\n;; (assuming a deterministic evaluator, which SX guarantees)\n(define image-cid-from-specs\n (fn (spec-cids)\n ;; 1. Fetch each spec file by CID\n ;; 2. Evaluate all specs in a fresh environment\n ;; 3. Extract components, macros, bindings\n ;; 4. Build (sx-image ...) form\n ;; 5. Canonical serialize\n ;; 6. Hash → CID\n ))" "lisp"))
(p "This means you can compute the expected image CID from the spec CIDs " (em "without") " having the image. If someone hands you an image claiming to be from spec " (code "bafy...eval") ", you can verify it by re-evaluating the spec and comparing CIDs. The image is a verifiable cache.")
(p "In practice, you'd only do this verification once per spec version. After that, the image CID is trusted by content-addressing — same bytes, same hash, forever."))
;; -----------------------------------------------------------------------
;; Endpoint provenance
;; -----------------------------------------------------------------------
(~doc-section :title "Endpoint Provenance" :id "provenance"
(p "Every served page gains a provenance header linking it to the spec that rendered it:")
(~doc-code :code (highlight "HTTP/1.1 200 OK\nContent-Type: text/html\nSX-Spec: bafy...eval,bafy...render,bafy...parser,bafy...prims\nSX-Image: bafy...image\nSX-Page-Components: ~card:bafy...card,~nav:bafy...nav" "http"))
(p "Three levels of verification:")
(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" "Level")
(th :class "px-3 py-2 font-medium text-stone-600" "What you verify")
(th :class "px-3 py-2 font-medium text-stone-600" "Trust assumption")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "Component")
(td :class "px-3 py-2 text-stone-700" "Fetch " (code "~card") " by CID, verify hash")
(td :class "px-3 py-2 text-stone-600" "Trust the evaluator"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "Image")
(td :class "px-3 py-2 text-stone-700" "Fetch image by CID, deserialize, re-render page")
(td :class "px-3 py-2 text-stone-600" "Trust the image producer"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "Spec")
(td :class "px-3 py-2 text-stone-700" "Fetch specs by CID, re-evaluate, compare image CID")
(td :class "px-3 py-2 text-stone-600" "Trust only the hash function")))))
(p "Level 3 is the nuclear option — full independent verification from source. It's expensive but proves the entire chain. Most consumers will operate at level 1 (component verification) or level 2 (image verification)."))
;; -----------------------------------------------------------------------
;; Cold start optimization
;; -----------------------------------------------------------------------
(~doc-section :title "Cold Start: Images as Cache" :id "cold-start"
(p "The practical motivation: evaluating all spec files + service components on every server restart is slow. An image eliminates this.")
(~doc-subsection :title "Server Startup"
(ol :class "list-decimal pl-5 text-stone-700 space-y-2"
(li "Check if a cached image exists for the current spec CIDs")
(li "If yes: deserialize the image (fast — parsing a single file, no evaluation)")
(li "If no: evaluate spec files from source, build image, cache it")
(li "Register IO primitives and page helpers (host-specific, not in image)")
(li "Ready to serve"))
(~doc-code :code (highlight "(define load-environment\n (fn (spec-cids image-cache-dir)\n (let ((expected-image-cid (image-cid-for-specs spec-cids))\n (cached-path (str image-cache-dir \"/\" expected-image-cid \".sx\")))\n (if (file-exists? cached-path)\n ;; Fast path: deserialize\n (let ((image (parse (read-file cached-path))))\n (if (= (verify-image-cid image) expected-image-cid)\n (deserialize-image image)\n ;; Cache corrupted — rebuild\n (build-and-cache-image spec-cids image-cache-dir)))\n ;; Cold path: evaluate from source\n (build-and-cache-image spec-cids image-cache-dir)))))" "lisp")))
(~doc-subsection :title "Client Boot"
(p "The client already caches component definitions in localStorage keyed by bundle hash. Images extend this: cache the entire evaluated environment, not just individual components.")
(ol :class "list-decimal pl-5 text-stone-700 space-y-1"
(li "Page ships " (code "SX-Image") " header with image CID")
(li "Client checks localStorage for " (code "sx-image:{cid}"))
(li "If hit: deserialize and boot (no component-by-component parsing)")
(li "If miss: fetch image from origin or IPFS, deserialize, cache")
(li "Same cache-forever semantics as component CIDs — content can't be stale"))
(p "First visit to any SX-powered site: one image fetch. Every subsequent visit: instant boot from cache. Cross-site: if two sites share the same spec CIDs, the image is shared too.")))
;; -----------------------------------------------------------------------
;; Standalone HTML
;; -----------------------------------------------------------------------
(~doc-section :title "Standalone HTML Bundles" :id "standalone"
(p "An image can be inlined into a single HTML document, producing a fully self-contained application with no server dependency:")
(~doc-code :code (highlight "<!doctype html>\n<html>\n<head>\n <script type=\"text/sx-image\">\n (sx-image\n :version 1\n :spec-cids {...}\n :components (...)\n :macros (...)\n :bindings (...))\n </script>\n <script type=\"text/sx-pages\">\n (defpage home :path \"/\" :content (~home-page))\n </script>\n <script src=\"sx-ref.js\"></script>\n</head>\n<body>\n <div id=\"app\"></div>\n <script>\n // Deserialize image, register components, render\n Sx.bootFromImage(document.querySelector('[type=\"text/sx-image\"]'))\n </script>\n</body>\n</html>" "html"))
(p "This document is its own CID. Pin it to IPFS and it's a permanent, executable, verifiable application. No origin server, no CDN, no DNS. The content network is the deployment target.")
(div :class "rounded border border-amber-200 bg-amber-50 p-4 mt-4"
(p :class "text-amber-900 font-medium" "Juno comparison")
(p :class "text-amber-800" "Juno's Seedling IDE already does this — export a running environment as a standalone HTML file. But their images are opaque JavaScript blobs serialized by the runtime. SX images are " (strong "s-expressions") " — parseable, inspectable, content-addressable. You can diff two SX images and see exactly what changed. You can extract a single component from an image by CID. You can merge images from different sources by composing their component lists. The format IS the tooling.")))
;; -----------------------------------------------------------------------
;; Namespace scoping
;; -----------------------------------------------------------------------
(~doc-section :title "Namespaced Environments" :id "namespaces"
(p "As the component library grows across services, a flat environment risks name collisions. Images provide a natural boundary for namespace scoping.")
(~doc-code :code (highlight "(sx-image\n :version 1\n :namespace \"market\"\n :spec-cids {...}\n :extends \"bafy...shared-image\" ;; inherits shared components\n :components (\n ;; market-specific components\n (defcomp ~product-card ...)\n (defcomp ~price-tag ...)\n ))" "lisp"))
(p "Resolution: " (code "market/~product-card") " → look in market image first, then fall through to the shared image (via " (code ":extends") "). Each service produces its own image, layered on top of the shared base.")
(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" "Image")
(th :class "px-3 py-2 font-medium text-stone-600" "Contents")
(th :class "px-3 py-2 font-medium text-stone-600" "Extends")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "bafy...shared")
(td :class "px-3 py-2 text-stone-700" "~card, ~nav, ~section-nav, ~doc-page, ~doc-code — shared components from " (code "shared/sx/templates/"))
(td :class "px-3 py-2 text-stone-600" "None (root)"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "bafy...blog")
(td :class "px-3 py-2 text-stone-700" "~post-card, ~post-body, ~tag-list — blog-specific from " (code "blog/sx/"))
(td :class "px-3 py-2 text-stone-600" "bafy...shared"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "bafy...market")
(td :class "px-3 py-2 text-stone-700" "~product-card, ~price-tag, ~cart-mini — market-specific from " (code "market/sx/"))
(td :class "px-3 py-2 text-stone-600" "bafy...shared"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "bafy...sx-docs")
(td :class "px-3 py-2 text-stone-700" "~doc-section, ~example-source, plans, essays — sx docs from " (code "sx/sx/"))
(td :class "px-3 py-2 text-stone-600" "bafy...shared")))))
(p "The " (code ":extends") " field is a CID, not a name. Image composition is content-addressed: changing the shared image produces a new shared CID, which invalidates all service images that extend it. Exactly the right cascading behavior."))
;; -----------------------------------------------------------------------
;; Spec → Image → Page chain
;; -----------------------------------------------------------------------
(~doc-section :title "The Verification Chain" :id "chain"
(p "The full provenance chain from served page back to source:")
(~doc-code :code (highlight ";; 1. Page served with provenance headers\n;;\n;; SX-Spec: bafy...eval,bafy...render,...\n;; SX-Image: bafy...market-image\n;; SX-Page: (defpage product :path \"/products/<slug>\" ...)\n;;\n;; 2. Verify image → spec\n;;\n;; Fetch specs by CID → evaluate → build image → compare CID\n;; If match: the image was correctly produced from these specs\n;;\n;; 3. Verify page → image\n;;\n;; Deserialize image → evaluate page defn → render\n;; If output matches served HTML: the page was correctly rendered\n;;\n;; 4. Trust chain terminates at the spec\n;;\n;; The spec is self-hosting (eval.sx evaluates itself)\n;; The spec's CID is its identity\n;; No external trust anchor needed beyond the hash function" "lisp"))
(p "This is stronger than code signing. Code signing says " (em "\"this entity vouches for this binary.\"") " Content addressing says " (em "\"this binary is the deterministic output of this source.\"") " No entity needed. No certificate authority. No revocation lists. Just math."))
;; -----------------------------------------------------------------------
;; Implementation phases
;; -----------------------------------------------------------------------
(~doc-section :title "Implementation" :id "implementation"
(~doc-subsection :title "Phase 1: Image Serialization"
(p "Spec module " (code "image.sx") " — serialize and deserialize evaluated environments.")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code "serialize-environment") " — walk the env, extract components/macros/bindings, produce " (code "(sx-image ...)") " form")
(li (code "deserialize-image") " — parse image, reconstitute components/macros/bindings into env")
(li (code "image-cid") " — canonical-serialize the image form, hash → CID")
(li "Must handle closure serialization — component closures reference other components by name, which must be re-linked on deserialization")))
(~doc-subsection :title "Phase 2: Spec Provenance"
(p "Compute CIDs for all spec files at startup. Attach to environment metadata.")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Hash each spec file's canonical source at load time")
(li "Store in env metadata as " (code ":spec-cids") " dict")
(li "Include in image serialization")))
(~doc-subsection :title "Phase 3: Server-Side Caching"
(p "Cache images on disk keyed by spec CIDs. Skip evaluation on warm restart.")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "On startup: compute spec CIDs → derive expected image CID → check cache")
(li "Cache hit: deserialize (parse only, no eval)")
(li "Cache miss: evaluate specs, serialize image, write cache")
(li "Any spec file change → new spec CID → new image CID → cache miss → rebuild")))
(~doc-subsection :title "Phase 4: Client Images"
(p "Ship image CID in response headers. Client caches full env in localStorage.")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code "SX-Image") " response header with image CID")
(li "Client boot checks localStorage for cached image")
(li "Cache hit: deserialize, skip per-component fetch/parse")
(li "Cache miss: fetch image (single request), deserialize, cache")))
(~doc-subsection :title "Phase 5: Standalone Export"
(p "Generate self-contained HTML with inlined image. Pin to IPFS.")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Inline " (code "(sx-image ...)") " as " (code "<script type=\"text/sx-image\">"))
(li "Inline page definitions as " (code "<script type=\"text/sx-pages\">"))
(li "Include sx-ref.js (or link to its CID)")
(li "The resulting HTML is a complete application — pin its CID to IPFS")))
(~doc-subsection :title "Phase 6: Namespaced Images"
(p "Per-service images with " (code ":extends") " for layered composition.")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Shared image: components from " (code "shared/sx/templates/"))
(li "Service images: extend shared, add service-specific components")
(li "Resolution: service image → shared image → primitives")
(li "Image merge: combine two images with conflict detection"))))
;; -----------------------------------------------------------------------
;; Dependencies
;; -----------------------------------------------------------------------
(~doc-section :title "Dependencies" :id "dependencies"
(p "What must exist before this plan can execute:")
(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" "Dependency")
(th :class "px-3 py-2 font-medium text-stone-600" "Status")
(th :class "px-3 py-2 font-medium text-stone-600" "Plan")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Canonical serialization")
(td :class "px-3 py-2 text-stone-700" (span :class "text-red-700 font-medium" "Not started"))
(td :class "px-3 py-2" (a :href "/plans/content-addressed-components" :class "text-violet-700 underline" "Content-Addressed Components") " Phase 1"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Component CIDs")
(td :class "px-3 py-2 text-stone-700" (span :class "text-red-700 font-medium" "Not started"))
(td :class "px-3 py-2" (a :href "/plans/content-addressed-components" :class "text-violet-700 underline" "Content-Addressed Components") " Phase 2"))
(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"))
(td :class "px-3 py-2 text-stone-600" "deps.sx + boundary.py"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Self-hosting spec")
(td :class "px-3 py-2 text-stone-700" (span :class "text-green-700 font-medium" "Complete"))
(td :class "px-3 py-2 text-stone-600" "eval.sx, render.sx, parser.sx, ..."))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Self-hosting bootstrappers")
(td :class "px-3 py-2 text-stone-700" (span :class "text-green-700 font-medium" "Complete"))
(td :class "px-3 py-2 text-stone-600" "py.sx, js.sx — G0 == G1"))
(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"))
(td :class "px-3 py-2 text-stone-600" "artdag L1/L2, IPFSPin model")))))
(div :class "rounded border border-amber-200 bg-amber-50 p-3 mt-2"
(p :class "text-amber-800 text-sm" (strong "Builds on: ") (a :href "/plans/content-addressed-components" :class "underline" "Content-Addressed Components") " (canonical serialization + CIDs), " (a :href "/plans/self-hosting-bootstrapper" :class "underline" "self-hosting bootstrappers") " (spec-first architecture). " (strong "Enables: ") (a :href "/plans/sx-activity" :class "underline" "SX-Activity") " (serverless applications on IPFS).")))))

245
sx/sx/plans/nav-redesign.sx Normal file
View File

@@ -0,0 +1,245 @@
;; ---------------------------------------------------------------------------
;; Navigation Redesign — SX Docs
;; ---------------------------------------------------------------------------
(defcomp ~plan-nav-redesign-content ()
(~doc-page :title "Navigation Redesign"
(~doc-section :title "The Problem" :id "problem"
(p "The current navigation is a horizontal menu bar system: root bar, sx bar, sub-section bar. 13 top-level sections crammed into a scrolling horizontal row. Hover to see dropdowns. Click a section, get a second bar underneath. Click a page, get a third bar. Three stacked bars eating vertical space on every page.")
(p "It's a conventional web pattern and it's bad for this site. SX docs has a deep hierarchy — sections contain subsections contain pages. Horizontal bars can't express depth. They flatten everything into one level and hide the rest behind hover states that don't work on mobile, that obscure content, that require spatial memory of where things are.")
(p "The new nav is vertical, hierarchical, and infinite. No dropdowns. No menu bars. Just a centered breadcrumb trail that expands downward as you drill in."))
;; -----------------------------------------------------------------------
;; Design
;; -----------------------------------------------------------------------
(~doc-section :title "Design" :id "design"
(~doc-subsection :title "Structure"
(p "One vertical column, centered. Each level is a row.")
(~doc-code :code (highlight ";; Home (nothing selected)\n;;\n;; [ sx ]\n;;\n;; Docs CSSX Reference Protocols Examples\n;; Essays Philosophy Specs Bootstrappers\n;; Testing Isomorphism Plans Reactive Islands\n\n\n;; Section selected (e.g. Plans)\n;;\n;; [ sx ]\n;;\n;; < Plans >\n;;\n;; Status Reader Macros Theorem Prover\n;; Self-Hosting JS Bootstrapper SX-Activity\n;; Predictive Prefetching Content-Addressed\n;; Environment Images Runtime Slicing Typed SX\n;; Fragment Protocol ...\n\n\n;; Page selected (e.g. Typed SX under Plans)\n;;\n;; [ sx ]\n;;\n;; < Plans >\n;;\n;; < Typed SX >\n;;\n;; [ page content here ]" "lisp")))
(~doc-subsection :title "Rules"
(ol :class "list-decimal pl-5 text-stone-700 space-y-2"
(li (strong "Logo at top, centered.") " Always visible. Click = home. The only fixed element.")
(li (strong "Level 1: section list.") " Shown on home page as a wrapped, centered list of links. This is the full menu — no hiding, no hamburger.")
(li (strong "When a section is selected:") " Section name replaces the list. Left arrow and right arrow for sibling navigation (previous/next section). The section's children appear as a new list below.")
(li (strong "When a child is selected:") " Same pattern — child name replaces the list, arrows for siblings, sub-children appear below. Recurse ad infinitum.")
(li (strong "Breadcrumb trail.") " Each selected level stays visible as a single row above the current level. The trail is: logo → section → subsection → page. Each row has arrows. Click any ancestor to navigate up.")
(li (strong "No dropdowns.") " Never. Hover does nothing special. The hierarchy is always visible in the breadcrumb trail.")
(li (strong "No hamburger menu.") " The nav IS the page on the home/index views. On content pages, the breadcrumb trail is compact enough to show without hiding.")
(li (strong "Responsive by default.") " Vertical + centered + wrapped = works at any width. No breakpoint-specific layout needed."))))
;; -----------------------------------------------------------------------
;; Visual language
;; -----------------------------------------------------------------------
(~doc-section :title "Visual Language" :id "visual"
(~doc-subsection :title "Levels"
(p "Each level has decreasing visual weight:")
(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" "Level")
(th :class "px-3 py-2 font-medium text-stone-600" "Selected state")
(th :class "px-3 py-2 font-medium text-stone-600" "List state")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "Logo")
(td :class "px-3 py-2 text-stone-700" "Large, violet, always visible")
(td :class "px-3 py-2 text-stone-600" "—"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "Section")
(td :class "px-3 py-2 text-stone-700" "Medium text, violet-700, arrows")
(td :class "px-3 py-2 text-stone-600" "Medium text, stone-600, wrapped inline"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "Subsection")
(td :class "px-3 py-2 text-stone-700" "Smaller text, violet-600, arrows")
(td :class "px-3 py-2 text-stone-600" "Small text, stone-500, wrapped inline"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "Level 3+")
(td :class "px-3 py-2 text-stone-700" "Same as subsection")
(td :class "px-3 py-2 text-stone-600" "Same as subsection"))))))
(~doc-subsection :title "Arrows"
(p "Left and right arrows are inline with the selected item name. They navigate to the previous/next sibling in the current list. Keyboard accessible: left/right arrow keys when the row is focused.")
(~doc-code :code (highlight ";; Arrow rendering\n;;\n;; < Plans >\n;;\n;; < is a link to /plans/content-addressed-components\n;; (the previous sibling in plans-nav-items)\n;; > is a link to /plans/fragment-protocol\n;; (the next sibling)\n;; \"Plans\" is a link to /plans/ (the section index)\n;;\n;; At the edges, the arrow wraps:\n;; first item: < wraps to last\n;; last item: > wraps to first" "lisp")))
(~doc-subsection :title "Transitions"
(p "Selecting an item: the list fades/collapses, the selected item moves to breadcrumb position, children appear below. This is an L0 morph — the server renders the new state, the client morphs. No JS animation library needed, just CSS transitions on the morph targets.")
(p "Going up: click an ancestor in the breadcrumb. Its children (the level below) expand back into a list. Reverse of the drill-down.")))
;; -----------------------------------------------------------------------
;; Data model
;; -----------------------------------------------------------------------
(~doc-section :title "Data Model" :id "data"
(p "The current nav data is flat — each section has its own " (code "define") ". The new model is a single tree:")
(~doc-code :code (highlight "(define sx-nav-tree\n {:label \"sx\"\n :href \"/\"\n :children (list\n {:label \"Docs\"\n :href \"/docs/introduction\"\n :children docs-nav-items}\n {:label \"CSSX\"\n :href \"/cssx/\"\n :children cssx-nav-items}\n {:label \"Reference\"\n :href \"/reference/\"\n :children reference-nav-items}\n {:label \"Protocols\"\n :href \"/protocols/wire-format\"\n :children protocols-nav-items}\n {:label \"Examples\"\n :href \"/examples/click-to-load\"\n :children examples-nav-items}\n {:label \"Essays\"\n :href \"/essays/\"\n :children essays-nav-items}\n {:label \"Philosophy\"\n :href \"/philosophy/sx-manifesto\"\n :children philosophy-nav-items}\n {:label \"Specs\"\n :href \"/specs/\"\n :children specs-nav-items}\n {:label \"Bootstrappers\"\n :href \"/bootstrappers/\"\n :children bootstrappers-nav-items}\n {:label \"Testing\"\n :href \"/testing/\"\n :children testing-nav-items}\n {:label \"Isomorphism\"\n :href \"/isomorphism/\"\n :children isomorphism-nav-items}\n {:label \"Plans\"\n :href \"/plans/\"\n :children plans-nav-items}\n {:label \"Reactive Islands\"\n :href \"/reactive-islands/\"\n :children reactive-islands-nav-items})})" "lisp"))
(p "The existing per-section lists (" (code "docs-nav-items") ", " (code "plans-nav-items") ", etc.) remain unchanged — they just become the " (code ":children") " of tree nodes. Sub-sections that have their own sub-items can nest further:")
(~doc-code :code (highlight ";; Future: deeper nesting\n{:label \"Plans\"\n :href \"/plans/\"\n :children (list\n {:label \"Status\" :href \"/plans/status\"}\n {:label \"Bootstrappers\" :href \"/plans/self-hosting-bootstrapper\"\n :children (list\n {:label \"py.sx\" :href \"/plans/self-hosting-bootstrapper\"}\n {:label \"js.sx\" :href \"/plans/js-bootstrapper\"})}\n ;; ...\n )}" "lisp"))
(p "The tree depth is unlimited. The nav component recurses."))
;; -----------------------------------------------------------------------
;; Components
;; -----------------------------------------------------------------------
(~doc-section :title "Components" :id "components"
(p "Three new components replace the entire menu bar system:")
(~doc-subsection :title "~sx-logo"
(~doc-code :code (highlight "(defcomp ~sx-logo ()\n (a :href \"/\"\n :sx-get \"/\" :sx-target \"#main-panel\" :sx-select \"#main-panel\"\n :sx-swap \"outerHTML\" :sx-push-url \"true\"\n :class \"block text-center py-4\"\n (span :class \"text-2xl font-bold text-violet-700\" \"sx\")))" "lisp"))
(p "Always at the top. Always centered. The anchor."))
(~doc-subsection :title "~nav-breadcrumb"
(~doc-code :code (highlight "(defcomp ~nav-breadcrumb (&key path siblings level)\n ;; Renders one breadcrumb row: < Label >\n ;; path = the nav tree node for this level\n ;; siblings = list of sibling nodes (for arrow nav)\n ;; level = depth (controls text size/color)\n (let ((idx (find-index siblings path))\n (prev (nth siblings (mod (- idx 1) (len siblings))))\n (next (nth siblings (mod (+ idx 1) (len siblings)))))\n (div :class (str \"flex items-center justify-center gap-3 py-1\"\n (nav-level-classes level))\n (a :href (get prev \"href\")\n :sx-get (get prev \"href\") :sx-target \"#main-panel\"\n :sx-select \"#main-panel\" :sx-swap \"outerHTML\"\n :sx-push-url \"true\"\n :class \"text-stone-400 hover:text-violet-600\"\n :aria-label \"Previous\"\n \"<\")\n (a :href (get path \"href\")\n :sx-get (get path \"href\") :sx-target \"#main-panel\"\n :sx-select \"#main-panel\" :sx-swap \"outerHTML\"\n :sx-push-url \"true\"\n :class \"font-medium\"\n (get path \"label\"))\n (a :href (get next \"href\")\n :sx-get (get next \"href\") :sx-target \"#main-panel\"\n :sx-select \"#main-panel\" :sx-swap \"outerHTML\"\n :sx-push-url \"true\"\n :class \"text-stone-400 hover:text-violet-600\"\n :aria-label \"Next\"\n \">\"))))" "lisp"))
(p "One row per selected level. Shows the current node with left/right arrows to siblings."))
(~doc-subsection :title "~nav-list"
(~doc-code :code (highlight "(defcomp ~nav-list (&key items level)\n ;; Renders a wrapped list of links — the children of the current level\n (div :class (str \"flex flex-wrap justify-center gap-x-4 gap-y-2 py-2\"\n (nav-level-classes level))\n (map (fn (item)\n (a :href (get item \"href\")\n :sx-get (get item \"href\") :sx-target \"#main-panel\"\n :sx-select \"#main-panel\" :sx-swap \"outerHTML\"\n :sx-push-url \"true\"\n :class \"hover:text-violet-700 transition-colors\"\n (get item \"label\")))\n items)))" "lisp"))
(p "The children of the current level, rendered as a centered wrapped list of plain links."))
(~doc-subsection :title "~sx-nav — the composition"
(~doc-code :code (highlight "(defcomp ~sx-nav (&key trail children-items level)\n ;; trail = list of {node, siblings} from root to current\n ;; children-items = children of the deepest selected node\n ;; level = depth of children\n (div :class \"max-w-3xl mx-auto px-4\"\n ;; Logo\n (~sx-logo)\n ;; Breadcrumb trail (one row per selected ancestor)\n (map-indexed (fn (i crumb)\n (~nav-breadcrumb\n :path (get crumb \"node\")\n :siblings (get crumb \"siblings\")\n :level (+ i 1)))\n trail)\n ;; Children of the deepest selected node\n (when children-items\n (~nav-list :items children-items :level level))))" "lisp"))
(p "That's the entire navigation. Three small components composed. No bars, no dropdowns, no mobile variants.")))
;; -----------------------------------------------------------------------
;; Path resolution
;; -----------------------------------------------------------------------
(~doc-section :title "Path Resolution" :id "resolution"
(p "Given a URL path, compute the breadcrumb trail and children. This is a tree walk:")
(~doc-code :code (highlight "(define resolve-nav-path\n (fn (tree current-href)\n ;; Walk sx-nav-tree, find the node matching current-href,\n ;; return the trail of ancestors + current children.\n ;;\n ;; Returns: {:trail (list of {:node N :siblings S})\n ;; :children (list) or nil\n ;; :depth number}\n ;;\n ;; Example: current-href = \"/plans/typed-sx\"\n ;; → trail: [{:node Plans :siblings [Docs, CSSX, ...]}\n ;; {:node Typed-SX :siblings [Status, Reader-Macros, ...]}]\n ;; → children: nil (leaf node)\n ;; → depth: 2\n (let ((result (walk-nav-tree tree current-href (list))))\n result)))" "lisp"))
(p "This runs server-side (it's a pure function, no IO). The layout component calls it with the current URL and passes the result to " (code "~sx-nav") ". Same pattern as the current " (code "find-current") " but produces a richer result.")
(p "For sx-get navigations (HTMX swaps), the server re-renders the nav with the new path. The morph diffs the old and new nav — breadcrumb rows appear/disappear, the list changes. CSS transitions handle the visual."))
;; -----------------------------------------------------------------------
;; What goes away
;; -----------------------------------------------------------------------
(~doc-section :title "What Goes Away" :id "removal"
(p "Significant deletion:")
(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" "Component")
(th :class "px-3 py-2 font-medium text-stone-600" "File")
(th :class "px-3 py-2 font-medium text-stone-600" "Why")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "~menu-row-sx")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/templates/layout.sx")
(td :class "px-3 py-2 text-stone-600" "Horizontal bar with colour levels — replaced by breadcrumb rows"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "~sx-header-row")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "sx/sx/layouts.sx")
(td :class "px-3 py-2 text-stone-600" "Top menu bar — replaced by logo + breadcrumb"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "~sx-sub-row")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "sx/sx/layouts.sx")
(td :class "px-3 py-2 text-stone-600" "Sub-section bar — replaced by second breadcrumb row"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "~sx-main-nav")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "sx/sx/layouts.sx")
(td :class "px-3 py-2 text-stone-600" "Horizontal nav list — replaced by ~nav-list"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "~section-nav")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "sx/sx/nav-data.sx")
(td :class "px-3 py-2 text-stone-600" "Sub-nav builder — replaced by ~nav-list"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "~nav-link")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/templates/layout.sx")
(td :class "px-3 py-2 text-stone-600" "Complex link with aria-selected + submenu wrapper — replaced by plain a tags"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "~mobile-menu-section")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/templates/layout.sx")
(td :class "px-3 py-2 text-stone-600" "Separate mobile menu — new nav is inherently responsive"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "6 layout variants")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "sx/sx/layouts.sx")
(td :class "px-3 py-2 text-stone-600" "full/oob/mobile × home/section — replaced by one layout with ~sx-nav"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" ".nav-group CSS")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/templates/shell.sx")
(td :class "px-3 py-2 text-stone-600" "Hover submenu CSS — no submenus to hover")))))
(p "The layout variants collapse from 6 (full/oob/mobile × home/section) to 2 (full/oob). No mobile variant needed — the nav is one column, it works everywhere."))
;; -----------------------------------------------------------------------
;; Layout simplification
;; -----------------------------------------------------------------------
(~doc-section :title "Layout Simplification" :id "layout"
(p "The defpage layout declarations currently specify section, sub-label, sub-href, sub-nav, selected — five params to configure two menu bars. The new layout takes one param: the nav trail.")
(~doc-code :code (highlight ";; Current (verbose, configures two bars)\n(defpage plan-page\n :path \"/plans/<slug>\"\n :layout (:sx-section\n :section \"Plans\"\n :sub-label \"Plans\"\n :sub-href \"/plans/\"\n :sub-nav (~section-nav :items plans-nav-items\n :current (find-current plans-nav-items slug))\n :selected (or (find-current plans-nav-items slug) \"\"))\n :content (...))\n\n;; New (one param, nav computed from URL)\n(defpage plan-page\n :path \"/plans/<slug>\"\n :layout (:sx-docs :path (str \"/plans/\" slug))\n :content (...))" "lisp"))
(p "The layout component computes the nav trail internally from the path and the nav tree. No more passing section names, sub-labels, or pre-built nav components through layout params.")
(~doc-code :code (highlight "(defcomp ~sx-docs-layout-full (&key path)\n (let ((nav-state (resolve-nav-path sx-nav-tree path)))\n (<> (~root-header-auto)\n (~sx-nav\n :trail (get nav-state \"trail\")\n :children-items (get nav-state \"children\")\n :level (get nav-state \"depth\")))))\n\n(defcomp ~sx-docs-layout-oob (&key path)\n (let ((nav-state (resolve-nav-path sx-nav-tree path)))\n (<> (~oob-nav\n :trail (get nav-state \"trail\")\n :children-items (get nav-state \"children\")\n :level (get nav-state \"depth\"))\n (~root-header-auto true))))" "lisp"))
(p "Two layout components instead of twelve. Every defpage in docs.sx simplifies from five layout params to one."))
;; -----------------------------------------------------------------------
;; Scope
;; -----------------------------------------------------------------------
(~doc-section :title "Scope" :id "scope"
(div :class "rounded border border-amber-200 bg-amber-50 p-4 mb-4"
(p :class "text-amber-900 font-medium" "SX docs only — for now")
(p :class "text-amber-800" "This redesign applies to the SX docs app (" (code "sx/") "). The other services (blog, market, events, etc.) keep their current navigation. If the pattern proves out, it can migrate to shared infrastructure and replace the root menu system too."))
(p "What changes:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code "sx/sx/nav-data.sx") " — add " (code "sx-nav-tree") " (wraps existing lists, no content change)")
(li (code "sx/sx/layouts.sx") " — rewrite: delete 12 components, add 5 (logo, breadcrumb, list, nav, 2 layouts)")
(li (code "sx/sxc/pages/docs.sx") " — simplify every defpage's " (code ":layout") " declaration")
(li (code "sx/sxc/pages/layouts.py") " — register new layout names")
(li (code "shared/sx/templates/layout.sx") " — no changes needed (shared components untouched, only SX-docs-specific ones change)"))
(p "What doesn't change:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Page content — all " (code "~plan-*-content") ", " (code "~doc-*-content") ", etc. are untouched")
(li "Nav data — all " (code "*-nav-items") " lists are unchanged, just composed into a tree")
(li "Routing — all defpage paths stay the same")
(li "Other services — blog, market, etc. unaffected")))
;; -----------------------------------------------------------------------
;; Implementation
;; -----------------------------------------------------------------------
(~doc-section :title "Implementation" :id "implementation"
(~doc-subsection :title "Phase 1: Nav tree + resolution"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Add " (code "sx-nav-tree") " to " (code "nav-data.sx") " — compose existing " (code "*-nav-items") " lists into a tree")
(li "Write " (code "resolve-nav-path") " — pure function, tree walk, returns trail + children")
(li "Test: given a path, produces the correct breadcrumb trail and child list")))
(~doc-subsection :title "Phase 2: New components"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Write " (code "~sx-logo") ", " (code "~nav-breadcrumb") ", " (code "~nav-list") ", " (code "~sx-nav"))
(li "Write " (code "~sx-docs-layout-full") " and " (code "~sx-docs-layout-oob"))
(li "Register new layout in " (code "layouts.py"))
(li "Test with one defpage first — verify morph transitions work")))
(~doc-subsection :title "Phase 3: Migrate all defpages"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Update every defpage in " (code "docs.sx") " to use " (code ":layout (:sx-docs :path ...)"))
(li "This is mechanical — replace the 5-param layout block with 1-param")))
(~doc-subsection :title "Phase 4: Delete old components"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Delete " (code "~sx-main-nav") ", " (code "~sx-header-row") ", " (code "~sx-sub-row") ", " (code "~section-nav"))
(li "Delete all 12 SX layout variants from " (code "layouts.sx"))
(li "Delete old layout registrations from " (code "layouts.py"))
(li "Remove " (code ".nav-group") " CSS if no other service uses it"))))))

View File

@@ -0,0 +1,282 @@
;; ---------------------------------------------------------------------------
;; Runtime Slicing
;; ---------------------------------------------------------------------------
(defcomp ~plan-runtime-slicing-content ()
(~doc-page :title "Runtime Slicing"
(~doc-section :title "The Problem" :id "problem"
(p "sx-browser.js is the full SX client runtime — evaluator, parser, renderer, engine, morph, signals, routing, orchestration, boot. Every page loads all of it.")
(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" "File")
(th :class "px-3 py-2 font-medium text-stone-600" "Raw")
(th :class "px-3 py-2 font-medium text-stone-600" "Gzipped")
(th :class "px-3 py-2 font-medium text-stone-600" "Min+Gz")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "sx-browser.js")
(td :class "px-3 py-2 text-stone-700" "354KB")
(td :class "px-3 py-2 text-stone-700" "75KB")
(td :class "px-3 py-2 text-stone-700" "44KB"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "sx-ref.js")
(td :class "px-3 py-2 text-stone-700" "244KB")
(td :class "px-3 py-2 text-stone-700" "49KB")
(td :class "px-3 py-2 text-stone-700" "29KB"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "React + ReactDOM")
(td :class "px-3 py-2 text-stone-700" "—")
(td :class "px-3 py-2 text-stone-700" "—")
(td :class "px-3 py-2 text-stone-700" "~5KB + ~40KB")))))
(p "Most pages are L0 (pure hypermedia — server renders, client morphs). They don't need the parser, the evaluator, the full primitive set, signals, or client-side routing. They need morph + swap + trigger dispatch. That's a fraction of the runtime.")
(p "The runtime should be sliceable: each page declares what level it operates at, and the bootstrapper emits only the code that level requires."))
;; -----------------------------------------------------------------------
;; Tiers
;; -----------------------------------------------------------------------
(~doc-section :title "Tiers" :id "tiers"
(p "Four tiers, matching the " (a :href "/reactive-islands/plan" :class "text-violet-700 underline" "reactive islands") " levels:")
(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" "What")
(th :class "px-3 py-2 font-medium text-stone-600" "Modules")
(th :class "px-3 py-2 font-medium text-stone-600" "Target (min+gz)")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "L0 Hypermedia")
(td :class "px-3 py-2 text-stone-700" "Morph, swap, trigger dispatch, history")
(td :class "px-3 py-2 font-mono text-sm text-stone-600" "engine, boot (partial)")
(td :class "px-3 py-2 text-stone-700" "~5KB"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "L1 DOM Ops")
(td :class "px-3 py-2 text-stone-700" "L0 + toggle!, set-attr!, on-event, class-list ops")
(td :class "px-3 py-2 font-mono text-sm text-stone-600" "+ DOM adapter (partial)")
(td :class "px-3 py-2 text-stone-700" "~8KB"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "L2 Islands")
(td :class "px-3 py-2 text-stone-700" "L1 + signals, computed, effect, defisland hydration")
(td :class "px-3 py-2 font-mono text-sm text-stone-600" "+ signals, DOM adapter (full)")
(td :class "px-3 py-2 text-stone-700" "~15KB"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "L3 Full Eval")
(td :class "px-3 py-2 text-stone-700" "L2 + parser, evaluator, all primitives, client routing, component resolution")
(td :class "px-3 py-2 font-mono text-sm text-stone-600" "everything")
(td :class "px-3 py-2 text-stone-700" "~44KB (current)")))))
(p "90% of typical pages are L0. A blog post, a product listing, a checkout form — server renders, client morphs on navigation. The full evaluator only loads for pages that do client-side rendering or have reactive islands.")
(div :class "rounded border border-amber-200 bg-amber-50 p-4 mt-4"
(p :class "text-amber-900 font-medium" "Progressive loading")
(p :class "text-amber-800" "A user navigating an L0 page downloads ~5KB. If they navigate to an L2 page (reactive island), the delta (~10KB) loads on demand. The runtime grows with need, never speculatively.")))
;; -----------------------------------------------------------------------
;; The slicer is SX
;; -----------------------------------------------------------------------
(~doc-section :title "The Slicer is SX" :id "slicer-is-sx"
(p "Per the " (a :href "/plans/self-hosting-bootstrapper" :class "text-violet-700 underline" "self-hosting principle") ", the slicer is not a build tool — it's a spec module. " (code "slice.sx") " analyzes the spec's own dependency graph and determines which defines belong to which tier.")
(p (code "js.sx") " (the self-hosting SX-to-JavaScript translator) already compiles the full spec. Slicing is a filter: " (code "js.sx") " translates only the defines that " (code "slice.sx") " selects for a given tier.")
(~doc-code :code (highlight ";; slice.sx — determine which defines each tier needs\n;;\n;; Input: the full list of defines from all spec files\n;; Output: a filtered list for the requested tier\n\n(define tier-deps\n ;; Which spec modules each tier requires\n {:L0 (list \"engine\" \"boot-partial\")\n :L1 (list \"engine\" \"boot-partial\" \"dom-partial\")\n :L2 (list \"engine\" \"boot-partial\" \"dom-partial\"\n \"signals\" \"dom-island\")\n :L3 (list \"eval\" \"render\" \"parser\"\n \"engine\" \"orchestration\" \"boot\"\n \"dom\" \"signals\" \"router\")})\n\n(define slice-defines\n (fn (tier all-defines)\n ;; 1. Get the module list for this tier\n ;; 2. Walk each define's dependency references\n ;; 3. Include a define only if ALL its deps are\n ;; satisfiable within the tier's module set\n ;; 4. Return the filtered define list\n (let ((modules (get tier-deps tier)))\n (filter\n (fn (d) (tier-satisfies? modules (define-deps d)))\n all-defines))))" "lisp"))
(p "The pipeline becomes:")
(ol :class "list-decimal pl-5 text-stone-700 space-y-2"
(li (code "slice.sx") " analyzes the spec and produces a define list per tier")
(li (code "js.sx") " translates each define list to JavaScript (same translator, different input)")
(li "The bootstrapper wraps each tier's output with its platform interface (the hand-written JS glue)")
(li "Output: one " (code ".js") " file per tier, or a single file with tier markers for runtime splitting"))
(p "Because " (code "js.sx") " is self-hosting, slicing is just function composition: " (code "(js-translate-file (slice-defines :L0 all-defines))") ". No new translator. No new build tool. The same 1,382-line " (code "js.sx") " that produces the full runtime produces every tier."))
;; -----------------------------------------------------------------------
;; Dependency analysis
;; -----------------------------------------------------------------------
(~doc-section :title "Define-Level Dependency Analysis" :id "deps"
(p "The slicer needs to know which defines reference which other defines. This is a simpler version of what " (code "deps.sx") " does for components — but at the define level, not the component level.")
(~doc-code :code (highlight ";; Walk a define's body AST, collect all symbol references\n;; that resolve to other top-level defines.\n;;\n;; (define morph-children\n;; (fn (old-node new-node)\n;; ...uses morph-node, morph-attrs, create-element...))\n;;\n;; → deps: #{morph-node morph-attrs create-element}\n\n(define define-refs\n (fn (body all-define-names)\n ;; Collect all symbols in body that appear in all-define-names\n (let ((refs (make-set)))\n (walk-ast body\n (fn (node)\n (when (and (symbol? node)\n (set-has? all-define-names (symbol-name node)))\n (set-add! refs (symbol-name node)))))\n refs)))" "lisp"))
(p "From these per-define deps, we build the full dependency graph and compute transitive closures. A tier's define set is the transitive closure of its entry points:")
(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" "Entry points")
(th :class "px-3 py-2 font-medium text-stone-600" "Pulls in")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "L0")
(td :class "px-3 py-2 font-mono text-sm text-stone-600" "morph-node, process-swap, dispatch-trigger, push-url")
(td :class "px-3 py-2 text-stone-700" "morph-attrs, morph-children, create-element, extract-swap-config — the morph/swap subgraph"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "L1")
(td :class "px-3 py-2 font-mono text-sm text-stone-600" "L0 + toggle-class!, set-attr!, add-event-listener!")
(td :class "px-3 py-2 text-stone-700" "DOM manipulation helpers — small subgraph"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "L2")
(td :class "px-3 py-2 font-mono text-sm text-stone-600" "L1 + signal, deref, reset!, computed, effect, render-dom-island")
(td :class "px-3 py-2 text-stone-700" "Signal runtime + reactive DOM adapter"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "L3")
(td :class "px-3 py-2 font-mono text-sm text-stone-600" "L2 + eval-expr, sx-parse, render-to-dom, resolve-component-by-cid")
(td :class "px-3 py-2 text-stone-700" "Everything — full evaluator, parser, all ~80 primitives"))))))
;; -----------------------------------------------------------------------
;; Platform interface slicing
;; -----------------------------------------------------------------------
(~doc-section :title "Platform Interface Slicing" :id "platform"
(p "The bootstrapped code (from js.sx) is only half the story. Each define also depends on platform primitives — the hand-written JS glue that js.sx doesn't produce. These live in " (code "bootstrap_js.py") "'s PLATFORM_* blocks.")
(p "The slicer must track platform deps too:")
(~doc-code :code (highlight ";; Platform primitives referenced by tier\n;;\n;; L0 needs: createElement, setAttribute, morphAttrs,\n;; fetch (for sx-get/post), pushState, replaceState\n;;\n;; L1 adds: classList.toggle, addEventListener\n;;\n;; L2 adds: createComment, createTextNode, domRemove,\n;; domChildNodes — the reactive DOM primitives\n;;\n;; L3 adds: everything in PLATFORM_PARSER_JS,\n;; full DOM adapter, async IO bridge\n\n(define platform-deps\n {:L0 (list :dom-morph :fetch :history)\n :L1 (list :dom-morph :fetch :history :dom-events)\n :L2 (list :dom-morph :fetch :history :dom-events\n :dom-reactive :signal-constructors)\n :L3 (list :dom-morph :fetch :history :dom-events\n :dom-reactive :signal-constructors\n :parser :evaluator :async-io)})" "lisp"))
(p "The platform JS blocks in " (code "bootstrap_js.py") " are already organized by adapter (" (code "PLATFORM_DOM_JS") ", " (code "PLATFORM_ENGINE_PURE_JS") ", etc). Slicing further subdivides these into the minimal set each tier needs.")
(p "This subdivision also happens in SX: " (code "slice.sx") " declares which platform blocks each tier requires. " (code "js.sx") " doesn't need to change — it translates defines. The bootstrapper script reads the slice spec and assembles the platform preamble accordingly."))
;; -----------------------------------------------------------------------
;; Progressive loading
;; -----------------------------------------------------------------------
(~doc-section :title "Progressive Loading" :id "progressive"
(p "The simplest approach: one file per tier. The server knows each page's tier (from " (code "defpage") " metadata or component analysis) and serves the right script tag.")
(p "Better: a base file (L0) that all pages load, plus tier deltas loaded on demand.")
(~doc-code :code (highlight ";; Server emits the appropriate script for the page's tier\n;;\n;; L0 page (blog post, product listing):\n;; <script src=\"/static/scripts/sx-L0.js\"></script>\n;;\n;; L2 page (reactive island):\n;; <script src=\"/static/scripts/sx-L0.js\"></script>\n;; <script src=\"/static/scripts/sx-L2-delta.js\"></script>\n;;\n;; Client-side navigation from L0 → L2:\n;; 1. L0 runtime handles the swap\n;; 2. New page declares tier=L2 in response header\n;; 3. L0 runtime loads sx-L2-delta.js dynamically\n;; 4. Island hydration proceeds\n\n(define page-tier\n (fn (page)\n ;; Analyze the page's component tree\n ;; If any component is defisland → L2\n ;; If any component uses on-event/toggle! → L1\n ;; Otherwise → L0\n (cond\n ((page-has-islands? page) :L2)\n ((page-has-dom-ops? page) :L1)\n (true :L0))))" "lisp"))
(~doc-subsection :title "SX-Tier Response Header"
(p "The server includes the page's tier in the response:")
(~doc-code :code (highlight "HTTP/1.1 200 OK\nSX-Tier: L0\nSX-Components: ~card:bafy...,~nav:bafy...\n\n;; or for an island page:\nSX-Tier: L2\nSX-Components: ~counter-island:bafy..." "http"))
(p "On client-side navigation, the engine reads " (code "SX-Tier") " from the response. If the new page requires a higher tier than currently loaded, it fetches the delta script before processing the swap. The delta script registers its additional primitives and the swap proceeds."))
(~doc-subsection :title "Cache Behavior"
(p "Each tier file is content-hashed (like the current " (code "sx_js_hash") " mechanism). Cache-forever semantics. A user who visits any L0 page caches the L0 runtime permanently. If they later visit an L2 page, only the ~10KB delta downloads.")
(p "Combined with " (a :href "/plans/environment-images" :class "text-violet-700 underline" "environment images") ": the image CID includes the tier. An L0 image is smaller than an L3 image — it contains fewer primitives, no parser state, no evaluator. The standalone HTML bundle for an L0 page is tiny.")))
;; -----------------------------------------------------------------------
;; Automatic tier detection
;; -----------------------------------------------------------------------
(~doc-section :title "Automatic Tier Detection" :id "auto-detect"
(p (code "deps.sx") " already classifies components as pure or IO-dependent. Extend it to classify pages by tier:")
(~doc-code :code (highlight ";; Extend deps.sx with tier analysis\n;;\n;; Walk the page's component tree:\n;; - Any defisland → L2 minimum\n;; - Any on-event, toggle!, set-attr! call → L1 minimum \n;; - Any client-eval'd component (SX wire + defcomp) → L3\n;; - Otherwise → L0\n;;\n;; The tier is the MAX of all components' requirements.\n\n(define component-tier\n (fn (comp)\n (cond\n ((island? comp) :L2)\n ((has-dom-ops? (component-body comp)) :L1)\n (true :L0))))\n\n(define page-tier\n (fn (page-def)\n (let ((comp-tiers (map component-tier\n (page-all-components page-def))))\n (max-tier comp-tiers))))" "lisp"))
(p "This runs at registration time (same phase as " (code "compute_all_deps") "). Each " (code "PageDef") " gains a " (code "tier") " field. The server uses it to select the script tag. No manual annotation needed — the tier is derived from what the page actually uses."))
;; -----------------------------------------------------------------------
;; What L0 actually needs
;; -----------------------------------------------------------------------
(~doc-section :title "What L0 Actually Needs" :id "l0-detail"
(p "L0 is the critical tier — it's what most pages load. Every byte matters. Let's be precise about what it contains:")
(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" "Function")
(th :class "px-3 py-2 font-medium text-stone-600" "Purpose")
(th :class "px-3 py-2 font-medium text-stone-600" "Source")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "morph-node")
(td :class "px-3 py-2 text-stone-700" "DOM diffing — update existing DOM from new HTML")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "engine.sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "morph-attrs")
(td :class "px-3 py-2 text-stone-700" "Attribute diffing on a single element")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "engine.sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "morph-children")
(td :class "px-3 py-2 text-stone-700" "Child node reconciliation")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "engine.sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "process-swap")
(td :class "px-3 py-2 text-stone-700" "Apply sx-swap directive (innerHTML, outerHTML, etc)")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "engine.sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "dispatch-trigger")
(td :class "px-3 py-2 text-stone-700" "Process sx-trigger attributes (click, submit, load, etc)")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "engine.sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "sx-fetch")
(td :class "px-3 py-2 text-stone-700" "Make sx-get/sx-post requests, process response")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "orchestration.sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "push-url / replace-url")
(td :class "px-3 py-2 text-stone-700" "History management for sx-push-url")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "engine.sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "boot-triggers")
(td :class "px-3 py-2 text-stone-700" "Scan DOM for sx-* attributes, wire up event listeners")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "boot.sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "resolve-suspense")
(td :class "px-3 py-2 text-stone-700" "Fill in streamed suspense slots")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "boot.sx")))))
(p "That's roughly 20-30 defines from engine.sx + orchestration.sx + boot.sx, plus their transitive deps. No parser, no evaluator, no primitives beyond what those defines call internally. The platform JS is just: fetch wrapper, DOM helpers (createElement, setAttribute, morphing), history API, and event delegation.")
(p "Target: " (strong "~5KB min+gz") " — competitive with htmx (10KB) while being semantically richer (morph-based, not innerHTML-based)."))
;; -----------------------------------------------------------------------
;; Build pipeline
;; -----------------------------------------------------------------------
(~doc-section :title "Build Pipeline" :id "pipeline"
(p "The pipeline uses the same tools that already exist — " (code "js.sx") " for translation, " (code "bootstrap_js.py") " for platform assembly — but feeds them filtered define lists.")
(~doc-code :code (highlight ";; Build all tiers\n;;\n;; 1. Load all spec .sx files\n;; 2. Extract all defines (same as current bootstrap)\n;; 3. Run slice.sx to partition defines by tier\n;; 4. For each tier:\n;; a. js.sx translates the tier's define list\n;; b. Platform assembler wraps with minimal platform JS\n;; c. Output: sx-L{n}.js\n;; 5. Compute deltas: L1-delta = L1 - L0, L2-delta = L2 - L1, etc.\n\n;; The bootstrapper script orchestrates this:\n;;\n;; python bootstrap_js.py --tier L0 -o sx-L0.js\n;; python bootstrap_js.py --tier L1 --delta -o sx-L1-delta.js\n;; python bootstrap_js.py --tier L2 --delta -o sx-L2-delta.js\n;; python bootstrap_js.py -o sx-browser.js # full (L3, backward compat)" "lisp"))
(p "The " (code "--delta") " flag emits only the defines not present in the previous tier. The delta file calls " (code "Sx.extend()") " to register its additions into the already-loaded runtime.")
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mt-4"
(p :class "text-violet-900 font-medium" "Self-hosting all the way")
(p :class "text-violet-800" (code "slice.sx") " is spec. " (code "js.sx") " is spec. The bootstrapper script (" (code "bootstrap_js.py") ") is the thin host-specific glue that reads slice output, calls js.sx via the evaluator, and wraps with platform JS. The slicer could itself be bootstrapped to JavaScript and run in a browser build tool — but that's a future concern.")))
;; -----------------------------------------------------------------------
;; Spec modules
;; -----------------------------------------------------------------------
(~doc-section :title "Spec Modules" :id "spec-modules"
(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" "Module")
(th :class "px-3 py-2 font-medium text-stone-600" "Functions")
(th :class "px-3 py-2 font-medium text-stone-600" "Depends on")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "slice.sx")
(td :class "px-3 py-2 text-stone-700" (code "define-refs") ", " (code "define-dep-graph") ", " (code "slice-defines") ", " (code "tier-entry-points") ", " (code "page-tier") ", " (code "component-tier"))
(td :class "px-3 py-2 text-stone-600" "deps.sx (component analysis), eval.sx (AST walking)"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "js.sx")
(td :class "px-3 py-2 text-stone-700" (code "js-translate-file") " — already exists, unchanged")
(td :class "px-3 py-2 text-stone-600" "eval.sx (runs on evaluator)")))))
(p "One new spec file (" (code "slice.sx") "), one existing translator (" (code "js.sx") "), one modified host script (" (code "bootstrap_js.py") " gains " (code "--tier") " and " (code "--delta") " flags)."))
;; -----------------------------------------------------------------------
;; Relationships
;; -----------------------------------------------------------------------
(~doc-section :title "Relationships" :id "relationships"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (a :href "/plans/environment-images" :class "text-violet-700 underline" "Environment Images") " — tiered images are smaller. An L0 image omits the parser, evaluator, and most primitives.")
(li (a :href "/plans/content-addressed-components" :class "text-violet-700 underline" "Content-Addressed Components") " — component CID resolution is L3-only. L0 pages don't resolve components client-side.")
(li (a :href "/reactive-islands/plan" :class "text-violet-700 underline" "Reactive Islands") " — L2 tier is defined by island presence. The signal runtime is the L1→L2 delta.")
(li (a :href "/plans/isomorphic-architecture" :class "text-violet-700 underline" "Isomorphic Architecture") " — client-side page rendering is L3. Most pages don't need it."))
(div :class "rounded border border-amber-200 bg-amber-50 p-3 mt-2"
(p :class "text-amber-800 text-sm" (strong "Depends on: ") (code "js.sx") " (complete), " (code "deps.sx") " (complete), " (code "bootstrap_js.py") " adapter selection (exists). " (strong "New: ") (code "slice.sx") " spec module.")))))

View File

@@ -0,0 +1,102 @@
;; sx-web.org — Online Development Platform
;; Plan: transform sx-web.org from documentation site into a live development
;; environment where content is authored, tested, and deployed in the browser.
(defcomp ~plan-sx-web-platform-content ()
(~doc-page :title "sx-web.org Development Platform"
(~doc-section :title "Vision" :id "vision"
(p "sx-web.org becomes the development environment for itself. "
"Authors write essays, examples, components, and specs directly in the browser. "
"Changes are planned, staged, tested, and deployed without leaving the site. "
"The documentation is not about the platform — it " (em "is") " the platform.")
(p "Every artifact is content-addressed on IPFS. Every change flows through sx-activity. "
"Every deployment runs through sx-ci. Claude Code is embedded as the AI pair programmer. "
"The entire development lifecycle happens over the web, using the same SX primitives "
"that the platform is built from."))
(~doc-section :title "Architecture" :id "architecture"
(p "The platform composes existing SX subsystems into a unified workflow:")
(div :class "overflow-x-auto mt-4"
(table :class "w-full text-sm text-left"
(thead
(tr :class "border-b border-stone-200"
(th :class "py-2 px-3 font-semibold text-stone-700" "Layer")
(th :class "py-2 px-3 font-semibold text-stone-700" "System")
(th :class "py-2 px-3 font-semibold text-stone-700" "Role")))
(tbody :class "text-stone-600"
(tr :class "border-b border-stone-100"
(td :class "py-2 px-3 font-semibold" "Author")
(td :class "py-2 px-3" "Embedded editor + Claude Code")
(td :class "py-2 px-3" "Write SX in the browser with AI assistance"))
(tr :class "border-b border-stone-100"
(td :class "py-2 px-3 font-semibold" "Stage")
(td :class "py-2 px-3" "Content-addressed components")
(td :class "py-2 px-3" "CID-identified artifacts, preview before publish"))
(tr :class "border-b border-stone-100"
(td :class "py-2 px-3 font-semibold" "Test")
(td :class "py-2 px-3" "sx-ci")
(td :class "py-2 px-3" "Run test suites against staged changes"))
(tr :class "border-b border-stone-100"
(td :class "py-2 px-3 font-semibold" "Publish")
(td :class "py-2 px-3" "sx-activity")
(td :class "py-2 px-3" "Federated distribution via ActivityPub"))
(tr :class "border-b border-stone-100"
(td :class "py-2 px-3 font-semibold" "Store")
(td :class "py-2 px-3" "IPFS")
(td :class "py-2 px-3" "Content-addressed storage, permanent availability"))
(tr :class "border-b border-stone-100"
(td :class "py-2 px-3 font-semibold" "Verify")
(td :class "py-2 px-3" "Environment images")
(td :class "py-2 px-3" "Spec CID \u2192 image CID \u2192 endpoint provenance"))))))
(~doc-section :title "Embedded Claude Code" :id "claude-code"
(p "Claude Code sessions run inside the browser as reactive islands. "
"The AI has access to the full SX component environment — it can read specs, "
"write components, run tests, and propose changes. All within the user's security context.")
(p "The session produces SX diffs — not text patches, but structural changes to the component tree. "
"These diffs are first-class SX values that can be inspected, composed, reverted, and replayed.")
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
(li "Read any component definition, spec file, or plan")
(li "Write new components (essays, examples, specs)")
(li "Modify existing components with structural diffs")
(li "Run sx-ci test suites against proposed changes")
(li "Stage changes as content-addressed preview")
(li "Publish via sx-activity when approved")))
(~doc-section :title "Workflow" :id "workflow"
(p "A typical session — adding a new essay:")
(ol :class "space-y-3 text-stone-600 list-decimal pl-5"
(li (strong "Author: ") "Open Claude Code session on sx-web.org. "
"Describe the essay topic. Claude writes the defcomp in SX.")
(li (strong "Preview: ") "The component renders live in the browser. "
"Author reviews, requests changes. Claude iterates.")
(li (strong "Stage: ") "Component is serialized, CID computed, stored to IPFS. "
"A preview URL is generated from the CID.")
(li (strong "Test: ") "sx-ci runs: component renders without error, "
"all referenced components exist, CSS classes are valid, links resolve.")
(li (strong "Publish: ") "sx-activity broadcasts the new component. "
"Federated subscribers receive it. The nav tree updates automatically.")
(li (strong "Verify: ") "Anyone can follow the CID chain from the served page "
"back to the spec that generated the evaluator that rendered it.")))
(~doc-section :title "Content Types" :id "content-types"
(p "Anything that can be a defcomp can be authored on the platform:")
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
(li (strong "Essays") " — opinion pieces, rationales, explorations")
(li (strong "Examples") " — interactive demos with live code")
(li (strong "Specs") " — new spec modules authored and tested in-browser")
(li (strong "Plans") " — architecture documents with embedded diagrams")
(li (strong "Components") " — reusable UI components shared via IPFS")
(li (strong "Tests") " — defsuite/deftest written and executed live")))
(~doc-section :title "Prerequisites" :id "prerequisites"
(p "Systems that must be complete before the platform can work:")
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
(li (strong "Reactive islands (L2+)") " — for the editor and preview panes")
(li (strong "Content-addressed components") " — CID computation and IPFS storage")
(li (strong "sx-activity") " — federated publish/subscribe")
(li (strong "sx-ci") " — test pipelines as SX components")
(li (strong "Runtime slicing") " — L3 full eval in browser for live preview")
(li (strong "Environment images") " — for provenance verification")
(li (strong "Claude Code API integration") " — embedded sessions via API")))))

364
sx/sx/plans/typed-sx.sx Normal file
View File

@@ -0,0 +1,364 @@
;; ---------------------------------------------------------------------------
;; Typed SX — Gradual Type System
;; ---------------------------------------------------------------------------
(defcomp ~plan-typed-sx-content ()
(~doc-page :title "Typed SX"
(~doc-section :title "The Opportunity" :id "opportunity"
(p "SX already has types. Every primitive in " (code "primitives.sx") " declares " (code ":returns \"number\"") " or " (code ":returns \"boolean\"") ". Every IO primitive in " (code "boundary.sx") " declares " (code ":returns \"dict?\"") " or " (code ":returns \"any\"") ". Component params are named. The information exists — nobody checks it.")
(p "A gradual type system makes this information useful. Annotations are optional. Unannotated code works exactly as before. Annotated code gets checked at registration time — zero runtime cost, errors before any request is served. The checker is a spec module (" (code "types.sx") "), bootstrapped to every host.")
(p "This is not Haskell. SX doesn't need a type system to be correct — " (a :href "/plans/theorem-prover" :class "text-violet-700 underline" "prove.sx") " already verifies primitive properties by exhaustive search. Types serve a different purpose: they catch " (strong "composition errors") " — wrong argument passed to a component, mismatched return type piped into another function, missing keyword arg. The kind of bug you find by reading the stack trace and slapping your forehead."))
;; -----------------------------------------------------------------------
;; What already exists
;; -----------------------------------------------------------------------
(~doc-section :title "What Already Exists" :id "existing"
(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" "Feature")
(th :class "px-3 py-2 font-medium text-stone-600" "Where")
(th :class "px-3 py-2 font-medium text-stone-600" "Types today")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Primitive return types")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "primitives.sx :returns")
(td :class "px-3 py-2 text-stone-600" "\"number\", \"string\", \"boolean\", \"list\", \"dict\", \"any\""))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "IO primitive return types")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "boundary.sx :returns")
(td :class "px-3 py-2 text-stone-600" "Same + \"dict?\", \"string?\", \"element\" — nullable types already appear"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Primitive param names")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "primitives.sx :params")
(td :class "px-3 py-2 text-stone-600" "Named but untyped: " (code "(a b)") ", " (code "(&rest args)")))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Component param names")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "eval.sx parse-comp-params")
(td :class "px-3 py-2 text-stone-600" (code "&key") " params, " (code "&rest") " children — named, untyped"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Runtime type predicates")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "primitives.sx")
(td :class "px-3 py-2 text-stone-600" (code "number?") ", " (code "string?") ", " (code "list?") ", " (code "dict?") ", " (code "nil?") ", " (code "symbol?") " — all exist"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Purity classification")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "deps.sx + boundary.py")
(td :class "px-3 py-2 text-stone-600" "Pure vs IO — a binary type at the component level"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Property verification")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "prove.sx")
(td :class "px-3 py-2 text-stone-600" "Algebraic properties (commutativity, transitivity) verified by bounded model checking")))))
(p "The foundation is solid. Primitives already have return types. Params already have names. Boundary enforcement already does structural analysis. Types extend this — they don't replace it."))
;; -----------------------------------------------------------------------
;; Type language
;; -----------------------------------------------------------------------
(~doc-section :title "Type Language" :id "type-language"
(p "Small, practical, no type theory PhD required.")
(~doc-subsection :title "Base types"
(~doc-code :code (highlight ";; Atomic types\nnumber string boolean nil symbol keyword element\n\n;; Nullable (already used in boundary.sx)\nstring? ;; (or string nil)\ndict? ;; (or dict nil)\nnumber? ;; (or number nil)\n\n;; The top type — anything goes\nany\n\n;; The bottom type — never returns (e.g. abort)\nnever" "lisp")))
(~doc-subsection :title "Compound types"
(~doc-code :code (highlight ";; Collections with element types\n(list-of number) ;; list where every element is a number\n(list-of string) ;; list of strings\n(list-of any) ;; list (same as untyped)\n(dict-of string number) ;; dict with string keys, number values\n(dict-of string any) ;; dict with string keys (typical kwargs)\n\n;; Union types\n(or string number) ;; either string or number\n(or string nil) ;; same as string?\n\n;; Function types\n(-> number number) ;; number → number\n(-> string string boolean) ;; (string, string) → boolean\n(-> (list-of any) number) ;; list → number\n(-> &rest any number) ;; variadic → number" "lisp")))
(~doc-subsection :title "Component types"
(~doc-code :code (highlight ";; Component type is its keyword signature\n;; Derived automatically from defcomp — no annotation needed\n\n(comp :title string :price number &rest element)\n;; keyword args children type\n\n;; This is NOT a new syntax for defcomp.\n;; It's the TYPE that a defcomp declaration produces.\n;; The checker infers it from parse-comp-params + annotations." "lisp")))
(p "That's it. No generics, no higher-kinded types, no dependent types, no type classes. Just: what goes in, what comes out, can it be nil."))
;; -----------------------------------------------------------------------
;; Annotation syntax
;; -----------------------------------------------------------------------
(~doc-section :title "Annotation Syntax" :id "syntax"
(p "Annotations are optional. Three places they can appear:")
(~doc-subsection :title "1. Component params"
(~doc-code :code (highlight ";; Current (unchanged, still works)\n(defcomp ~product-card (&key title price image-url &rest children)\n (div ...))\n\n;; Annotated — colon after param name\n(defcomp ~product-card (&key (title : string)\n (price : number)\n (image-url : string?)\n &rest children)\n (div ...))\n\n;; Parenthesized pairs: (name : type)\n;; Unannotated params default to `any`\n;; &rest children is always (list-of element)" "lisp"))
(p "The " (code "(name : type)") " syntax is unambiguous — a 3-element list where the second element is the symbol " (code ":") ". The parser already handles lists inside parameter lists. " (code "parse-comp-params") " gains a branch: if a param is a list of length 3 with " (code ":") " in the middle, extract name and type."))
(~doc-subsection :title "2. Define/lambda return types"
(~doc-code :code (highlight ";; Current (unchanged)\n(define total-price\n (fn (items)\n (reduce + 0 (map (fn (i) (get i \"price\")) items))))\n\n;; Annotated — :returns after params\n(define total-price\n (fn ((items : (list-of dict)) :returns number)\n (reduce + 0 (map (fn (i) (get i \"price\")) items))))" "lisp"))
(p (code ":returns") " is already the convention in " (code "primitives.sx") " and " (code "boundary.sx") ". Same keyword, same position (after params), same meaning."))
(~doc-subsection :title "3. Let bindings"
(~doc-code :code (highlight ";; Current (unchanged)\n(let ((x (compute-value)))\n (+ x 1))\n\n;; Annotated\n(let (((x : number) (compute-value)))\n (+ x 1))\n\n;; Usually unnecessary — the checker infers let binding\n;; types from the right-hand side. Only annotate when\n;; the inference is ambiguous (e.g. the RHS returns `any`)." "lisp"))
(p "All annotations are syntactically backward-compatible. Unannotated code parses and runs identically. The annotations are simply ignored by evaluators that don't have the type checker loaded."))
;; -----------------------------------------------------------------------
;; Type checking
;; -----------------------------------------------------------------------
(~doc-section :title "Type Checking" :id "checking"
(p "The checker runs at registration time — after " (code "compute_all_deps") ", before serving. It walks every component's body AST and verifies that call sites match declared signatures.")
(~doc-subsection :title "What it checks"
(ol :class "list-decimal pl-5 text-stone-700 space-y-2"
(li (strong "Primitive calls:") " " (code "(+ \"hello\" 3)") " — " (code "+") " expects numbers, got a string. Error.")
(li (strong "Component calls:") " " (code "(~product-card :title 42)") " — " (code ":title") " declared as " (code "string") ", got " (code "number") ". Error.")
(li (strong "Missing required params:") " " (code "(~product-card :price 29.99)") " — " (code ":title") " not provided, no default. Error.")
(li (strong "Unknown keyword args:") " " (code "(~product-card :title \"Hi\" :colour \"red\")") " — " (code ":colour") " not in param list. Warning.")
(li (strong "Nil safety:") " " (code "(+ 1 (get user \"age\"))") " — " (code "get") " returns " (code "any") " (might be nil). " (code "+") " expects " (code "number") ". Warning: possible nil.")
(li (strong "Thread-first type flow:") " " (code "(-> items (filter active?) (map name) (join \", \"))") " — checks each step's output matches the next step's input.")))
(~doc-subsection :title "What it does NOT check"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "Runtime values.") " " (code "(if condition 42 \"hello\")") " — the type is " (code "(or number string)") ". The checker doesn't know which branch executes.")
(li (strong "Dict key presence.") " " (code "(get user \"name\")") " — the checker knows " (code "get") " returns " (code "any") " but doesn't track which keys a dict has. (Future: typed dicts/records.)")
(li (strong "Termination.") " That's " (a :href "/plans/theorem-prover" :class "text-violet-700 underline" "prove.sx") "'s domain.")
(li (strong "Effects.") " Purity is already enforced by " (code "deps.sx") " + boundary. Types don't duplicate it.")))
(~doc-subsection :title "Inference"
(p "Most types are inferred, not annotated. The checker knows:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Literal types: " (code "42") " → " (code "number") ", " (code "\"hi\"") " → " (code "string") ", " (code "true") " → " (code "boolean") ", " (code "nil") " → " (code "nil"))
(li "Primitive return types: " (code "(+ a b)") " → " (code "number") ", " (code "(str x)") " → " (code "string") ", " (code "(empty? x)") " → " (code "boolean"))
(li "Let bindings: " (code "(let ((x 42)) ...)") " → " (code "x : number"))
(li "If/cond narrowing: " (code "(if (nil? x) \"default\" (str x))") " — in the else branch, " (code "x") " is not nil")
(li "Component return: always " (code "element"))
(li "Map/filter propagation: " (code "(map name items)") " → " (code "(list-of string)") " if " (code "name") " returns " (code "string")))
(p "In practice, most component bodies need zero annotations. The checker infers types from literals and primitive return declarations. Annotations are for the edges: params coming from outside, and ambiguous flows.")))
;; -----------------------------------------------------------------------
;; Gradual semantics
;; -----------------------------------------------------------------------
(~doc-section :title "Gradual Semantics" :id "gradual"
(p "The type " (code "any") " is the escape hatch. It's compatible with everything — passes every check, accepts every value. Unannotated params are " (code "any") ". The return type of " (code "get") " is " (code "any") ". This means:")
(ul :class "list-disc pl-5 text-stone-700 space-y-2"
(li (strong "Fully untyped code:") " All params are " (code "any") ", all returns are " (code "any") ". The checker has nothing to verify. No errors, no warnings. Exactly the same as today.")
(li (strong "Partially typed:") " Some components annotate params, others don't. The checker verifies annotated call sites and skips untyped ones. You get value proportional to effort.")
(li (strong "Fully typed:") " Every component, every lambda, every let binding. The checker catches every composition error. Maximum value, maximum annotation cost."))
(p "The practical sweet spot: " (strong "annotate component params, nothing else.") " Components are the public API — the boundary between independent pieces of code. Their params are the contract. Internal lambdas and let bindings benefit less from annotations because the checker can infer their types from context.")
(~doc-code :code (highlight ";; Sweet spot: annotate the interface, infer the rest\n(defcomp ~price-display (&key (price : number)\n (currency : string)\n (sale-price : number?))\n ;; Everything below is inferred:\n ;; formatted → string (str returns string)\n ;; discount → number (- returns number)\n ;; has-sale → boolean (and returns boolean)\n (let ((formatted (str currency (format-number price 2)))\n (has-sale (and sale-price (< sale-price price)))\n (discount (if has-sale\n (round (* 100 (/ (- price sale-price) price)))\n 0)))\n (div :class \"price\"\n (span :class (if has-sale \"line-through text-stone-400\" \"font-bold\")\n formatted)\n (when has-sale\n (span :class \"text-green-700 font-bold ml-2\"\n (str currency (format-number sale-price 2))\n (span :class \"text-xs ml-1\" (str \"(-\" discount \"%)\")))))))" "lisp")))
;; -----------------------------------------------------------------------
;; Error reporting
;; -----------------------------------------------------------------------
(~doc-section :title "Error Reporting" :id "errors"
(p "Type errors are reported at registration time with source location, expected type, actual type, and the full call chain.")
(~doc-code :code (highlight ";; Example error output:\n;;\n;; TYPE ERROR in ~checkout-summary (checkout.sx:34)\n;;\n;; (str \"Total: \" (compute-total items))\n;; ^^^^^^^^^^^^^^^^^\n;; Argument 2 of `str` expects: string\n;; Got: number (from compute-total :returns number)\n;;\n;; Fix: (str \"Total: \" (str (compute-total items)))\n;;\n;;\n;; TYPE ERROR in ~product-page (products.sx:12)\n;;\n;; (~product-card :title product-name :price \"29.99\")\n;; ^^^^^^\n;; Keyword :price of ~product-card expects: number\n;; Got: string (literal \"29.99\")\n;;\n;; Fix: (~product-card :title product-name :price 29.99)" "lisp"))
(p "Severity levels:")
(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" "Level")
(th :class "px-3 py-2 font-medium text-stone-600" "When")
(th :class "px-3 py-2 font-medium text-stone-600" "Behavior")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-red-700" "Error")
(td :class "px-3 py-2 text-stone-700" "Definite type mismatch: " (code "number") " where " (code "string") " expected")
(td :class "px-3 py-2 text-stone-600" "Strict mode: startup crash. Permissive: logged warning."))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-amber-700" "Warning")
(td :class "px-3 py-2 text-stone-700" "Possible mismatch: " (code "any") " where " (code "number") " expected, unknown kwarg")
(td :class "px-3 py-2 text-stone-600" "Logged, never crashes"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-500" "Info")
(td :class "px-3 py-2 text-stone-700" "Annotation suggestion: frequently-called untyped component")
(td :class "px-3 py-2 text-stone-600" "Dev mode only")))))
(p (code "SX_TYPE_STRICT=1") " (env var, like " (code "SX_BOUNDARY_STRICT") ") makes type errors fatal at startup. Absent = permissive. Same pattern as boundary enforcement."))
;; -----------------------------------------------------------------------
;; Nil narrowing
;; -----------------------------------------------------------------------
(~doc-section :title "Nil Narrowing" :id "nil"
(p "The most common real-world type error in SX: passing a possibly-nil value where a non-nil is required. " (code "get") " returns " (code "any") " (might be nil). " (code "current-user") " returns " (code "dict?") " (explicitly nullable). Piping these into " (code "str") " or arithmetic without checking is the #1 source of runtime errors.")
(~doc-code :code (highlight ";; Before: runtime error if user is nil\n(defcomp ~greeting (&key (user : dict?))\n (h1 (str \"Hello, \" (get user \"name\"))))\n ;; ^^^ TYPE WARNING: user is dict?, get needs non-nil first arg\n\n;; After: checker enforces nil handling\n(defcomp ~greeting (&key (user : dict?))\n (if user\n (h1 (str \"Hello, \" (get user \"name\")))\n ;; In this branch, checker narrows user to `dict` (not nil)\n (h1 \"Hello, guest\")))\n ;; No warning — nil case handled" "lisp"))
(p "Narrowing rules:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code "(if x then else)") " — in " (code "then") ", " (code "x") " is narrowed to exclude " (code "nil") " and " (code "false"))
(li (code "(when x body)") " — in " (code "body") ", " (code "x") " is narrowed")
(li (code "(nil? x)") " in an if test — " (code "then") " branch: " (code "x") " is " (code "nil") ", " (code "else") " branch: " (code "x") " is non-nil")
(li (code "(string? x)") " in an if test — " (code "then") " branch: " (code "x") " is " (code "string")))
(p "This is standard flow typing, nothing exotic. TypeScript does the same thing with " (code "if (x !== null)") " narrowing."))
;; -----------------------------------------------------------------------
;; Component signature verification
;; -----------------------------------------------------------------------
(~doc-section :title "Component Signature Verification" :id "signatures"
(p "The highest-value check: verifying that component call sites match declared signatures. This is where most bugs live.")
(~doc-code :code (highlight ";; Definition\n(defcomp ~product-card (&key (title : string)\n (price : number)\n (image-url : string?)\n &rest children)\n ...)\n\n;; Call site checks:\n(~product-card :title \"Widget\" :price 29.99) ;; OK\n(~product-card :title \"Widget\") ;; ERROR: :price required\n(~product-card :title 42 :price 29.99) ;; ERROR: :title expects string\n(~product-card :title \"Widget\" :price 29.99\n (p \"Description\") (p \"Details\")) ;; OK: children\n(~product-card :titel \"Widget\" :price 29.99) ;; WARNING: :titel unknown\n ;; (did you mean :title?)" "lisp"))
(p "The checker walks every component call in every component body. For each call:")
(ol :class "list-decimal pl-5 text-stone-700 space-y-1"
(li "Look up the callee component in env")
(li "Match provided keyword args against declared params")
(li "Check each arg's inferred type against the param's declared type")
(li "Report missing required params (those without defaults)")
(li "Report unknown keyword args (with Levenshtein suggestion)"))
(p "This catches the majority of composition bugs. A renamed param, a swapped argument, a missing required field — all caught before serving."))
;; -----------------------------------------------------------------------
;; Thread-first type flow
;; -----------------------------------------------------------------------
(~doc-section :title "Thread-First Type Flow" :id "thread-first"
(p "The " (code "->") " (thread-first) form is SX's primary composition operator. Type checking it means verifying each step's output matches the next step's input:")
(~doc-code :code (highlight ";; (-> items\n;; (filter active?) ;; (list-of dict) → (list-of dict)\n;; (map name) ;; (list-of dict) → (list-of string)\n;; (join \", \")) ;; (list-of string) → string\n;;\n;; Type flow: (list-of dict) → (list-of dict) → (list-of string) → string\n;; Each step's output is the next step's first argument.\n\n;; ERROR example:\n;; (-> items\n;; (filter active?)\n;; (join \", \") ;; join expects (list-of string),\n;; (map name)) ;; got string — wrong order!\n;;\n;; TYPE ERROR: step 3 (map) expects (list-of any) as first arg\n;; got: string (from join)" "lisp"))
(p "The checker threads the inferred type through each step. If any step's input type doesn't match the previous step's output type, it reports the exact point where the pipeline breaks."))
;; -----------------------------------------------------------------------
;; Relationship to prove.sx
;; -----------------------------------------------------------------------
(~doc-section :title "Types vs Proofs" :id "types-vs-proofs"
(p (a :href "/plans/theorem-prover" :class "text-violet-700 underline" "prove.sx") " and types.sx are complementary, not competing:")
(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" "")
(th :class "px-3 py-2 font-medium text-stone-600" "types.sx")
(th :class "px-3 py-2 font-medium text-stone-600" "prove.sx")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-700" "Checks")
(td :class "px-3 py-2 text-stone-700" "Composition: does A's output fit B's input?")
(td :class "px-3 py-2 text-stone-700" "Properties: is + commutative? Is sort stable?"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-700" "Scope")
(td :class "px-3 py-2 text-stone-700" "All component bodies, every call site")
(td :class "px-3 py-2 text-stone-700" "Primitives only (declared in primitives.sx)"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-700" "Method")
(td :class "px-3 py-2 text-stone-700" "Type inference + checking (fast, O(n) AST walk)")
(td :class "px-3 py-2 text-stone-700" "Bounded model checking (exhaustive, slower)"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-700" "When")
(td :class "px-3 py-2 text-stone-700" "Registration time (every startup)")
(td :class "px-3 py-2 text-stone-700" "CI / on-demand (not every startup)"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-700" "Catches")
(td :class "px-3 py-2 text-stone-700" "Wrong arg type, missing param, nil misuse")
(td :class "px-3 py-2 text-stone-700" "Algebraic law violations, edge case failures")))))
(p "Types answer: " (em "\"does this code fit together?\"") " Proofs answer: " (em "\"does this code do the right thing?\"") " Both are spec modules, both bootstrapped, both run without external tools."))
;; -----------------------------------------------------------------------
;; Implementation
;; -----------------------------------------------------------------------
(~doc-section :title "Implementation" :id "implementation"
(~doc-subsection :title "Phase 1: Type Registry"
(p "Build the type registry from existing declarations.")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Parse " (code ":returns") " from " (code "primitives.sx") " and " (code "boundary.sx") " into a type map: " (code "primitive-name → return-type"))
(li "Parse " (code ":params") " declarations into param type maps (currently untyped — default to " (code "any") ")")
(li "Compute component signatures from " (code "parse-comp-params") " + any type annotations")
(li "Store in env as metadata alongside existing component/primitive objects")))
(~doc-subsection :title "Phase 2: Type Inference Engine"
(p "Walk AST, infer types bottom-up.")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Literals → concrete types")
(li "Primitive calls → look up return type in registry")
(li "Component calls → " (code "element"))
(li "Let bindings → RHS inferred type")
(li "If/when/cond → union of branch types, with narrowing in branches")
(li "Lambda → " (code "(-> param-types return-type)") " from body inference")
(li "Map/filter → propagate element types through the transform")))
(~doc-subsection :title "Phase 3: Type Checker"
(p "Compare inferred types at call sites against declared types.")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Subtype check: " (code "number") " <: " (code "any") ", " (code "string") " <: " (code "string?") ", " (code "nil") " <: " (code "string?"))
(li "Error on definite mismatch: " (code "number") " vs " (code "string"))
(li "Warn on possible mismatch: " (code "any") " vs " (code "number") " (might work, might not)")
(li "Component kwarg checking: required params, unknown kwargs, type mismatches")))
(~doc-subsection :title "Phase 4: Annotation Parsing"
(p "Extend " (code "parse-comp-params") " and " (code "sf-defcomp") " to recognize type annotations.")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code "(name : type)") " in param lists → extract type, store in component metadata")
(li (code ":returns type") " in lambda/fn bodies → store as declared return type")
(li "Backward compatible: unannotated params remain " (code "any"))))
(~doc-subsection :title "Phase 5: Typed Primitives"
(p "Add param types to " (code "primitives.sx") " declarations.")
(~doc-code :code (highlight ";; Current\n(define-primitive \"+\"\n :params (&rest args)\n :returns \"number\"\n :doc \"Sum all arguments.\")\n\n;; Extended\n(define-primitive \"+\"\n :params (&rest (args : number))\n :returns \"number\"\n :doc \"Sum all arguments.\")" "lisp"))
(p "This is the biggest payoff for effort: ~80 primitives gain param types, enabling the checker to catch every mistyped primitive call across the entire codebase.")))
;; -----------------------------------------------------------------------
;; Spec module
;; -----------------------------------------------------------------------
(~doc-section :title "Spec Module" :id "spec-module"
(p (code "types.sx") " — the type checker, written in SX, bootstrapped to every host.")
(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" "Function")
(th :class "px-3 py-2 font-medium text-stone-600" "Purpose")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "infer-type")
(td :class "px-3 py-2 text-stone-700" "Infer the type of an AST node in a given type environment"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "check-call")
(td :class "px-3 py-2 text-stone-700" "Check a function/component call against declared signature"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "check-component")
(td :class "px-3 py-2 text-stone-700" "Type-check an entire component body"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "check-all")
(td :class "px-3 py-2 text-stone-700" "Check all registered components, return error/warning list"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "subtype?")
(td :class "px-3 py-2 text-stone-700" "Is type A a subtype of type B?"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "narrow-type")
(td :class "px-3 py-2 text-stone-700" "Narrow a type based on a predicate test (nil?, string?, etc)"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "type-union")
(td :class "px-3 py-2 text-stone-700" "Compute the union of two types"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "parse-type-annotation")
(td :class "px-3 py-2 text-stone-700" "Parse a type expression from annotation syntax"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "build-type-registry")
(td :class "px-3 py-2 text-stone-700" "Build type map from primitives.sx + boundary.sx declarations")))))
(p "The checker is ~300-500 lines of SX. It's an AST walk with a type environment — structurally similar to " (code "deps.sx") " (which walks ASTs to find IO refs) and " (code "prove.sx") " (which walks ASTs to generate verification conditions). Same pattern, different question."))
;; -----------------------------------------------------------------------
;; Relationships
;; -----------------------------------------------------------------------
(~doc-section :title "Relationships" :id "relationships"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (a :href "/plans/theorem-prover" :class "text-violet-700 underline" "Theorem Prover") " — prove.sx verifies primitive properties; types.sx verifies composition. Complementary.")
(li (a :href "/plans/content-addressed-components" :class "text-violet-700 underline" "Content-Addressed Components") " — component manifests gain type signatures. A consumer knows param types before fetching the source.")
(li (a :href "/plans/environment-images" :class "text-violet-700 underline" "Environment Images") " — the type registry serializes into the image. Type checking happens once at image build time, not on every startup.")
(li (a :href "/plans/runtime-slicing" :class "text-violet-700 underline" "Runtime Slicing") " — types.sx is a registration-time module, not a runtime module. It doesn't ship to the client. Zero impact on bundle size."))
(div :class "rounded border border-amber-200 bg-amber-50 p-3 mt-2"
(p :class "text-amber-800 text-sm" (strong "Depends on: ") "primitives.sx (return types exist), boundary.sx (IO return types exist), eval.sx (defcomp parsing). " (strong "New: ") (code "types.sx") " spec module, type annotations in " (code "parse-comp-params") "."))
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mt-4"
(p :class "text-violet-900 font-medium" "Why not a Haskell host?")
(p :class "text-violet-800" "A Haskell SX host would type-check the " (em "host") " code (the evaluator, renderer, parser). But it can't type-check " (em "SX programs") " — those are dynamically typed values passing through " (code "SxVal") ". " (code "types.sx") " checks SX programs directly, on every host. One spec, all hosts benefit. The type system lives where it matters — in the language, not in any particular implementation of it."))))))

View File

@@ -5,7 +5,7 @@
(h1 :class "text-5xl font-bold text-stone-900 mb-4"
(span :class "text-violet-600 font-mono" "(<sx>)"))
(p :class "text-2xl text-stone-600 mb-4"
"s-expressions for the web")
"Framework free reactive hypermedia")
(p :class "text-sm text-stone-400"
"© Giles Bradshaw 2026")
(p :class "text-lg text-stone-500 max-w-2xl mx-auto mb-12"

File diff suppressed because it is too large Load Diff

View File

@@ -8,20 +8,12 @@ def _register_sx_layouts() -> None:
from shared.sx.layouts import register_sx_layout
if os.getenv("SX_STANDALONE") == "true":
register_sx_layout("sx",
"sx-standalone-layout-full",
"sx-standalone-layout-oob",
"sx-standalone-layout-mobile")
register_sx_layout("sx-section",
"sx-standalone-section-layout-full",
"sx-standalone-section-layout-oob",
"sx-standalone-section-layout-mobile")
register_sx_layout("sx-docs",
"sx-standalone-docs-layout-full",
"sx-standalone-docs-layout-oob",
"sx-standalone-docs-layout-mobile")
else:
register_sx_layout("sx",
"sx-layout-full",
"sx-layout-oob",
"sx-layout-mobile")
register_sx_layout("sx-section",
"sx-section-layout-full",
"sx-section-layout-oob",
"sx-section-layout-mobile")
register_sx_layout("sx-docs",
"sx-docs-layout-full",
"sx-docs-layout-oob",
"sx-docs-layout-mobile")