diff --git a/sx/sx/layouts.sx b/sx/sx/layouts.sx index 576001d..d8d43bf 100644 --- a/sx/sx/layouts.sx +++ b/sx/sx/layouts.sx @@ -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" "()")) + (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) diff --git a/sx/sx/nav-data.sx b/sx/sx/nav-data.sx index a93b317..ba7f426 100644 --- a/sx/sx/nav-data.sx +++ b/sx/sx/nav-data.sx @@ -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)))) diff --git a/sx/sx/plans/environment-images.sx b/sx/sx/plans/environment-images.sx new file mode 100644 index 0000000..a96f52c --- /dev/null +++ b/sx/sx/plans/environment-images.sx @@ -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 "\n\n\n \n \n \n\n\n
\n \n\n" "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/\" ...)\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 "\n;;\n;; L2 page (reactive island):\n;; \n;; \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."))))) diff --git a/sx/sx/plans/sx-web-platform.sx b/sx/sx/plans/sx-web-platform.sx new file mode 100644 index 0000000..5aa8793 --- /dev/null +++ b/sx/sx/plans/sx-web-platform.sx @@ -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"))))) diff --git a/sx/sx/plans/typed-sx.sx b/sx/sx/plans/typed-sx.sx new file mode 100644 index 0000000..687d6a0 --- /dev/null +++ b/sx/sx/plans/typed-sx.sx @@ -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.")))))) diff --git a/sx/sxc/home.sx b/sx/sxc/home.sx index 7445d3d..64d6765 100644 --- a/sx/sxc/home.sx +++ b/sx/sxc/home.sx @@ -5,7 +5,7 @@ (h1 :class "text-5xl font-bold text-stone-900 mb-4" (span :class "text-violet-600 font-mono" "()")) (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" diff --git a/sx/sxc/pages/docs.sx b/sx/sxc/pages/docs.sx index 7335b6a..93be11f 100644 --- a/sx/sxc/pages/docs.sx +++ b/sx/sxc/pages/docs.sx @@ -1,6 +1,6 @@ ;; SX docs app — declarative page definitions ;; All content dispatched via case + direct component references. -;; Navigation built from SX data (nav-data.sx), no Python intermediaries. +;; Navigation is in-page via (~sx-doc :path "..." content...). ;; --------------------------------------------------------------------------- ;; Home page @@ -9,8 +9,8 @@ (defpage home :path "/" :auth :public - :layout :sx - :content (~sx-home-content)) + :layout :sx-docs + :content (~sx-doc :path "/" (~sx-home-content))) ;; --------------------------------------------------------------------------- ;; Docs section @@ -19,35 +19,25 @@ (defpage docs-index :path "/docs/" :auth :public - :layout (:sx-section - :section "Docs" - :sub-label "Docs" - :sub-href "/docs/introduction" - :sub-nav (~section-nav :items docs-nav-items :current "Introduction") - :selected "Introduction") - :content (~docs-introduction-content)) + :layout :sx-docs + :content (~sx-doc :path "/docs/" (~docs-introduction-content))) (defpage docs-page :path "/docs/" :auth :public - :layout (:sx-section - :section "Docs" - :sub-label "Docs" - :sub-href "/docs/introduction" - :sub-nav (~section-nav :items docs-nav-items - :current (find-current docs-nav-items slug)) - :selected (or (find-current docs-nav-items slug) "")) - :content (case slug - "introduction" (~docs-introduction-content) - "getting-started" (~docs-getting-started-content) - "components" (~docs-components-content) - "evaluator" (~docs-evaluator-content) - "primitives" (~docs-primitives-content - :prims (~doc-primitives-tables :primitives (primitives-data))) - "special-forms" (~docs-special-forms-content - :forms (~doc-special-forms-tables :forms (special-forms-data))) - "server-rendering" (~docs-server-rendering-content) - :else (~docs-introduction-content))) + :layout :sx-docs + :content (~sx-doc :path (str "/docs/" slug) + (case slug + "introduction" (~docs-introduction-content) + "getting-started" (~docs-getting-started-content) + "components" (~docs-components-content) + "evaluator" (~docs-evaluator-content) + "primitives" (~docs-primitives-content + :prims (~doc-primitives-tables :primitives (primitives-data))) + "special-forms" (~docs-special-forms-content + :forms (~doc-special-forms-tables :forms (special-forms-data))) + "server-rendering" (~docs-server-rendering-content) + :else (~docs-introduction-content)))) ;; --------------------------------------------------------------------------- ;; Reference section @@ -56,102 +46,80 @@ (defpage reference-index :path "/reference/" :auth :public - :layout (:sx-section - :section "Reference" - :sub-label "Reference" - :sub-href "/reference/" - :sub-nav (~section-nav :items reference-nav-items :current "") - :selected "") - :content (~reference-index-content)) + :layout :sx-docs + :content (~sx-doc :path "/reference/" (~reference-index-content))) (defpage reference-page :path "/reference/" :auth :public - :layout (:sx-section - :section "Reference" - :sub-label "Reference" - :sub-href "/reference/" - :sub-nav (~section-nav :items reference-nav-items - :current (find-current reference-nav-items slug)) - :selected (or (find-current reference-nav-items slug) "")) + :layout :sx-docs :data (reference-data slug) - :content (case slug - "attributes" (~reference-attrs-content - :req-table (~doc-attr-table-from-data :title "Request Attributes" :attrs req-attrs) - :beh-table (~doc-attr-table-from-data :title "Behavior Attributes" :attrs beh-attrs) - :uniq-table (~doc-attr-table-from-data :title "Unique to sx" :attrs uniq-attrs)) - "headers" (~reference-headers-content - :req-table (~doc-headers-table-from-data :title "Request Headers" :headers req-headers) - :resp-table (~doc-headers-table-from-data :title "Response Headers" :headers resp-headers)) - "events" (~reference-events-content - :table (~doc-two-col-table-from-data - :intro "sx fires custom DOM events at various points in the request lifecycle." - :col1 "Event" :col2 "Description" :items events-list)) - "js-api" (~reference-js-api-content - :table (~doc-two-col-table-from-data - :intro "The client-side sx.js library exposes a public API for programmatic use." - :col1 "Method" :col2 "Description" :items js-api-list)) - :else (~reference-attrs-content - :req-table (~doc-attr-table-from-data :title "Request Attributes" :attrs req-attrs) - :beh-table (~doc-attr-table-from-data :title "Behavior Attributes" :attrs beh-attrs) - :uniq-table (~doc-attr-table-from-data :title "Unique to sx" :attrs uniq-attrs)))) + :content (~sx-doc :path (str "/reference/" slug) + (case slug + "attributes" (~reference-attrs-content + :req-table (~doc-attr-table-from-data :title "Request Attributes" :attrs req-attrs) + :beh-table (~doc-attr-table-from-data :title "Behavior Attributes" :attrs beh-attrs) + :uniq-table (~doc-attr-table-from-data :title "Unique to sx" :attrs uniq-attrs)) + "headers" (~reference-headers-content + :req-table (~doc-headers-table-from-data :title "Request Headers" :headers req-headers) + :resp-table (~doc-headers-table-from-data :title "Response Headers" :headers resp-headers)) + "events" (~reference-events-content + :table (~doc-two-col-table-from-data + :intro "sx fires custom DOM events at various points in the request lifecycle." + :col1 "Event" :col2 "Description" :items events-list)) + "js-api" (~reference-js-api-content + :table (~doc-two-col-table-from-data + :intro "The client-side sx.js library exposes a public API for programmatic use." + :col1 "Method" :col2 "Description" :items js-api-list)) + :else (~reference-attrs-content + :req-table (~doc-attr-table-from-data :title "Request Attributes" :attrs req-attrs) + :beh-table (~doc-attr-table-from-data :title "Behavior Attributes" :attrs beh-attrs) + :uniq-table (~doc-attr-table-from-data :title "Unique to sx" :attrs uniq-attrs))))) (defpage reference-attr-detail :path "/reference/attributes/" :auth :public - :layout (:sx-section - :section "Reference" - :sub-label "Reference" - :sub-href "/reference/" - :sub-nav (~section-nav :items reference-nav-items :current "Attributes") - :selected "Attributes") + :layout :sx-docs :data (attr-detail-data slug) - :content (if attr-not-found - (~reference-attr-not-found :slug slug) - (~reference-attr-detail-content - :title attr-title - :description attr-description - :demo attr-demo - :example-code attr-example - :handler-code attr-handler - :wire-placeholder-id attr-wire-id))) + :content (~sx-doc :path "/reference/attributes" + (if attr-not-found + (~reference-attr-not-found :slug slug) + (~reference-attr-detail-content + :title attr-title + :description attr-description + :demo attr-demo + :example-code attr-example + :handler-code attr-handler + :wire-placeholder-id attr-wire-id)))) (defpage reference-header-detail :path "/reference/headers/" :auth :public - :layout (:sx-section - :section "Reference" - :sub-label "Reference" - :sub-href "/reference/" - :sub-nav (~section-nav :items reference-nav-items :current "Headers") - :selected "Headers") + :layout :sx-docs :data (header-detail-data slug) - :content (if header-not-found - (~reference-attr-not-found :slug slug) - (~reference-header-detail-content - :title header-title - :direction header-direction - :description header-description - :example-code header-example - :demo header-demo))) + :content (~sx-doc :path "/reference/headers" + (if header-not-found + (~reference-attr-not-found :slug slug) + (~reference-header-detail-content + :title header-title + :direction header-direction + :description header-description + :example-code header-example + :demo header-demo)))) (defpage reference-event-detail :path "/reference/events/" :auth :public - :layout (:sx-section - :section "Reference" - :sub-label "Reference" - :sub-href "/reference/" - :sub-nav (~section-nav :items reference-nav-items :current "Events") - :selected "Events") + :layout :sx-docs :data (event-detail-data slug) - :content (if event-not-found - (~reference-attr-not-found :slug slug) - (~reference-event-detail-content - :title event-title - :description event-description - :example-code event-example - :demo event-demo))) + :content (~sx-doc :path "/reference/events" + (if event-not-found + (~reference-attr-not-found :slug slug) + (~reference-event-detail-content + :title event-title + :description event-description + :example-code event-example + :demo event-demo)))) ;; --------------------------------------------------------------------------- ;; Protocols section @@ -160,32 +128,22 @@ (defpage protocols-index :path "/protocols/" :auth :public - :layout (:sx-section - :section "Protocols" - :sub-label "Protocols" - :sub-href "/protocols/wire-format" - :sub-nav (~section-nav :items protocols-nav-items :current "Wire Format") - :selected "Wire Format") - :content (~protocol-wire-format-content)) + :layout :sx-docs + :content (~sx-doc :path "/protocols/" (~protocol-wire-format-content))) (defpage protocol-page :path "/protocols/" :auth :public - :layout (:sx-section - :section "Protocols" - :sub-label "Protocols" - :sub-href "/protocols/wire-format" - :sub-nav (~section-nav :items protocols-nav-items - :current (find-current protocols-nav-items slug)) - :selected (or (find-current protocols-nav-items slug) "")) - :content (case slug - "wire-format" (~protocol-wire-format-content) - "fragments" (~protocol-fragments-content) - "resolver-io" (~protocol-resolver-io-content) - "internal-services" (~protocol-internal-services-content) - "activitypub" (~protocol-activitypub-content) - "future" (~protocol-future-content) - :else (~protocol-wire-format-content))) + :layout :sx-docs + :content (~sx-doc :path (str "/protocols/" slug) + (case slug + "wire-format" (~protocol-wire-format-content) + "fragments" (~protocol-fragments-content) + "resolver-io" (~protocol-resolver-io-content) + "internal-services" (~protocol-internal-services-content) + "activitypub" (~protocol-activitypub-content) + "future" (~protocol-future-content) + :else (~protocol-wire-format-content)))) ;; --------------------------------------------------------------------------- ;; Examples section @@ -194,53 +152,43 @@ (defpage examples-index :path "/examples/" :auth :public - :layout (:sx-section - :section "Examples" - :sub-label "Examples" - :sub-href "/examples/click-to-load" - :sub-nav (~section-nav :items examples-nav-items :current "Click to Load") - :selected "Click to Load") - :content (~example-click-to-load)) + :layout :sx-docs + :content (~sx-doc :path "/examples/")) (defpage examples-page :path "/examples/" :auth :public - :layout (:sx-section - :section "Examples" - :sub-label "Examples" - :sub-href "/examples/click-to-load" - :sub-nav (~section-nav :items examples-nav-items - :current (find-current examples-nav-items slug)) - :selected (or (find-current examples-nav-items slug) "")) - :content (case slug - "click-to-load" (~example-click-to-load) - "form-submission" (~example-form-submission) - "polling" (~example-polling) - "delete-row" (~example-delete-row) - "inline-edit" (~example-inline-edit) - "oob-swaps" (~example-oob-swaps) - "lazy-loading" (~example-lazy-loading) - "infinite-scroll" (~example-infinite-scroll) - "progress-bar" (~example-progress-bar) - "active-search" (~example-active-search) - "inline-validation" (~example-inline-validation) - "value-select" (~example-value-select) - "reset-on-submit" (~example-reset-on-submit) - "edit-row" (~example-edit-row) - "bulk-update" (~example-bulk-update) - "swap-positions" (~example-swap-positions) - "select-filter" (~example-select-filter) - "tabs" (~example-tabs) - "animations" (~example-animations) - "dialogs" (~example-dialogs) - "keyboard-shortcuts" (~example-keyboard-shortcuts) - "put-patch" (~example-put-patch) - "json-encoding" (~example-json-encoding) - "vals-and-headers" (~example-vals-and-headers) - "loading-states" (~example-loading-states) - "sync-replace" (~example-sync-replace) - "retry" (~example-retry) - :else (~example-click-to-load))) + :layout :sx-docs + :content (~sx-doc :path (str "/examples/" slug) + (case slug + "click-to-load" (~example-click-to-load) + "form-submission" (~example-form-submission) + "polling" (~example-polling) + "delete-row" (~example-delete-row) + "inline-edit" (~example-inline-edit) + "oob-swaps" (~example-oob-swaps) + "lazy-loading" (~example-lazy-loading) + "infinite-scroll" (~example-infinite-scroll) + "progress-bar" (~example-progress-bar) + "active-search" (~example-active-search) + "inline-validation" (~example-inline-validation) + "value-select" (~example-value-select) + "reset-on-submit" (~example-reset-on-submit) + "edit-row" (~example-edit-row) + "bulk-update" (~example-bulk-update) + "swap-positions" (~example-swap-positions) + "select-filter" (~example-select-filter) + "tabs" (~example-tabs) + "animations" (~example-animations) + "dialogs" (~example-dialogs) + "keyboard-shortcuts" (~example-keyboard-shortcuts) + "put-patch" (~example-put-patch) + "json-encoding" (~example-json-encoding) + "vals-and-headers" (~example-vals-and-headers) + "loading-states" (~example-loading-states) + "sync-replace" (~example-sync-replace) + "retry" (~example-retry) + :else (~example-click-to-load)))) ;; --------------------------------------------------------------------------- ;; Essays section @@ -249,41 +197,31 @@ (defpage essays-index :path "/essays/" :auth :public - :layout (:sx-section - :section "Essays" - :sub-label "Essays" - :sub-href "/essays/" - :sub-nav (~section-nav :items essays-nav-items :current "") - :selected "") - :content (~essays-index-content)) + :layout :sx-docs + :content (~sx-doc :path "/essays/" (~essays-index-content))) (defpage essay-page :path "/essays/" :auth :public - :layout (:sx-section - :section "Essays" - :sub-label "Essays" - :sub-href "/essays/" - :sub-nav (~section-nav :items essays-nav-items - :current (find-current essays-nav-items slug)) - :selected (or (find-current essays-nav-items slug) "")) - :content (case slug - "sx-sucks" (~essay-sx-sucks) - "why-sexps" (~essay-why-sexps) - "htmx-react-hybrid" (~essay-htmx-react-hybrid) - "on-demand-css" (~essay-on-demand-css) - "client-reactivity" (~essay-client-reactivity) - "sx-native" (~essay-sx-native) - "tail-call-optimization" (~essay-tail-call-optimization) - "continuations" (~essay-continuations) - "reflexive-web" (~essay-reflexive-web) - "server-architecture" (~essay-server-architecture) - "separation-of-concerns" (~essay-separation-of-concerns) - "sx-and-ai" (~essay-sx-and-ai) - "no-alternative" (~essay-no-alternative) - "zero-tooling" (~essay-zero-tooling) - "react-is-hypermedia" (~essay-react-is-hypermedia) - :else (~essays-index-content))) + :layout :sx-docs + :content (~sx-doc :path (str "/essays/" slug) + (case slug + "sx-sucks" (~essay-sx-sucks) + "why-sexps" (~essay-why-sexps) + "htmx-react-hybrid" (~essay-htmx-react-hybrid) + "on-demand-css" (~essay-on-demand-css) + "client-reactivity" (~essay-client-reactivity) + "sx-native" (~essay-sx-native) + "tail-call-optimization" (~essay-tail-call-optimization) + "continuations" (~essay-continuations) + "reflexive-web" (~essay-reflexive-web) + "server-architecture" (~essay-server-architecture) + "separation-of-concerns" (~essay-separation-of-concerns) + "sx-and-ai" (~essay-sx-and-ai) + "no-alternative" (~essay-no-alternative) + "zero-tooling" (~essay-zero-tooling) + "react-is-hypermedia" (~essay-react-is-hypermedia) + :else (~essays-index-content)))) ;; --------------------------------------------------------------------------- ;; Philosophy section @@ -292,31 +230,21 @@ (defpage philosophy-index :path "/philosophy/" :auth :public - :layout (:sx-section - :section "Philosophy" - :sub-label "Philosophy" - :sub-href "/philosophy/" - :sub-nav (~section-nav :items philosophy-nav-items :current "") - :selected "") - :content (~philosophy-index-content)) + :layout :sx-docs + :content (~sx-doc :path "/philosophy/" (~philosophy-index-content))) (defpage philosophy-page :path "/philosophy/" :auth :public - :layout (:sx-section - :section "Philosophy" - :sub-label "Philosophy" - :sub-href "/philosophy/" - :sub-nav (~section-nav :items philosophy-nav-items - :current (find-current philosophy-nav-items slug)) - :selected (or (find-current philosophy-nav-items slug) "")) - :content (case slug - "sx-manifesto" (~essay-sx-manifesto) - "godel-escher-bach" (~essay-godel-escher-bach) - "wittgenstein" (~essay-sx-and-wittgenstein) - "dennett" (~essay-sx-and-dennett) - "existentialism" (~essay-s-existentialism) - :else (~philosophy-index-content))) + :layout :sx-docs + :content (~sx-doc :path (str "/philosophy/" slug) + (case slug + "sx-manifesto" (~essay-sx-manifesto) + "godel-escher-bach" (~essay-godel-escher-bach) + "wittgenstein" (~essay-sx-and-wittgenstein) + "dennett" (~essay-sx-and-dennett) + "existentialism" (~essay-s-existentialism) + :else (~philosophy-index-content)))) ;; --------------------------------------------------------------------------- ;; CSSX section @@ -325,32 +253,22 @@ (defpage cssx-index :path "/cssx/" :auth :public - :layout (:sx-section - :section "CSSX" - :sub-label "CSSX" - :sub-href "/cssx/" - :sub-nav (~section-nav :items cssx-nav-items :current "Overview") - :selected "Overview") - :content (~cssx-overview-content)) + :layout :sx-docs + :content (~sx-doc :path "/cssx/" (~cssx-overview-content))) (defpage cssx-page :path "/cssx/" :auth :public - :layout (:sx-section - :section "CSSX" - :sub-label "CSSX" - :sub-href "/cssx/" - :sub-nav (~section-nav :items cssx-nav-items - :current (find-current cssx-nav-items slug)) - :selected (or (find-current cssx-nav-items slug) "")) - :content (case slug - "patterns" (~cssx-patterns-content) - "delivery" (~cssx-delivery-content) - "async" (~cssx-async-content) - "live" (~cssx-live-content) - "comparisons" (~cssx-comparison-content) - "philosophy" (~cssx-philosophy-content) - :else (~cssx-overview-content))) + :layout :sx-docs + :content (~sx-doc :path (str "/cssx/" slug) + (case slug + "patterns" (~cssx-patterns-content) + "delivery" (~cssx-delivery-content) + "async" (~cssx-async-content) + "live" (~cssx-live-content) + "comparisons" (~cssx-comparison-content) + "philosophy" (~cssx-philosophy-content) + :else (~cssx-overview-content)))) ;; --------------------------------------------------------------------------- ;; Specs section @@ -359,66 +277,56 @@ (defpage specs-index :path "/specs/" :auth :public - :layout (:sx-section - :section "Specs" - :sub-label "Specs" - :sub-href "/specs/" - :sub-nav (~section-nav :items specs-nav-items :current "Architecture") - :selected "Architecture") - :content (~spec-architecture-content)) + :layout :sx-docs + :content (~sx-doc :path "/specs/" (~spec-architecture-content))) (defpage specs-page :path "/specs/" :auth :public - :layout (:sx-section - :section "Specs" - :sub-label "Specs" - :sub-href "/specs/" - :sub-nav (~section-nav :items specs-nav-items - :current (find-current specs-nav-items slug)) - :selected (or (find-current specs-nav-items slug) "")) - :content (case slug - "core" (~spec-overview-content - :spec-title "Core Language" - :spec-files (map (fn (item) - (dict :title (get item "title") :desc (get item "desc") - :prose (get item "prose") - :filename (get item "filename") :href (str "/specs/" (get item "slug")) - :source (read-spec-file (get item "filename")))) - core-spec-items)) - "adapters" (~spec-overview-content - :spec-title "Adapters & Engine" - :spec-files (map (fn (item) - (dict :title (get item "title") :desc (get item "desc") - :prose (get item "prose") - :filename (get item "filename") :href (str "/specs/" (get item "slug")) - :source (read-spec-file (get item "filename")))) - adapter-spec-items)) - "browser" (~spec-overview-content - :spec-title "Browser" - :spec-files (map (fn (item) - (dict :title (get item "title") :desc (get item "desc") - :prose (get item "prose") - :filename (get item "filename") :href (str "/specs/" (get item "slug")) - :source (read-spec-file (get item "filename")))) - browser-spec-items)) - "extensions" (~spec-overview-content - :spec-title "Extensions" - :spec-files (map (fn (item) - (dict :title (get item "title") :desc (get item "desc") - :prose (get item "prose") - :filename (get item "filename") :href (str "/specs/" (get item "slug")) - :source (read-spec-file (get item "filename")))) - extension-spec-items)) - :else (let ((spec (find-spec slug))) - (if spec - (~spec-detail-content - :spec-title (get spec "title") - :spec-desc (get spec "desc") - :spec-filename (get spec "filename") - :spec-source (read-spec-file (get spec "filename")) - :spec-prose (get spec "prose")) - (~spec-not-found :slug slug))))) + :layout :sx-docs + :content (~sx-doc :path (str "/specs/" slug) + (case slug + "core" (~spec-overview-content + :spec-title "Core Language" + :spec-files (map (fn (item) + (dict :title (get item "title") :desc (get item "desc") + :prose (get item "prose") + :filename (get item "filename") :href (str "/specs/" (get item "slug")) + :source (read-spec-file (get item "filename")))) + core-spec-items)) + "adapters" (~spec-overview-content + :spec-title "Adapters & Engine" + :spec-files (map (fn (item) + (dict :title (get item "title") :desc (get item "desc") + :prose (get item "prose") + :filename (get item "filename") :href (str "/specs/" (get item "slug")) + :source (read-spec-file (get item "filename")))) + adapter-spec-items)) + "browser" (~spec-overview-content + :spec-title "Browser" + :spec-files (map (fn (item) + (dict :title (get item "title") :desc (get item "desc") + :prose (get item "prose") + :filename (get item "filename") :href (str "/specs/" (get item "slug")) + :source (read-spec-file (get item "filename")))) + browser-spec-items)) + "extensions" (~spec-overview-content + :spec-title "Extensions" + :spec-files (map (fn (item) + (dict :title (get item "title") :desc (get item "desc") + :prose (get item "prose") + :filename (get item "filename") :href (str "/specs/" (get item "slug")) + :source (read-spec-file (get item "filename")))) + extension-spec-items)) + :else (let ((spec (find-spec slug))) + (if spec + (~spec-detail-content + :spec-title (get spec "title") + :spec-desc (get spec "desc") + :spec-filename (get spec "filename") + :spec-source (read-spec-file (get spec "filename")) + :spec-prose (get spec "prose")) + (~spec-not-found :slug slug)))))) ;; --------------------------------------------------------------------------- ;; Bootstrappers section @@ -427,53 +335,43 @@ (defpage bootstrappers-index :path "/bootstrappers/" :auth :public - :layout (:sx-section - :section "Bootstrappers" - :sub-label "Bootstrappers" - :sub-href "/bootstrappers/" - :sub-nav (~section-nav :items bootstrappers-nav-items :current "Overview") - :selected "Overview") - :content (~bootstrappers-index-content)) + :layout :sx-docs + :content (~sx-doc :path "/bootstrappers/" (~bootstrappers-index-content))) (defpage bootstrapper-page :path "/bootstrappers/" :auth :public - :layout (:sx-section - :section "Bootstrappers" - :sub-label "Bootstrappers" - :sub-href "/bootstrappers/" - :sub-nav (~section-nav :items bootstrappers-nav-items - :current (find-current bootstrappers-nav-items slug)) - :selected (or (find-current bootstrappers-nav-items slug) "")) + :layout :sx-docs :data (bootstrapper-data slug) - :content (if bootstrapper-not-found - (~spec-not-found :slug slug) - (case slug - "self-hosting" - (~bootstrapper-self-hosting-content - :py-sx-source py-sx-source - :g0-output g0-output - :g1-output g1-output - :defines-matched defines-matched - :defines-total defines-total - :g0-lines g0-lines - :g0-bytes g0-bytes - :verification-status verification-status) - "self-hosting-js" - (~bootstrapper-self-hosting-js-content - :js-sx-source js-sx-source - :defines-matched defines-matched - :defines-total defines-total - :js-sx-lines js-sx-lines - :verification-status verification-status) - "python" - (~bootstrapper-py-content - :bootstrapper-source bootstrapper-source - :bootstrapped-output bootstrapped-output) - :else - (~bootstrapper-js-content - :bootstrapper-source bootstrapper-source - :bootstrapped-output bootstrapped-output)))) + :content (~sx-doc :path (str "/bootstrappers/" slug) + (if bootstrapper-not-found + (~spec-not-found :slug slug) + (case slug + "self-hosting" + (~bootstrapper-self-hosting-content + :py-sx-source py-sx-source + :g0-output g0-output + :g1-output g1-output + :defines-matched defines-matched + :defines-total defines-total + :g0-lines g0-lines + :g0-bytes g0-bytes + :verification-status verification-status) + "self-hosting-js" + (~bootstrapper-self-hosting-js-content + :js-sx-source js-sx-source + :defines-matched defines-matched + :defines-total defines-total + :js-sx-lines js-sx-lines + :verification-status verification-status) + "python" + (~bootstrapper-py-content + :bootstrapper-source bootstrapper-source + :bootstrapped-output bootstrapped-output) + :else + (~bootstrapper-js-content + :bootstrapper-source bootstrapper-source + :bootstrapped-output bootstrapped-output))))) ;; --------------------------------------------------------------------------- ;; Isomorphism section @@ -482,81 +380,55 @@ (defpage isomorphism-index :path "/isomorphism/" :auth :public - :layout (:sx-section - :section "Isomorphism" - :sub-label "Isomorphism" - :sub-href "/isomorphism/" - :sub-nav (~section-nav :items isomorphism-nav-items :current "Roadmap") - :selected "Roadmap") - :content (~plan-isomorphic-content)) + :layout :sx-docs + :content (~sx-doc :path "/isomorphism/" (~plan-isomorphic-content))) (defpage bundle-analyzer :path "/isomorphism/bundle-analyzer" :auth :public - :layout (:sx-section - :section "Isomorphism" - :sub-label "Isomorphism" - :sub-href "/isomorphism/" - :sub-nav (~section-nav :items isomorphism-nav-items :current "Bundle Analyzer") - :selected "Bundle Analyzer") + :layout :sx-docs :data (bundle-analyzer-data) - :content (~bundle-analyzer-content - :pages pages :total-components total-components :total-macros total-macros - :pure-count pure-count :io-count io-count)) + :content (~sx-doc :path "/isomorphism/bundle-analyzer" + (~bundle-analyzer-content + :pages pages :total-components total-components :total-macros total-macros + :pure-count pure-count :io-count io-count))) (defpage routing-analyzer :path "/isomorphism/routing-analyzer" :auth :public - :layout (:sx-section - :section "Isomorphism" - :sub-label "Isomorphism" - :sub-href "/isomorphism/" - :sub-nav (~section-nav :items isomorphism-nav-items :current "Routing Analyzer") - :selected "Routing Analyzer") + :layout :sx-docs :data (routing-analyzer-data) - :content (~routing-analyzer-content - :pages pages :total-pages total-pages :client-count client-count - :server-count server-count :registry-sample registry-sample)) + :content (~sx-doc :path "/isomorphism/routing-analyzer" + (~routing-analyzer-content + :pages pages :total-pages total-pages :client-count client-count + :server-count server-count :registry-sample registry-sample))) (defpage data-test :path "/isomorphism/data-test" :auth :public - :layout (:sx-section - :section "Isomorphism" - :sub-label "Isomorphism" - :sub-href "/isomorphism/" - :sub-nav (~section-nav :items isomorphism-nav-items :current "Data Test") - :selected "Data Test") + :layout :sx-docs :data (data-test-data) - :content (~data-test-content - :server-time server-time :items items - :phase phase :transport transport)) + :content (~sx-doc :path "/isomorphism/data-test" + (~data-test-content + :server-time server-time :items items + :phase phase :transport transport))) (defpage async-io-demo :path "/isomorphism/async-io" :auth :public - :layout (:sx-section - :section "Isomorphism" - :sub-label "Isomorphism" - :sub-href "/isomorphism/" - :sub-nav (~section-nav :items isomorphism-nav-items :current "Async IO") - :selected "Async IO") - :content (~async-io-demo-content)) + :layout :sx-docs + :content (~sx-doc :path "/isomorphism/async-io" (~async-io-demo-content))) (defpage streaming-demo :path "/isomorphism/streaming" :auth :public :stream true - :layout (:sx-section - :section "Isomorphism" - :sub-label "Isomorphism" - :sub-href "/isomorphism/" - :sub-nav (~section-nav :items isomorphism-nav-items :current "Streaming") - :selected "Streaming") - :shell (~streaming-demo-layout - (~suspense :id "stream-fast" :fallback (~stream-skeleton)) - (~suspense :id "stream-medium" :fallback (~stream-skeleton)) - (~suspense :id "stream-slow" :fallback (~stream-skeleton))) + :layout :sx-docs + :shell (~sx-doc :path "/isomorphism/streaming" + (~streaming-demo-layout + (~suspense :id "stream-fast" :fallback (~stream-skeleton)) + (~suspense :id "stream-medium" :fallback (~stream-skeleton)) + (~suspense :id "stream-slow" :fallback (~stream-skeleton)))) :data (streaming-demo-data) :content (~streaming-demo-chunk :stream-label stream-label @@ -567,58 +439,41 @@ (defpage affinity-demo :path "/isomorphism/affinity" :auth :public - :layout (:sx-section - :section "Isomorphism" - :sub-label "Isomorphism" - :sub-href "/isomorphism/" - :sub-nav (~section-nav :items isomorphism-nav-items :current "Affinity") - :selected "Affinity") + :layout :sx-docs :data (affinity-demo-data) - :content (~affinity-demo-content :components components :page-plans page-plans)) + :content (~sx-doc :path "/isomorphism/affinity" + (~affinity-demo-content :components components :page-plans page-plans))) (defpage optimistic-demo :path "/isomorphism/optimistic" :auth :public - :layout (:sx-section - :section "Isomorphism" - :sub-label "Isomorphism" - :sub-href "/isomorphism/" - :sub-nav (~section-nav :items isomorphism-nav-items :current "Optimistic") - :selected "Optimistic") + :layout :sx-docs :data (optimistic-demo-data) - :content (~optimistic-demo-content :items items :server-time server-time)) + :content (~sx-doc :path "/isomorphism/optimistic" + (~optimistic-demo-content :items items :server-time server-time))) (defpage offline-demo :path "/isomorphism/offline" :auth :public - :layout (:sx-section - :section "Isomorphism" - :sub-label "Isomorphism" - :sub-href "/isomorphism/" - :sub-nav (~section-nav :items isomorphism-nav-items :current "Offline") - :selected "Offline") + :layout :sx-docs :data (offline-demo-data) - :content (~offline-demo-content :notes notes :server-time server-time)) + :content (~sx-doc :path "/isomorphism/offline" + (~offline-demo-content :notes notes :server-time server-time))) ;; Wildcard must come AFTER specific routes (first-match routing) (defpage isomorphism-page :path "/isomorphism/" :auth :public - :layout (:sx-section - :section "Isomorphism" - :sub-label "Isomorphism" - :sub-href "/isomorphism/" - :sub-nav (~section-nav :items isomorphism-nav-items - :current (find-current isomorphism-nav-items slug)) - :selected (or (find-current isomorphism-nav-items slug) "")) - :content (case slug - "bundle-analyzer" (~bundle-analyzer-content - :pages pages :total-components total-components :total-macros total-macros - :pure-count pure-count :io-count io-count) - "routing-analyzer" (~routing-analyzer-content - :pages pages :total-pages total-pages :client-count client-count - :server-count server-count :registry-sample registry-sample) - :else (~plan-isomorphic-content))) + :layout :sx-docs + :content (~sx-doc :path (str "/isomorphism/" slug) + (case slug + "bundle-analyzer" (~bundle-analyzer-content + :pages pages :total-components total-components :total-macros total-macros + :pure-count pure-count :io-count io-count) + "routing-analyzer" (~routing-analyzer-content + :pages pages :total-pages total-pages :client-count client-count + :server-count server-count :registry-sample registry-sample) + :else (~plan-isomorphic-content)))) ;; --------------------------------------------------------------------------- ;; Plans section @@ -627,43 +482,38 @@ (defpage plans-index :path "/plans/" :auth :public - :layout (:sx-section - :section "Plans" - :sub-label "Plans" - :sub-href "/plans/" - :sub-nav (~section-nav :items plans-nav-items :current "") - :selected "") - :content (~plans-index-content)) + :layout :sx-docs + :content (~sx-doc :path "/plans/" (~plans-index-content))) (defpage plan-page :path "/plans/" :auth :public - :layout (:sx-section - :section "Plans" - :sub-label "Plans" - :sub-href "/plans/" - :sub-nav (~section-nav :items plans-nav-items - :current (find-current plans-nav-items slug)) - :selected (or (find-current plans-nav-items slug) "")) + :layout :sx-docs :data (case slug "theorem-prover" (prove-data) :else nil) - :content (case slug - "status" (~plan-status-content) - "reader-macros" (~plan-reader-macros-content) - "reader-macro-demo" (~plan-reader-macro-demo-content) - "theorem-prover" (~plan-theorem-prover-content) - "self-hosting-bootstrapper" (~plan-self-hosting-bootstrapper-content) - "js-bootstrapper" (~plan-js-bootstrapper-content) - "sx-activity" (~plan-sx-activity-content) - "predictive-prefetch" (~plan-predictive-prefetch-content) - "content-addressed-components" (~plan-content-addressed-components-content) - "fragment-protocol" (~plan-fragment-protocol-content) - "glue-decoupling" (~plan-glue-decoupling-content) - "social-sharing" (~plan-social-sharing-content) - "sx-ci" (~plan-sx-ci-content) - "live-streaming" (~plan-live-streaming-content) - :else (~plans-index-content))) + :content (~sx-doc :path (str "/plans/" slug) + (case slug + "status" (~plan-status-content) + "reader-macros" (~plan-reader-macros-content) + "reader-macro-demo" (~plan-reader-macro-demo-content) + "theorem-prover" (~plan-theorem-prover-content) + "self-hosting-bootstrapper" (~plan-self-hosting-bootstrapper-content) + "js-bootstrapper" (~plan-js-bootstrapper-content) + "sx-activity" (~plan-sx-activity-content) + "predictive-prefetch" (~plan-predictive-prefetch-content) + "content-addressed-components" (~plan-content-addressed-components-content) + "environment-images" (~plan-environment-images-content) + "runtime-slicing" (~plan-runtime-slicing-content) + "typed-sx" (~plan-typed-sx-content) + "nav-redesign" (~plan-nav-redesign-content) + "fragment-protocol" (~plan-fragment-protocol-content) + "glue-decoupling" (~plan-glue-decoupling-content) + "social-sharing" (~plan-social-sharing-content) + "sx-ci" (~plan-sx-ci-content) + "live-streaming" (~plan-live-streaming-content) + "sx-web-platform" (~plan-sx-web-platform-content) + :else (~plans-index-content)))) ;; --------------------------------------------------------------------------- ;; Reactive Islands section @@ -672,31 +522,21 @@ (defpage reactive-islands-index :path "/reactive-islands/" :auth :public - :layout (:sx-section - :section "Reactive Islands" - :sub-label "Reactive Islands" - :sub-href "/reactive-islands/" - :sub-nav (~section-nav :items reactive-islands-nav-items :current "Overview") - :selected "Overview") - :content (~reactive-islands-index-content)) + :layout :sx-docs + :content (~sx-doc :path "/reactive-islands/" (~reactive-islands-index-content))) (defpage reactive-islands-page :path "/reactive-islands/" :auth :public - :layout (:sx-section - :section "Reactive Islands" - :sub-label "Reactive Islands" - :sub-href "/reactive-islands/" - :sub-nav (~section-nav :items reactive-islands-nav-items - :current (find-current reactive-islands-nav-items slug)) - :selected (or (find-current reactive-islands-nav-items slug) "")) - :content (case slug - "demo" (~reactive-islands-demo-content) - "event-bridge" (~reactive-islands-event-bridge-content) - "named-stores" (~reactive-islands-named-stores-content) - "plan" (~reactive-islands-plan-content) - "phase2" (~reactive-islands-phase2-content) - :else (~reactive-islands-index-content))) + :layout :sx-docs + :content (~sx-doc :path (str "/reactive-islands/" slug) + (case slug + "demo" (~reactive-islands-demo-content) + "event-bridge" (~reactive-islands-event-bridge-content) + "named-stores" (~reactive-islands-named-stores-content) + "plan" (~reactive-islands-plan-content) + "phase2" (~reactive-islands-phase2-content) + :else (~reactive-islands-index-content)))) ;; --------------------------------------------------------------------------- ;; Testing section @@ -705,33 +545,23 @@ (defpage testing-index :path "/testing/" :auth :public - :layout (:sx-section - :section "Testing" - :sub-label "Testing" - :sub-href "/testing/" - :sub-nav (~section-nav :items testing-nav-items :current "Overview") - :selected "Overview") + :layout :sx-docs :data (run-modular-tests "all") - :content (~testing-overview-content - :server-results server-results - :framework-source framework-source - :eval-source eval-source - :parser-source parser-source - :router-source router-source - :render-source render-source - :deps-source deps-source - :engine-source engine-source)) + :content (~sx-doc :path "/testing/" + (~testing-overview-content + :server-results server-results + :framework-source framework-source + :eval-source eval-source + :parser-source parser-source + :router-source router-source + :render-source render-source + :deps-source deps-source + :engine-source engine-source))) (defpage testing-page :path "/testing/" :auth :public - :layout (:sx-section - :section "Testing" - :sub-label "Testing" - :sub-href "/testing/" - :sub-nav (~section-nav :items testing-nav-items - :current (find-current testing-nav-items slug)) - :selected (or (find-current testing-nav-items slug) "")) + :layout :sx-docs :data (case slug "eval" (run-modular-tests "eval") "parser" (run-modular-tests "parser") @@ -741,56 +571,57 @@ "engine" (run-modular-tests "engine") "orchestration" (run-modular-tests "orchestration") :else (dict)) - :content (case slug - "eval" (~testing-spec-content - :spec-name "eval" - :spec-title "Evaluator Tests" - :spec-desc "81 tests covering the core evaluator and all primitives — literals, arithmetic, comparison, strings, lists, dicts, predicates, special forms, lambdas, higher-order functions, components, macros, threading, and edge cases." - :spec-source spec-source - :framework-source framework-source - :server-results server-results) - "parser" (~testing-spec-content - :spec-name "parser" - :spec-title "Parser Tests" - :spec-desc "39 tests covering tokenization and parsing — integers, floats, strings, escape sequences, booleans, nil, keywords, symbols, lists, dicts, whitespace, comments, quote sugar, serialization, and round-trips." - :spec-source spec-source - :framework-source framework-source - :server-results server-results) - "router" (~testing-spec-content - :spec-name "router" - :spec-title "Router Tests" - :spec-desc "18 tests covering client-side route matching — path splitting, pattern parsing, segment matching, parameter extraction, and route table search." - :spec-source spec-source - :framework-source framework-source - :server-results server-results) - "render" (~testing-spec-content - :spec-name "render" - :spec-title "Renderer Tests" - :spec-desc "23 tests covering HTML rendering — elements, attributes, void elements, boolean attributes, fragments, escaping, control flow, and component rendering." - :spec-source spec-source - :framework-source framework-source - :server-results server-results) - "deps" (~testing-spec-content - :spec-name "deps" - :spec-title "Dependency Analysis Tests" - :spec-desc "33 tests covering component dependency analysis — scan-refs, scan-components-from-source, transitive-deps, components-needed, scan-io-refs, and component-pure? classification." - :spec-source spec-source - :framework-source framework-source - :server-results server-results) - "engine" (~testing-spec-content - :spec-name "engine" - :spec-title "Engine Tests" - :spec-desc "37 tests covering engine pure functions — parse-time, parse-trigger-spec, default-trigger, parse-swap-spec, parse-retry-spec, next-retry-ms, and filter-params." - :spec-source spec-source - :framework-source framework-source - :server-results server-results) - "orchestration" (~testing-spec-content - :spec-name "orchestration" - :spec-title "Orchestration Tests" - :spec-desc "17 tests covering Phase 7c+7d orchestration — page data cache, optimistic cache update/revert/confirm, offline connectivity, offline queue mutation, and offline-aware routing." - :spec-source spec-source - :framework-source framework-source - :server-results server-results) - "runners" (~testing-runners-content) - :else (~testing-overview-content - :server-results server-results))) + :content (~sx-doc :path (str "/testing/" slug) + (case slug + "eval" (~testing-spec-content + :spec-name "eval" + :spec-title "Evaluator Tests" + :spec-desc "81 tests covering the core evaluator and all primitives — literals, arithmetic, comparison, strings, lists, dicts, predicates, special forms, lambdas, higher-order functions, components, macros, threading, and edge cases." + :spec-source spec-source + :framework-source framework-source + :server-results server-results) + "parser" (~testing-spec-content + :spec-name "parser" + :spec-title "Parser Tests" + :spec-desc "39 tests covering tokenization and parsing — integers, floats, strings, escape sequences, booleans, nil, keywords, symbols, lists, dicts, whitespace, comments, quote sugar, serialization, and round-trips." + :spec-source spec-source + :framework-source framework-source + :server-results server-results) + "router" (~testing-spec-content + :spec-name "router" + :spec-title "Router Tests" + :spec-desc "18 tests covering client-side route matching — path splitting, pattern parsing, segment matching, parameter extraction, and route table search." + :spec-source spec-source + :framework-source framework-source + :server-results server-results) + "render" (~testing-spec-content + :spec-name "render" + :spec-title "Renderer Tests" + :spec-desc "23 tests covering HTML rendering — elements, attributes, void elements, boolean attributes, fragments, escaping, control flow, and component rendering." + :spec-source spec-source + :framework-source framework-source + :server-results server-results) + "deps" (~testing-spec-content + :spec-name "deps" + :spec-title "Dependency Analysis Tests" + :spec-desc "33 tests covering component dependency analysis — scan-refs, scan-components-from-source, transitive-deps, components-needed, scan-io-refs, and component-pure? classification." + :spec-source spec-source + :framework-source framework-source + :server-results server-results) + "engine" (~testing-spec-content + :spec-name "engine" + :spec-title "Engine Tests" + :spec-desc "37 tests covering engine pure functions — parse-time, parse-trigger-spec, default-trigger, parse-swap-spec, parse-retry-spec, next-retry-ms, and filter-params." + :spec-source spec-source + :framework-source framework-source + :server-results server-results) + "orchestration" (~testing-spec-content + :spec-name "orchestration" + :spec-title "Orchestration Tests" + :spec-desc "17 tests covering Phase 7c+7d orchestration — page data cache, optimistic cache update/revert/confirm, offline connectivity, offline queue mutation, and offline-aware routing." + :spec-source spec-source + :framework-source framework-source + :server-results server-results) + "runners" (~testing-runners-content) + :else (~testing-overview-content + :server-results server-results)))) diff --git a/sx/sxc/pages/layouts.py b/sx/sxc/pages/layouts.py index f04a965..b6553e4 100644 --- a/sx/sxc/pages/layouts.py +++ b/sx/sxc/pages/layouts.py @@ -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")