Rename all 1,169 components to path-based names with namespace support

Component names now reflect filesystem location using / as path separator
and : as namespace separator for shared components:
  ~sx-header → ~layouts/header
  ~layout-app-body → ~shared:layout/app-body
  ~blog-admin-dashboard → ~admin/dashboard

209 files, 4,941 replacements across all services.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 22:00:12 +00:00
parent de80d921e9
commit b0920a1121
209 changed files with 4620 additions and 4620 deletions

View File

@@ -187,8 +187,8 @@ def create_app() -> "Quart":
path = request.path
content_ast = [
Symbol("~sx-doc"), Keyword("path"), path,
[Symbol("~not-found-content"), Keyword("path"), path],
Symbol("~layouts/doc"), Keyword("path"), path,
[Symbol("~not-found/content"), Keyword("path"), path],
]
env = dict(get_component_env())

View File

@@ -322,7 +322,7 @@ HEADER_DETAILS: dict[str, dict] = {
"reducing response size. This is the component caching protocol."
),
"example": (
';; Client sends: SX-Components: ~card,~nav-link,~footer\n'
';; Client sends: SX-Components: ~card,~shared:layout/nav-link,~footer\n'
';; Server omits those defcomps from the response.\n'
';; Only new/changed components are sent.\n'
'(response\n'

View File

@@ -6,14 +6,14 @@
;; --- Demo components with different affinities ---
(defcomp ~aff-demo-auto (&key (label :as string?))
(defcomp ~affinity-demo/aff-demo-auto (&key (label :as string?))
(div :class "rounded border border-stone-200 bg-white p-4"
(div :class "flex items-center gap-2 mb-2"
(span :class "inline-block w-2 h-2 rounded-full bg-stone-400")
(span :class "text-sm font-mono text-stone-500" ":affinity :auto"))
(p :class "text-stone-800" (or label "Pure component — no IO calls. Auto-detected as client-renderable."))))
(defcomp ~aff-demo-client (&key (label :as string?))
(defcomp ~affinity-demo/aff-demo-client (&key (label :as string?))
:affinity :client
(div :class "rounded border border-blue-200 bg-blue-50 p-4"
(div :class "flex items-center gap-2 mb-2"
@@ -21,7 +21,7 @@
(span :class "text-sm font-mono text-blue-600" ":affinity :client"))
(p :class "text-blue-800" (or label "Explicitly client-rendered — even IO calls would be proxied."))))
(defcomp ~aff-demo-server (&key (label :as string?))
(defcomp ~affinity-demo/aff-demo-server (&key (label :as string?))
:affinity :server
(div :class "rounded border border-amber-200 bg-amber-50 p-4"
(div :class "flex items-center gap-2 mb-2"
@@ -29,27 +29,27 @@
(span :class "text-sm font-mono text-amber-600" ":affinity :server"))
(p :class "text-amber-800" (or label "Always server-rendered — auth-sensitive or secret-dependent."))))
(defcomp ~aff-demo-io-auto ()
(defcomp ~affinity-demo/aff-demo-io-auto ()
(div :class "rounded border border-red-200 bg-red-50 p-4"
(div :class "flex items-center gap-2 mb-2"
(span :class "inline-block w-2 h-2 rounded-full bg-red-400")
(span :class "text-sm font-mono text-red-600" ":affinity :auto + IO"))
(p :class "text-red-800 mb-3" "Auto affinity with IO dependency — auto-detected as server-rendered.")
(~doc-code :code (highlight "(render-target name env io-names)" "lisp"))))
(~docs/code :code (highlight "(render-target name env io-names)" "lisp"))))
(defcomp ~aff-demo-io-client ()
(defcomp ~affinity-demo/aff-demo-io-client ()
:affinity :client
(div :class "rounded border border-violet-200 bg-violet-50 p-4"
(div :class "flex items-center gap-2 mb-2"
(span :class "inline-block w-2 h-2 rounded-full bg-violet-400")
(span :class "text-sm font-mono text-violet-600" ":affinity :client + IO"))
(p :class "text-violet-800 mb-3" "Client affinity overrides IO — calls proxied to server via /sx/io/.")
(~doc-code :code (highlight "(component-affinity comp)" "lisp"))))
(~docs/code :code (highlight "(component-affinity comp)" "lisp"))))
;; --- Main page component ---
(defcomp ~affinity-demo-content (&key components page-plans)
(defcomp ~affinity-demo/content (&key components page-plans)
(div :class "space-y-8"
(div :class "border-b border-stone-200 pb-6"
(h1 :class "text-2xl font-bold text-stone-900" "Affinity Annotations")
@@ -59,9 +59,9 @@
" function in deps.sx combines the annotation with IO analysis to produce a per-component boundary decision."))
;; Syntax
(~doc-section :title "Syntax" :id "syntax"
(~docs/section :title "Syntax" :id "syntax"
(p "Add " (code ":affinity") " between the params list and the body:")
(~doc-code :code (highlight "(defcomp ~my-component (&key title)\n :affinity :client ;; or :server, or omit for :auto\n (div title))" "lisp"))
(~docs/code :code (highlight "(defcomp ~affinity-demo/my-component (&key title)\n :affinity :client ;; or :server, or omit for :auto\n (div title))" "lisp"))
(p "Three values:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code ":auto") " (default) — runtime decides from IO dependency analysis")
@@ -69,18 +69,18 @@
(li (code ":server") " — always render server-side; never sent to client as SX")))
;; Live components
(~doc-section :title "Live Components" :id "live"
(~docs/section :title "Live Components" :id "live"
(p "These components are defined with different affinities. The server analyzed them at registration time:")
(div :class "space-y-4 mt-4"
(~aff-demo-auto)
(~aff-demo-client)
(~aff-demo-server)
(~aff-demo-io-auto)
(~aff-demo-io-client)))
(~affinity-demo/aff-demo-auto)
(~affinity-demo/aff-demo-client)
(~affinity-demo/aff-demo-server)
(~affinity-demo/aff-demo-io-auto)
(~affinity-demo/aff-demo-io-client)))
;; Analysis table from server
(~doc-section :title "Server Analysis" :id "analysis"
(~docs/section :title "Server Analysis" :id "analysis"
(p "The server computed these render targets at component registration time:")
(div :class "overflow-x-auto rounded border border-stone-200"
(table :class "w-full text-left text-sm"
@@ -114,7 +114,7 @@
components)))))
;; Decision matrix
(~doc-section :title "Decision Matrix" :id "matrix"
(~docs/section :title "Decision Matrix" :id "matrix"
(div :class "overflow-x-auto rounded border border-stone-200"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-100"
@@ -155,7 +155,7 @@
(td :class "px-3 py-2 text-stone-600" "Both affinity and IO say server"))))))
;; Per-page render plans
(~doc-section :title "Page Render Plans" :id "plans"
(~docs/section :title "Page Render Plans" :id "plans"
(p "Phase 7b: render plans are pre-computed at registration time for each page. The plan maps every component needed by the page to its render target.")
(when (> (len page-plans) 0)
@@ -184,7 +184,7 @@
page-plans))))
;; How it integrates
(~doc-section :title "How It Works" :id "how"
(~docs/section :title "How It Works" :id "how"
(ol :class "list-decimal list-inside text-stone-700 space-y-2"
(li (code "defcomp") " parses " (code ":affinity") " annotation between params and body")
(li "Component object stores " (code "affinity") " field (\"auto\", \"client\", or \"server\")")
@@ -201,6 +201,6 @@
(p :class "font-semibold text-amber-800" "How to verify")
(ol :class "list-decimal list-inside text-amber-700 space-y-1"
(li "View page source — components with render-target \"server\" are expanded to HTML")
(li "Components with render-target \"client\" appear as " (code "(~name ...)") " in the SX wire format")
(li "Components with render-target \"client\" appear as " (code "(~plans/content-addressed-components/name ...)") " in the SX wire format")
(li "Navigate away and back — client-routable pure components render instantly")
(li "Check the analysis table above — it shows live data from the server's component registry")))))

View File

@@ -3,9 +3,9 @@
;; Drill down into each bundle to see component tree; expand to see SX source.
;; @css bg-green-100 text-green-800 bg-violet-600 bg-stone-200 text-violet-600 text-stone-600 text-green-600 rounded-full h-2.5 grid-cols-3 bg-blue-100 text-blue-800 bg-amber-100 text-amber-800 grid-cols-4 marker:text-stone-400 bg-blue-50 bg-amber-50 text-blue-700 text-amber-700 border-blue-200 border-amber-200 bg-blue-500 bg-amber-500
(defcomp ~bundle-analyzer-content (&key (pages :as list) (total-components :as number) (total-macros :as number)
(defcomp ~analyzer/bundle-analyzer-content (&key (pages :as list) (total-components :as number) (total-macros :as number)
(pure-count :as number) (io-count :as number))
(~doc-page :title "Page Bundle Analyzer"
(~docs/page :title "Page Bundle Analyzer"
(p :class "text-stone-600 mb-6"
"Live analysis of component dependency graphs and IO classification across all pages. "
@@ -17,19 +17,19 @@
"Click a page to see its component tree; expand a component to see its SX source.")
(div :class "mb-8 grid grid-cols-4 gap-4"
(~analyzer-stat :label "Total Components" :value (str total-components)
(~analyzer/stat :label "Total Components" :value (str total-components)
:cls "text-violet-600")
(~analyzer-stat :label "Total Macros" :value (str total-macros)
(~analyzer/stat :label "Total Macros" :value (str total-macros)
:cls "text-stone-600")
(~analyzer-stat :label "Pure Components" :value (str pure-count)
(~analyzer/stat :label "Pure Components" :value (str pure-count)
:cls "text-blue-600")
(~analyzer-stat :label "IO-Dependent" :value (str io-count)
(~analyzer/stat :label "IO-Dependent" :value (str io-count)
:cls "text-amber-600"))
(~doc-section :title "Per-Page Bundles" :id "bundles"
(~docs/section :title "Per-Page Bundles" :id "bundles"
(div :class "space-y-3"
(map (fn (page)
(~analyzer-row
(~analyzer/row
:name (get page "name")
:path (get page "path")
:needed (get page "needed")
@@ -43,9 +43,9 @@
:components (get page "components")))
pages)))
(~doc-section :title "How It Works" :id "how"
(~docs/section :title "How It Works" :id "how"
(ol :class "list-decimal pl-5 space-y-2 text-stone-700"
(li (strong "Scan: ") "Regex finds all " (code "(~name") " patterns in the page's content expression.")
(li (strong "Scan: ") "Regex finds all " (code "(~plans/content-addressed-components/name") " patterns in the page's content expression.")
(li (strong "Resolve: ") "Each referenced component's body AST is walked to find transitive " (code "~") " references.")
(li (strong "Closure: ") "The full set is the union of direct + transitive deps, following chains through the component graph.")
(li (strong "Bundle: ") "Only these component definitions are serialized into the page payload. Everything else is omitted.")
@@ -55,12 +55,12 @@
"walks all branches of control flow (if/when/cond/case), "
"and includes macro definitions shared across components."))))
(defcomp ~analyzer-stat (&key (label :as string) (value :as string) (cls :as string))
(defcomp ~analyzer/stat (&key (label :as string) (value :as string) (cls :as string))
(div :class "rounded-lg border border-stone-200 p-4 text-center"
(div :class (str "text-3xl font-bold " cls) value)
(div :class "text-sm text-stone-500 mt-1" label)))
(defcomp ~analyzer-row (&key (name :as string) (path :as string) (needed :as number) (direct :as number) (total :as number) (pct :as number) (savings :as number)
(defcomp ~analyzer/row (&key (name :as string) (path :as string) (needed :as number) (direct :as number) (total :as number) (pct :as number) (savings :as number)
(io-refs :as list) (pure-in-page :as number) (io-in-page :as number) (components :as list))
(details :class "rounded border border-stone-200"
(summary :class "p-4 cursor-pointer hover:bg-stone-50 transition-colors"
@@ -89,7 +89,7 @@
(str needed " components in bundle"))
(div :class "space-y-1"
(map (fn (comp)
(~analyzer-component
(~analyzer/component
:comp-name (get comp "name")
:is-pure (get comp "is-pure")
:io-refs (get comp "io-refs")
@@ -97,7 +97,7 @@
:source (get comp "source")))
components)))))
(defcomp ~analyzer-component (&key (comp-name :as string) (is-pure :as boolean) (io-refs :as list) (deps :as list) (source :as string))
(defcomp ~analyzer/component (&key (comp-name :as string) (is-pure :as boolean) (io-refs :as list) (deps :as list) (source :as string))
(details :class (str "rounded border "
(if is-pure "border-blue-200 bg-blue-50" "border-amber-200 bg-amber-50"))
(summary :class "px-3 py-2 cursor-pointer hover:opacity-80 transition-opacity"

View File

@@ -13,7 +13,7 @@
;; "sx:route client+async" — async render with IO proxy
;; "sx:io registered N proxied primitives" — IO proxy initialization
(defcomp ~async-io-demo-content ()
(defcomp ~async-io-demo/content ()
(div :class "space-y-8"
(div :class "border-b border-stone-200 pb-6"
(h1 :class "text-2xl font-bold text-stone-900" "Async IO Demo")
@@ -30,17 +30,17 @@
(div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3"
(h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "SX component definition")
(~doc-code :code
(highlight "(defcomp ~card (&key title subtitle &rest children)\n (div :class \"border rounded-lg p-4 shadow-sm\"\n (h2 :class \"text-lg font-bold\" title)\n (when subtitle\n (p :class \"text-stone-500 text-sm\" subtitle))\n (div :class \"mt-3\" children)))" "lisp")))
(~docs/code :code
(highlight "(defcomp ~async-io-demo/card (&key title subtitle &rest children)\n (div :class \"border rounded-lg p-4 shadow-sm\"\n (h2 :class \"text-lg font-bold\" title)\n (when subtitle\n (p :class \"text-stone-500 text-sm\" subtitle))\n (div :class \"mt-3\" children)))" "lisp")))
(div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3"
(h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "Python server code")
(~doc-code :code
(~docs/code :code
(highlight "from shared.sx.pages import mount_io_endpoint\n\n# The IO proxy serves any allowed primitive:\n# GET /sx/io/highlight?_arg0=code&_arg1=lisp\nasync def io_proxy(name):\n result = await execute_io(name, args, kwargs, ctx)\n return serialize(result)" "python")))
(div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3"
(h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "SX async rendering spec")
(~doc-code :code
(~docs/code :code
(highlight ";; try-client-route reads io-deps from page registry\n(let ((io-deps (get match \"io-deps\"))\n (has-io (and io-deps (not (empty? io-deps)))))\n ;; Register IO deps as proxied primitives on demand\n (when has-io (register-io-deps io-deps))\n (if has-io\n ;; Async render: IO primitives proxied via /sx/io/<name>\n (do\n (try-async-eval-content content-src env\n (fn (rendered)\n (when rendered\n (swap-rendered-content target rendered pathname))))\n true)\n ;; Sync render: pure components, no IO\n (let ((rendered (try-eval-content content-src env)))\n (swap-rendered-content target rendered pathname))))" "lisp"))))
;; Architecture explanation
@@ -48,7 +48,7 @@
(h2 :class "text-lg font-semibold text-blue-900" "How it works")
(ol :class "list-decimal list-inside text-blue-800 space-y-2 text-sm"
(li "Server renders the page — " (code "highlight") " runs Python directly")
(li "Client receives component definitions including " (code "~async-io-demo-content"))
(li "Client receives component definitions including " (code "~async-io-demo/content"))
(li "On client navigation, " (code "io-deps") " list routes to async renderer")
(li (code "register-io-deps") " ensures each IO name is proxied via " (code "registerProxiedIo"))
(li "Proxied call: " (code "fetch(\"/sx/io/highlight?_arg0=...&_arg1=lisp\")"))

View File

@@ -6,19 +6,19 @@
;; Overview
;; ---------------------------------------------------------------------------
(defcomp ~cssx-overview-content ()
(~doc-page :title "CSSX Components"
(defcomp ~cssx/overview-content ()
(~docs/page :title "CSSX Components"
(~doc-section :title "The Idea" :id "idea"
(~docs/section :title "The Idea" :id "idea"
(p (strong "Styling is just components.") " A CSSX component is a regular "
(code "defcomp") " that decides how to style its children. It might apply "
"Tailwind classes, or hand-written CSS classes, or inline styles, or generate "
"rules at runtime. The implementation is the component's private business. "
"The consumer just calls " (code "(~btn :variant \"primary\" \"Submit\")") " and doesn't care.")
"The consumer just calls " (code "(~cssx/btn :variant \"primary\" \"Submit\")") " and doesn't care.")
(p "Because it's " (code "defcomp") ", you get everything for free: caching, bundling, "
"dependency scanning, server/client rendering, composition. No parallel infrastructure."))
(~doc-section :title "Why Not a Style Dictionary?" :id "why"
(~docs/section :title "Why Not a Style Dictionary?" :id "why"
(p "SX previously had a parallel CSS system: a style dictionary (JSON blob of "
"atom-to-declaration mappings), a " (code "StyleValue") " type threaded through "
"the evaluator and renderer, content-addressed hash class names (" (code "sx-a3f2b1")
@@ -30,25 +30,25 @@
(code "class=\"sx-a3f2b1\"") ". DevTools became useless. You couldn't inspect an "
"element and understand its styling. " (strong "That was a deal breaker.")))
(~doc-section :title "Key Advantages" :id "advantages"
(~docs/section :title "Key Advantages" :id "advantages"
(ul :class "list-disc pl-5 space-y-2 text-stone-700"
(li (strong "Readable DOM: ") "Elements have real class names, not content-addressed "
"hashes. DevTools works.")
(li (strong "Data-driven styling: ") "Components receive data and decide styling. "
(code "(~metric :value 150)") " renders red because " (code "value > 100")
(code "(~cssx/metric :value 150)") " renders red because " (code "value > 100")
" — logic lives in the component, not a CSS preprocessor.")
(li (strong "One system: ") "No separate " (code "StyleValue") " type, no style "
"dictionary JSON, no injection pipeline. Components ARE the styling abstraction.")
(li (strong "One cache: ") "Component hash/localStorage handles everything. No "
"separate style dict caching.")
(li (strong "Composable: ") (code "(~card :elevated true (~metric :value v))")
(li (strong "Composable: ") (code "(~card :elevated true (~cssx/metric :value v))")
" — styling composes like any other component.")
(li (strong "Strategy-agnostic: ") "A component can apply Tailwind classes, emit "
(code "<style>") " blocks, use inline styles, generate CSS custom properties, or "
"any combination. Swap strategies without touching call sites.")))
(~doc-section :title "What Changed" :id "changes"
(~doc-subsection :title "Removed (~3,000 lines)"
(~docs/section :title "What Changed" :id "changes"
(~docs/subsection :title "Removed (~3,000 lines)"
(ul :class "list-disc pl-5 space-y-1 text-stone-600 text-sm"
(li (code "StyleValue") " type and all plumbing (type checks in eval, render, serialize)")
(li (code "cssx.sx") " spec module (resolve-style, resolve-atom, split-variant, hash, injection)")
@@ -59,7 +59,7 @@
(li (code "defkeyframes") " special form")
(li "Style dict cookies and localStorage keys")))
(~doc-subsection :title "Kept"
(~docs/subsection :title "Kept"
(ul :class "list-disc pl-5 space-y-1 text-stone-600 text-sm"
(li (code "defstyle") " — simplified to bind any value (string, function, etc.)")
(li (code "tw.css") " — the compiled Tailwind stylesheet, delivered via CSS class tracking")
@@ -67,7 +67,7 @@
(li "CSS class delivery (" (code "SX-Css") " headers, " (code "<style id=\"sx-css\">") ")")
(li "All component infrastructure (defcomp, caching, bundling, deps)")))
(~doc-subsection :title "Added"
(~docs/subsection :title "Added"
(p "Nothing. CSSX components are just " (code "defcomp") ". The only new thing is "
"a convention: components whose primary purpose is styling.")))))
@@ -76,26 +76,26 @@
;; Patterns
;; ---------------------------------------------------------------------------
(defcomp ~cssx-patterns-content ()
(~doc-page :title "Patterns"
(defcomp ~cssx/patterns-content ()
(~docs/page :title "Patterns"
(~doc-section :title "Class Mapping" :id "class-mapping"
(~docs/section :title "Class Mapping" :id "class-mapping"
(p "The simplest pattern: a component that maps semantic keywords to class strings.")
(highlight
"(defcomp ~btn (&key variant disabled &rest children)\n (button\n :class (str \"px-4 py-2 rounded font-medium transition \"\n (case variant\n \"primary\" \"bg-blue-600 text-white hover:bg-blue-700\"\n \"danger\" \"bg-red-600 text-white hover:bg-red-700\"\n \"ghost\" \"bg-transparent hover:bg-stone-100\"\n \"bg-stone-200 hover:bg-stone-300\")\n (when disabled \" opacity-50 cursor-not-allowed\"))\n :disabled disabled\n children))"
"(defcomp ~cssx/btn (&key variant disabled &rest children)\n (button\n :class (str \"px-4 py-2 rounded font-medium transition \"\n (case variant\n \"primary\" \"bg-blue-600 text-white hover:bg-blue-700\"\n \"danger\" \"bg-red-600 text-white hover:bg-red-700\"\n \"ghost\" \"bg-transparent hover:bg-stone-100\"\n \"bg-stone-200 hover:bg-stone-300\")\n (when disabled \" opacity-50 cursor-not-allowed\"))\n :disabled disabled\n children))"
"lisp")
(p "Consumers call " (code "(~btn :variant \"primary\" \"Submit\")") ". The Tailwind "
(p "Consumers call " (code "(~cssx/btn :variant \"primary\" \"Submit\")") ". The Tailwind "
"classes are readable in DevTools but never repeated across call sites."))
(~doc-section :title "Data-Driven Styling" :id "data-driven"
(~docs/section :title "Data-Driven Styling" :id "data-driven"
(p "Styling that responds to data values — impossible with static CSS:")
(highlight
"(defcomp ~metric (&key value label threshold)\n (let ((t (or threshold 10)))\n (div :class (str \"p-3 rounded font-bold \"\n (cond\n ((> value (* t 10)) \"bg-red-500 text-white\")\n ((> value t) \"bg-amber-200 text-amber-900\")\n (:else \"bg-green-100 text-green-800\")))\n (span :class \"text-sm\" label) \": \" (span (str value)))))"
"(defcomp ~cssx/metric (&key value label threshold)\n (let ((t (or threshold 10)))\n (div :class (str \"p-3 rounded font-bold \"\n (cond\n ((> value (* t 10)) \"bg-red-500 text-white\")\n ((> value t) \"bg-amber-200 text-amber-900\")\n (:else \"bg-green-100 text-green-800\")))\n (span :class \"text-sm\" label) \": \" (span (str value)))))"
"lisp")
(p "The component makes a " (em "decision") " about styling based on data. "
"No CSS preprocessor or class name convention can express \"red when value > 100\"."))
(~doc-section :title "Style Functions" :id "style-functions"
(~docs/section :title "Style Functions" :id "style-functions"
(p "Reusable style logic that returns class strings — no wrapping element needed:")
(highlight
"(define card-classes\n (fn (&key elevated bordered)\n (str \"rounded-lg p-4 \"\n (if elevated \"shadow-lg\" \"shadow-sm\")\n (when bordered \" border border-stone-200\"))))\n\n;; Usage:\n(div :class (card-classes :elevated true) ...)\n(article :class (card-classes :bordered true) ...)"
@@ -105,24 +105,24 @@
"(defstyle card-base \"rounded-lg p-4 shadow-sm\")\n(defstyle card-elevated \"rounded-lg p-4 shadow-lg\")\n\n(div :class card-base ...)"
"lisp"))
(~doc-section :title "Responsive Layouts" :id "responsive"
(~docs/section :title "Responsive Layouts" :id "responsive"
(p "Components that encode responsive breakpoints:")
(highlight
"(defcomp ~responsive-grid (&key cols &rest children)\n (div :class (str \"grid gap-4 \"\n (case (or cols 3)\n 1 \"grid-cols-1\"\n 2 \"grid-cols-1 md:grid-cols-2\"\n 3 \"grid-cols-1 md:grid-cols-2 lg:grid-cols-3\"\n 4 \"grid-cols-2 md:grid-cols-3 lg:grid-cols-4\"))\n children))"
"(defcomp ~cssx/responsive-grid (&key cols &rest children)\n (div :class (str \"grid gap-4 \"\n (case (or cols 3)\n 1 \"grid-cols-1\"\n 2 \"grid-cols-1 md:grid-cols-2\"\n 3 \"grid-cols-1 md:grid-cols-2 lg:grid-cols-3\"\n 4 \"grid-cols-2 md:grid-cols-3 lg:grid-cols-4\"))\n children))"
"lisp"))
(~doc-section :title "Emitting CSS Directly" :id "emitting-css"
(~docs/section :title "Emitting CSS Directly" :id "emitting-css"
(p "Components are not limited to referencing existing classes. They can generate "
"CSS — " (code "<style>") " tags, keyframes, custom properties — as part of their output:")
(highlight
"(defcomp ~pulse (&key color duration &rest children)\n (<>\n (style (str \"@keyframes sx-pulse {\"\n \"0%,100% { opacity:1 } 50% { opacity:.5 } }\"))\n (div :style (str \"animation: sx-pulse \" (or duration \"2s\") \" infinite;\"\n \"color:\" (or color \"inherit\"))\n children)))"
"(defcomp ~cssx/pulse (&key color duration &rest children)\n (<>\n (style (str \"@keyframes sx-pulse {\"\n \"0%,100% { opacity:1 } 50% { opacity:.5 } }\"))\n (div :style (str \"animation: sx-pulse \" (or duration \"2s\") \" infinite;\"\n \"color:\" (or color \"inherit\"))\n children)))"
"lisp")
(highlight
"(defcomp ~theme (&key primary surface &rest children)\n (<>\n (style (str \":root {\"\n \"--color-primary:\" (or primary \"#7c3aed\") \";\"\n \"--color-surface:\" (or surface \"#fafaf9\") \"}\"))\n children))"
"(defcomp ~cssx/theme (&key primary surface &rest children)\n (<>\n (style (str \":root {\"\n \"--color-primary:\" (or primary \"#7c3aed\") \";\"\n \"--color-surface:\" (or surface \"#fafaf9\") \"}\"))\n children))"
"lisp")
(p "The CSS strategy is the component's private implementation detail. Consumers call "
(code "(~pulse :color \"red\" \"Loading...\")") " or "
(code "(~theme :primary \"#2563eb\" ...)") " without knowing or caring whether the "
(code "(~cssx/pulse :color \"red\" \"Loading...\")") " or "
(code "(~cssx/theme :primary \"#2563eb\" ...)") " without knowing or caring whether the "
"component uses classes, inline styles, generated rules, or all three."))))
@@ -130,47 +130,47 @@
;; Async CSS
;; ---------------------------------------------------------------------------
(defcomp ~cssx-async-content ()
(~doc-page :title "Async CSS"
(defcomp ~cssx/async-content ()
(~docs/page :title "Async CSS"
(~doc-section :title "The Pattern" :id "pattern"
(~docs/section :title "The Pattern" :id "pattern"
(p "A CSSX component that needs CSS it doesn't have yet can "
(strong "fetch and cache it before rendering") ". This is just "
(code "~suspense") " combined with a style component — no new infrastructure:")
(code "~shared:pages/suspense") " combined with a style component — no new infrastructure:")
(highlight
"(defcomp ~styled (&key css-url css-hash fallback &rest children)\n (if (css-cached? css-hash)\n ;; Already have it — render immediately\n children\n ;; Don't have it — suspense while we fetch\n (~suspense :id (str \"css-\" css-hash)\n :fallback (or fallback (span \"\"))\n (do\n (fetch-css css-url css-hash)\n children))))"
"(defcomp ~cssx/styled (&key css-url css-hash fallback &rest children)\n (if (css-cached? css-hash)\n ;; Already have it — render immediately\n children\n ;; Don't have it — suspense while we fetch\n (~shared:pages/suspense :id (str \"css-\" css-hash)\n :fallback (or fallback (span \"\"))\n (do\n (fetch-css css-url css-hash)\n children))))"
"lisp")
(p "The consumer never knows:")
(highlight
"(~styled :css-url \"/css/charts.css\" :css-hash \"abc123\"\n (~bar-chart :data metrics))"
"(~cssx/styled :css-url \"/css/charts.css\" :css-hash \"abc123\"\n (~bar-chart :data metrics))"
"lisp"))
(~doc-section :title "Use Cases" :id "use-cases"
(~doc-subsection :title "Federated Components"
(p "A " (code "~btn") " from another site arrives via IPFS with a CID pointing "
(~docs/section :title "Use Cases" :id "use-cases"
(~docs/subsection :title "Federated Components"
(p "A " (code "~cssx/btn") " from another site arrives via IPFS with a CID pointing "
"to its CSS. The component fetches and caches it before rendering. "
"No coordination needed between sites.")
(highlight
"(defcomp ~federated-widget (&key cid &rest children)\n (let ((css-cid (str cid \"/style.css\"))\n (cached (css-cached? css-cid)))\n (if cached\n children\n (~suspense :id (str \"fed-\" cid)\n :fallback (div :class \"animate-pulse bg-stone-100 rounded h-20\")\n (do (fetch-css (str \"https://ipfs.io/ipfs/\" css-cid) css-cid)\n children)))))"
"(defcomp ~cssx/federated-widget (&key cid &rest children)\n (let ((css-cid (str cid \"/style.css\"))\n (cached (css-cached? css-cid)))\n (if cached\n children\n (~shared:pages/suspense :id (str \"fed-\" cid)\n :fallback (div :class \"animate-pulse bg-stone-100 rounded h-20\")\n (do (fetch-css (str \"https://ipfs.io/ipfs/\" css-cid) css-cid)\n children)))))"
"lisp"))
(~doc-subsection :title "Heavy UI Libraries"
(~docs/subsection :title "Heavy UI Libraries"
(p "Code editors, chart libraries, rich text editors — their CSS only loads "
"when the component actually appears on screen:")
(highlight
"(defcomp ~code-editor (&key language value on-change)\n (~styled :css-url \"/css/codemirror.css\" :css-hash (asset-hash \"codemirror\")\n :fallback (pre :class \"p-4 bg-stone-900 text-stone-300 rounded\" value)\n (div :class \"cm-editor\"\n :data-language language\n :data-value value)))"
"(defcomp ~cssx/code-editor (&key language value on-change)\n (~cssx/styled :css-url \"/css/codemirror.css\" :css-hash (asset-hash \"codemirror\")\n :fallback (pre :class \"p-4 bg-stone-900 text-stone-300 rounded\" value)\n (div :class \"cm-editor\"\n :data-language language\n :data-value value)))"
"lisp"))
(~doc-subsection :title "Lazy Themes"
(~docs/subsection :title "Lazy Themes"
(p "Theme CSS loads on first use, then is instant on subsequent visits:")
(highlight
"(defcomp ~lazy-theme (&key name &rest children)\n (let ((css-url (str \"/css/themes/\" name \".css\"))\n (hash (str \"theme-\" name)))\n (~styled :css-url css-url :css-hash hash\n :fallback children ;; render unstyled immediately\n children)))"
"(defcomp ~cssx/lazy-theme (&key name &rest children)\n (let ((css-url (str \"/css/themes/\" name \".css\"))\n (hash (str \"theme-\" name)))\n (~cssx/styled :css-url css-url :css-hash hash\n :fallback children ;; render unstyled immediately\n children)))"
"lisp")))
(~doc-section :title "How It Composes" :id "composition"
(~docs/section :title "How It Composes" :id "composition"
(p "Async CSS composes with everything already in SX:")
(ul :class "list-disc pl-5 space-y-1 text-stone-700"
(li (code "~suspense") " handles the async gap with fallback content")
(li (code "~shared:pages/suspense") " handles the async gap with fallback content")
(li "localStorage handles caching across sessions")
(li (code "<style id=\"sx-css\">") " is the injection target (same as CSS class delivery)")
(li "Component content-hashing tracks what the client has")
@@ -181,55 +181,55 @@
;; Live Styles
;; ---------------------------------------------------------------------------
(defcomp ~cssx-live-content ()
(~doc-page :title "Live Styles"
(defcomp ~cssx/live-content ()
(~docs/page :title "Live Styles"
(~doc-section :title "Styles That Respond to Events" :id "concept"
(~docs/section :title "Styles That Respond to Events" :id "concept"
(p "Combine " (code "~live") " (SSE) or " (code "~ws") " (WebSocket) with style "
"components, and you get styles that change in real-time in response to server "
"events. No new infrastructure — just components receiving data through a "
"persistent transport."))
(~doc-section :title "SSE: Live Theme Updates" :id "sse-theme"
(~docs/section :title "SSE: Live Theme Updates" :id "sse-theme"
(p "A " (code "~live") " component declares a persistent connection to an SSE "
"endpoint. When the server pushes a new event, " (code "resolveSuspense")
" replaces the content:")
(highlight
"(~live :src \"/api/stream/brand\"\n (~suspense :id \"theme\"\n (~theme :primary \"#7c3aed\" :surface \"#fafaf9\")))"
"(~live :src \"/api/stream/brand\"\n (~shared:pages/suspense :id \"theme\"\n (~cssx/theme :primary \"#7c3aed\" :surface \"#fafaf9\")))"
"lisp")
(p "Server pushes a new theme:")
(highlight
"event: sx-resolve\ndata: {\"id\": \"theme\", \"sx\": \"(~theme :primary \\\"#2563eb\\\" :surface \\\"#1e1e2e\\\")\"}"
"event: sx-resolve\ndata: {\"id\": \"theme\", \"sx\": \"(~cssx/theme :primary \\\"#2563eb\\\" :surface \\\"#1e1e2e\\\")\"}"
"text")
(p "The " (code "~theme") " component emits CSS custom properties. Everything "
(p "The " (code "~cssx/theme") " component emits CSS custom properties. Everything "
"using " (code "var(--color-primary)") " repaints instantly:")
(highlight
"(defcomp ~theme (&key primary surface)\n (style (str \":root {\"\n \"--color-primary:\" (or primary \"#7c3aed\") \";\"\n \"--color-surface:\" (or surface \"#fafaf9\") \"}\")))"
"(defcomp ~cssx/theme (&key primary surface)\n (style (str \":root {\"\n \"--color-primary:\" (or primary \"#7c3aed\") \";\"\n \"--color-surface:\" (or surface \"#fafaf9\") \"}\")))"
"lisp"))
(~doc-section :title "SSE: Live Dashboard Metrics" :id "sse-metrics"
(~docs/section :title "SSE: Live Dashboard Metrics" :id "sse-metrics"
(p "Style changes driven by live data — the component decides the visual treatment:")
(highlight
"(~live :src \"/api/stream/dashboard\"\n (~suspense :id \"cpu\"\n (~metric :value 0 :label \"CPU\" :threshold 80))\n (~suspense :id \"memory\"\n (~metric :value 0 :label \"Memory\" :threshold 90))\n (~suspense :id \"requests\"\n (~metric :value 0 :label \"RPS\" :threshold 1000)))"
"(~live :src \"/api/stream/dashboard\"\n (~shared:pages/suspense :id \"cpu\"\n (~cssx/metric :value 0 :label \"CPU\" :threshold 80))\n (~shared:pages/suspense :id \"memory\"\n (~cssx/metric :value 0 :label \"Memory\" :threshold 90))\n (~shared:pages/suspense :id \"requests\"\n (~cssx/metric :value 0 :label \"RPS\" :threshold 1000)))"
"lisp")
(p "Server pushes updated values. " (code "~metric") " turns red when "
(p "Server pushes updated values. " (code "~cssx/metric") " turns red when "
(code "value > threshold") " — the styling logic lives in the component, "
"not in CSS selectors or JavaScript event handlers."))
(~doc-section :title "WebSocket: Collaborative Design" :id "ws-design"
(~docs/section :title "WebSocket: Collaborative Design" :id "ws-design"
(p "Bidirectional channel for real-time collaboration. A designer adjusts a color, "
"all connected clients see the change:")
(highlight
"(~ws :src \"/ws/design-studio\"\n (~suspense :id \"canvas-theme\"\n (~theme :primary \"#7c3aed\")))"
"(~ws :src \"/ws/design-studio\"\n (~shared:pages/suspense :id \"canvas-theme\"\n (~cssx/theme :primary \"#7c3aed\")))"
"lisp")
(p "Client sends a color change:")
(highlight
";; Designer picks a new primary color\n(sx-send ws-conn '(theme-update :primary \"#dc2626\"))"
"lisp")
(p "Server broadcasts to all connected clients via " (code "sx-resolve") " — "
"every client's " (code "~theme") " component re-renders with the new color."))
"every client's " (code "~cssx/theme") " component re-renders with the new color."))
(~doc-section :title "Why This Works" :id "why"
(~docs/section :title "Why This Works" :id "why"
(p "Every one of these patterns is just a " (code "defcomp") " receiving data "
"through a persistent transport. The styling strategy — CSS custom properties, "
"class swaps, inline styles, " (code "<style>") " blocks — is the component's "
@@ -247,10 +247,10 @@
;; Comparison with CSS Technologies
;; ---------------------------------------------------------------------------
(defcomp ~cssx-comparison-content ()
(~doc-page :title "Comparisons"
(defcomp ~cssx/comparison-content ()
(~docs/page :title "Comparisons"
(~doc-section :title "styled-components / Emotion" :id "styled-components"
(~docs/section :title "styled-components / Emotion" :id "styled-components"
(p (a :href "https://styled-components.com" :class "text-violet-600 hover:underline" "styled-components")
" pioneered the idea that styling belongs in components. But it generates CSS "
"at runtime, injects " (code "<style>") " tags, and produces opaque hashed class "
@@ -263,26 +263,26 @@
" blocks, it's explicit — not hidden behind a tagged template literal. "
"And the DOM is always readable."))
(~doc-section :title "CSS Modules" :id "css-modules"
(~docs/section :title "CSS Modules" :id "css-modules"
(p (a :href "https://github.com/css-modules/css-modules" :class "text-violet-600 hover:underline" "CSS Modules")
" scope class names to avoid collisions by rewriting them at build time: "
(code ".button") " becomes " (code ".button_abc123")
". This solves the global namespace problem but creates the same opacity issue — "
"hashed names in the DOM that you can't grep for or reason about.")
(p "CSSX components don't need scoping because component boundaries already provide "
"isolation. A " (code "~btn") " owns its markup. There's nothing to collide with."))
"isolation. A " (code "~cssx/btn") " owns its markup. There's nothing to collide with."))
(~doc-section :title "Tailwind CSS" :id "tailwind"
(~docs/section :title "Tailwind CSS" :id "tailwind"
(p "Tailwind is " (em "complementary") ", not competitive. CSSX components are the "
"semantic layer on top. Raw Tailwind in markup — "
(code ":class \"px-4 py-2 bg-blue-600 text-white font-medium rounded hover:bg-blue-700\"")
" — is powerful but verbose and duplicated across call sites.")
(p "A CSSX component wraps that string once: " (code "(~btn :variant \"primary\" \"Submit\")")
(p "A CSSX component wraps that string once: " (code "(~cssx/btn :variant \"primary\" \"Submit\")")
". The Tailwind classes are still there, readable in DevTools, but consumers don't "
"repeat them. This is the same pattern Tailwind's own docs recommend ("
(em "\"extracting components\"") ") — CSSX components are just SX's native way of doing it."))
(~doc-section :title "Vanilla Extract" :id "vanilla-extract"
(~docs/section :title "Vanilla Extract" :id "vanilla-extract"
(p (a :href "https://vanilla-extract.style" :class "text-violet-600 hover:underline" "Vanilla Extract")
" is zero-runtime CSS-in-JS: styles are written in TypeScript, compiled to static "
"CSS at build time, and referenced by generated class names. It avoids the runtime "
@@ -293,7 +293,7 @@
"reference pre-built classes (zero runtime) " (em "or") " generate CSS on the fly — "
"same API either way."))
(~doc-section :title "Design Tokens / Style Dictionary" :id "design-tokens"
(~docs/section :title "Design Tokens / Style Dictionary" :id "design-tokens"
(p "The " (a :href "https://amzn.github.io/style-dictionary/" :class "text-violet-600 hover:underline" "Style Dictionary")
" pattern — a JSON/YAML file mapping token names to values, compiled to "
"platform-specific output — is essentially what the old CSSX was. It's the "
@@ -301,7 +301,7 @@
(p "The problem is that it's a parallel system: separate file format, separate build "
"pipeline, separate caching, separate tooling. CSSX components eliminate all of "
"that by expressing tokens as component parameters: "
(code "(~theme :primary \"#7c3aed\")") " instead of "
(code "(~cssx/theme :primary \"#7c3aed\")") " instead of "
(code "{\"color\": {\"primary\": {\"value\": \"#7c3aed\"}}}")
". Same result, no parallel infrastructure."))))
@@ -310,10 +310,10 @@
;; Philosophy
;; ---------------------------------------------------------------------------
(defcomp ~cssx-philosophy-content ()
(~doc-page :title "Philosophy"
(defcomp ~cssx/philosophy-content ()
(~docs/page :title "Philosophy"
(~doc-section :title "The Collapse" :id "collapse"
(~docs/section :title "The Collapse" :id "collapse"
(p "The web has spent two decades building increasingly complex CSS tooling: "
"preprocessors, CSS-in-JS, atomic CSS, utility frameworks, design tokens, style "
"dictionaries. Each solves a real problem but adds a new system with its own "
@@ -323,26 +323,26 @@
" That's what a component already is. There is no separate styling system because "
"there doesn't need to be."))
(~doc-section :title "Proof by Deletion" :id "proof"
(~docs/section :title "Proof by Deletion" :id "proof"
(p "The strongest validation: we built the full parallel system — style dictionary, "
"StyleValue type, content-addressed hashing, runtime injection, localStorage "
"caching — and then deleted it because nobody used it. The codebase already had "
"the answer: " (code "defcomp") " with " (code ":class") " strings.")
(p "3,000 lines of infrastructure removed. Zero lines added. Every use case still works."))
(~doc-section :title "The Right Abstraction Level" :id "abstraction"
(~docs/section :title "The Right Abstraction Level" :id "abstraction"
(p "CSS-in-JS puts styling " (em "below") " components — you style elements, then compose "
"them. Utility CSS puts styling " (em "beside") " components — classes in markup, logic "
"elsewhere. Both create a seam between what something does and how it looks.")
(p "CSSX components put styling " (em "inside") " components — at the same level as "
"structure and behavior. A " (code "~metric") " component knows its own thresholds, "
"structure and behavior. A " (code "~cssx/metric") " component knows its own thresholds, "
"its own color scheme, its own responsive behavior. Styling is just another "
"decision the component makes, not a separate concern."))
(~doc-section :title "Relationship to Other Plans" :id "relationships"
(~docs/section :title "Relationship to Other Plans" :id "relationships"
(ul :class "list-disc pl-5 space-y-2 text-stone-700"
(li (strong "Content-Addressed Components: ") "CSSX components get CIDs like any "
"other component. A " (code "~btn") " from one site can be shared to another via "
"other component. A " (code "~cssx/btn") " from one site can be shared to another via "
"IPFS. No " (code ":css-atoms") " manifest field needed — the component carries "
"its own styling logic.")
(li (strong "Isomorphic Rendering: ") "Components render the same on server and client. "
@@ -358,10 +358,10 @@
;; CSS Delivery
;; ---------------------------------------------------------------------------
(defcomp ~cssx-delivery-content ()
(~doc-page :title "CSS Delivery"
(defcomp ~cssx/delivery-content ()
(~docs/page :title "CSS Delivery"
(~doc-section :title "Multiple Strategies" :id "strategies"
(~docs/section :title "Multiple Strategies" :id "strategies"
(p "A CSSX component chooses its own styling strategy — and each strategy has its "
"own delivery path:")
(ul :class "list-disc pl-5 space-y-1 text-stone-700"
@@ -373,14 +373,14 @@
"directly. They arrive as part of the rendered HTML — keyframes, custom properties, "
"scoped rules, anything.")
(li (strong "External stylesheets: ") "Components can reference pre-loaded CSS files "
"or lazy-load them via " (code "~suspense") " (see " (a :href "/sx/(applications.(cssx.async))" "Async CSS") ").")
(li (strong "Custom properties: ") "A " (code "~theme") " component sets "
"or lazy-load them via " (code "~shared:pages/suspense") " (see " (a :href "/sx/(applications.(cssx.async))" "Async CSS") ").")
(li (strong "Custom properties: ") "A " (code "~cssx/theme") " component sets "
(code "--color-primary") " etc. via a " (code "<style>") " block. Everything "
"using " (code "var()") " repaints automatically."))
(p "The protocol below handles the Tailwind utility class case — the most common "
"strategy — but it's not the only game in town."))
(~doc-section :title "On-Demand Tailwind Delivery" :id "on-demand"
(~docs/section :title "On-Demand Tailwind Delivery" :id "on-demand"
(p "When components use Tailwind utility classes, SX ships " (em "only the CSS rules "
"actually used on the page") ", computed at render time. No build step, no purging, "
"no unused CSS.")
@@ -388,7 +388,7 @@
"startup. When a response is rendered, SX scans all " (code ":class") " values in "
"the output, looks up only those classes, and embeds the matching rules."))
(~doc-section :title "The Protocol" :id "protocol"
(~docs/section :title "The Protocol" :id "protocol"
(p "First page load gets the full set of used rules. Subsequent navigations send a "
"hash of what the client already has, and the server ships only the delta:")
(highlight
@@ -397,7 +397,7 @@
(p "The client merges new rules into the existing " (code "<style id=\"sx-css\">")
" block and updates its hash. No flicker, no duplicate rules."))
(~doc-section :title "Component-Aware Scanning" :id "scanning"
(~docs/section :title "Component-Aware Scanning" :id "scanning"
(p "CSS scanning happens at two levels:")
(ul :class "list-disc pl-5 space-y-1 text-stone-700"
(li (strong "Registration time: ") "When a component is defined via " (code "defcomp")
@@ -408,7 +408,7 @@
(p "This means the CSS registry knows roughly what a page needs before rendering, "
"and catches any stragglers after."))
(~doc-section :title "Trade-offs" :id "tradeoffs"
(~docs/section :title "Trade-offs" :id "tradeoffs"
(ul :class "list-disc pl-5 space-y-2 text-stone-700"
(li (strong "Full Tailwind in memory: ") "The parsed CSS registry is ~4MB. This is "
"a one-time startup cost per app instance.")

View File

@@ -9,7 +9,7 @@
;; "sx:route client+data" — cache miss, fetched from server
;; "sx:route client+cache" — cache hit, rendered from cached data
(defcomp ~data-test-content (&key (server-time :as string) (items :as list) (phase :as string) (transport :as string))
(defcomp ~data-test/content (&key (server-time :as string) (items :as list) (phase :as string) (transport :as string))
(div :class "space-y-8"
(div :class "border-b border-stone-200 pb-6"
(h1 :class "text-2xl font-bold text-stone-900" "Data Test")

View File

@@ -1,99 +1,99 @@
;; Docs page content — fully self-contained, no Python intermediaries
(defcomp ~sx-home-content ()
(defcomp ~docs-content/home-content ()
(div :id "main-content" :class "max-w-3xl mx-auto px-4 py-6"
(~doc-code :code (highlight (component-source "~sx-header") "lisp"))))
(~docs/code :code (highlight (component-source "~sx-header") "lisp"))))
(defcomp ~docs-introduction-content ()
(~doc-page :title "Introduction"
(~doc-section :title "What is sx?" :id "what"
(defcomp ~docs-content/docs-introduction-content ()
(~docs/page :title "Introduction"
(~docs/section :title "What is sx?" :id "what"
(p :class "text-stone-600"
"sx is an s-expression language for building web UIs. It combines htmx's server-first hypermedia approach with React's component model. The server sends s-expression source code over the wire. The client parses, evaluates, and renders it to DOM.")
(p :class "text-stone-600"
"The same evaluator runs on both server (Python) and client (JavaScript). Components defined once render identically in both environments."))
(~doc-section :title "Design decisions" :id "design"
(~docs/section :title "Design decisions" :id "design"
(p :class "text-stone-600"
"HTML elements are first-class: (div :class \"card\" (p \"hello\")) renders exactly what you'd expect. Components use defcomp with keyword parameters and optional children. The evaluator supports let bindings, conditionals, lambda, map/filter/reduce, and ~80 primitives.")
(p :class "text-stone-600"
"sx replaces the pattern of shipping a JS framework + build step + client-side router + state management library just to render some server data. For most applications, sx eliminates the need for JavaScript entirely — htmx attributes handle interactivity, hyperscript handles small behaviours, and the server handles everything else."))
(~doc-section :title "What sx is not" :id "not"
(~docs/section :title "What sx is not" :id "not"
(ul :class "space-y-2 text-stone-600"
(li "Not a general-purpose programming language — it's a UI rendering language")
(li "Not a full Lisp — it has macros, TCO, and delimited continuations, but no full call/cc")
(li "Not production-hardened at scale — it runs one website")))))
(defcomp ~docs-getting-started-content ()
(~doc-page :title "Getting Started"
(~doc-section :title "Minimal example" :id "minimal"
(defcomp ~docs-content/docs-getting-started-content ()
(~docs/page :title "Getting Started"
(~docs/section :title "Minimal example" :id "minimal"
(p :class "text-stone-600"
"An sx response is s-expression source code with content type text/sx:")
(~doc-code :code (highlight "(div :class \"p-4 bg-white rounded\"\n (h1 :class \"text-2xl font-bold\" \"Hello, world!\")\n (p \"This is rendered from an s-expression.\"))" "lisp"))
(~docs/code :code (highlight "(div :class \"p-4 bg-white rounded\"\n (h1 :class \"text-2xl font-bold\" \"Hello, world!\")\n (p \"This is rendered from an s-expression.\"))" "lisp"))
(p :class "text-stone-600"
"Add sx-get to any element to make it fetch and render sx:"))
(~doc-section :title "Hypermedia attributes" :id "attrs"
(~docs/section :title "Hypermedia attributes" :id "attrs"
(p :class "text-stone-600"
"Like htmx, sx adds attributes to HTML elements to trigger HTTP requests:")
(~doc-code :code (highlight "(button\n :sx-get \"/api/data\"\n :sx-target \"#result\"\n :sx-swap \"innerHTML\"\n \"Load data\")" "lisp"))
(~docs/code :code (highlight "(button\n :sx-get \"/api/data\"\n :sx-target \"#result\"\n :sx-swap \"innerHTML\"\n \"Load data\")" "lisp"))
(p :class "text-stone-600"
"sx-get, sx-post, sx-put, sx-delete, sx-patch — all work the same way. The response is parsed as sx and rendered into the target element."))))
(defcomp ~docs-components-content ()
(~doc-page :title "Components"
(~doc-section :title "defcomp" :id "defcomp"
(defcomp ~docs-content/docs-components-content ()
(~docs/page :title "Components"
(~docs/section :title "defcomp" :id "defcomp"
(p :class "text-stone-600"
"Components are defined with defcomp. They take keyword parameters and optional children:")
(~doc-code :code (highlight "(defcomp ~card (&key title subtitle &rest children)\n (div :class \"border rounded p-4\"\n (h2 :class \"font-bold\" title)\n (when subtitle (p :class \"text-stone-500\" subtitle))\n (div :class \"mt-3\" children)))" "lisp"))
(~docs/code :code (highlight "(defcomp ~docs-content/card (&key title subtitle &rest children)\n (div :class \"border rounded p-4\"\n (h2 :class \"font-bold\" title)\n (when subtitle (p :class \"text-stone-500\" subtitle))\n (div :class \"mt-3\" children)))" "lisp"))
(p :class "text-stone-600"
"Use components with the ~ prefix:")
(~doc-code :code (highlight "(~card :title \"My Card\" :subtitle \"A description\"\n (p \"First child\")\n (p \"Second child\"))" "lisp")))
(~doc-section :title "Component caching" :id "caching"
(~docs/code :code (highlight "(~docs-content/card :title \"My Card\" :subtitle \"A description\"\n (p \"First child\")\n (p \"Second child\"))" "lisp")))
(~docs/section :title "Component caching" :id "caching"
(p :class "text-stone-600"
"Component definitions are sent in a <script type=\"text/sx\" data-components> block. The client caches them in localStorage keyed by a content hash. On subsequent page loads, the client sends an SX-Components header listing what it has. The server only sends definitions the client is missing.")
(p :class "text-stone-600"
"This means the first page load sends all component definitions (~5-15KB). Subsequent navigations send zero component bytes — just the page content."))
(~doc-section :title "Parameters" :id "params"
(~docs/section :title "Parameters" :id "params"
(p :class "text-stone-600"
"&key declares keyword parameters. &rest children captures remaining positional arguments. Missing parameters evaluate to nil. Components always receive all declared parameters — use (when param ...) or (if param ... ...) to handle optional values."))))
(defcomp ~docs-evaluator-content ()
(~doc-page :title "Evaluator"
(~doc-section :title "Special forms" :id "special"
(defcomp ~docs-content/docs-evaluator-content ()
(~docs/page :title "Evaluator"
(~docs/section :title "Special forms" :id "special"
(p :class "text-stone-600"
"Special forms have lazy evaluation — arguments are not evaluated before the form runs:")
(~doc-code :code (highlight ";; Conditionals\n(if condition then-expr else-expr)\n(when condition body...)\n(cond (test1 body1) (test2 body2) (else default))\n\n;; Bindings\n(let ((name value) (name2 value2)) body...)\n(define name value)\n\n;; Functions\n(lambda (x y) (+ x y))\n(fn (x) (* x x))\n\n;; Sequencing\n(do expr1 expr2 expr3)\n(begin expr1 expr2)\n\n;; Threading\n(-> value (fn1 arg) (fn2 arg))" "lisp")))
(~doc-section :title "Higher-order forms" :id "higher"
(~docs/code :code (highlight ";; Conditionals\n(if condition then-expr else-expr)\n(when condition body...)\n(cond (test1 body1) (test2 body2) (else default))\n\n;; Bindings\n(let ((name value) (name2 value2)) body...)\n(define name value)\n\n;; Functions\n(lambda (x y) (+ x y))\n(fn (x) (* x x))\n\n;; Sequencing\n(do expr1 expr2 expr3)\n(begin expr1 expr2)\n\n;; Threading\n(-> value (fn1 arg) (fn2 arg))" "lisp")))
(~docs/section :title "Higher-order forms" :id "higher"
(p :class "text-stone-600"
"These operate on collections with function arguments:")
(~doc-code :code (highlight "(map (fn (x) (* x 2)) (list 1 2 3)) ;; => (2 4 6)\n(filter (fn (x) (> x 2)) (list 1 2 3 4 5)) ;; => (3 4 5)\n(reduce (fn (acc x) (+ acc x)) 0 (list 1 2 3)) ;; => 6\n(some (fn (x) (> x 3)) (list 1 2 3 4)) ;; => true\n(every? (fn (x) (> x 0)) (list 1 2 3)) ;; => true" "lisp")))))
(~docs/code :code (highlight "(map (fn (x) (* x 2)) (list 1 2 3)) ;; => (2 4 6)\n(filter (fn (x) (> x 2)) (list 1 2 3 4 5)) ;; => (3 4 5)\n(reduce (fn (acc x) (+ acc x)) 0 (list 1 2 3)) ;; => 6\n(some (fn (x) (> x 3)) (list 1 2 3 4)) ;; => true\n(every? (fn (x) (> x 0)) (list 1 2 3)) ;; => true" "lisp")))))
(defcomp ~docs-primitives-content (&key prims)
(~doc-page :title "Primitives"
(~doc-section :title "Built-in functions" :id "builtins"
(defcomp ~docs-content/docs-primitives-content (&key prims)
(~docs/page :title "Primitives"
(~docs/section :title "Built-in functions" :id "builtins"
(p :class "text-stone-600"
"sx provides ~80 built-in pure functions. They work identically on server (Python) and client (JavaScript).")
(div :class "space-y-6" prims))))
(defcomp ~docs-special-forms-content (&key forms)
(~doc-page :title "Special Forms"
(~doc-section :title "Syntactic constructs" :id "special-forms"
(defcomp ~docs-content/docs-special-forms-content (&key forms)
(~docs/page :title "Special Forms"
(~docs/section :title "Syntactic constructs" :id "special-forms"
(p :class "text-stone-600"
"Special forms are syntactic constructs whose arguments are NOT evaluated before dispatch. Each form has its own evaluation rules — unlike primitives, which receive pre-evaluated values. Together with primitives, special forms define the complete language surface.")
(p :class "text-stone-600"
"Forms marked with a tail position enable " (a :href "/sx/(etc.(essay.tail-call-optimization))" :class "text-violet-600 hover:underline" "tail-call optimization") " — recursive calls in tail position use constant stack space.")
(div :class "space-y-10" forms))))
(defcomp ~docs-server-rendering-content ()
(~doc-page :title "Server Rendering"
(~doc-section :title "Python API" :id "python"
(defcomp ~docs-content/docs-server-rendering-content ()
(~docs/page :title "Server Rendering"
(~docs/section :title "Python API" :id "python"
(p :class "text-stone-600"
"The server-side sx library provides several entry points for rendering:")
(~doc-code :code (highlight "from shared.sx.helpers import sx_page, sx_response, sx_call\nfrom shared.sx.parser import SxExpr\n\n# Build a component call from Python kwargs\nsx_call(\"card\", title=\"Hello\", subtitle=\"World\")\n\n# Return an sx wire-format response\nreturn sx_response(sx_call(\"card\", title=\"Hello\"))\n\n# Return a full HTML page shell with sx boot\nreturn sx_page(ctx, page_sx)" "python")))
(~doc-section :title "sx_call" :id "sx-call"
(~docs/code :code (highlight "from shared.sx.helpers import sx_page, sx_response, sx_call\nfrom shared.sx.parser import SxExpr\n\n# Build a component call from Python kwargs\nsx_call(\"card\", title=\"Hello\", subtitle=\"World\")\n\n# Return an sx wire-format response\nreturn sx_response(sx_call(\"card\", title=\"Hello\"))\n\n# Return a full HTML page shell with sx boot\nreturn sx_page(ctx, page_sx)" "python")))
(~docs/section :title "sx_call" :id "sx-call"
(p :class "text-stone-600"
"sx_call converts Python kwargs to an s-expression component call. Snake_case becomes kebab-case. SxExpr values are inlined without quoting. None becomes nil. Bools become true/false."))
(~doc-section :title "sx_response" :id "sx-response"
(~docs/section :title "sx_response" :id "sx-response"
(p :class "text-stone-600"
"sx_response returns a Quart Response with content type text/sx. It prepends missing component definitions, scans for CSS classes, and sets SX-Css-Hash and SX-Css-Add headers."))
(~doc-section :title "sx_page" :id "sx-page"
(~docs/section :title "sx_page" :id "sx-page"
(p :class "text-stone-600"
"sx_page returns a minimal HTML document that boots the page from sx source. The browser loads component definitions and page sx from inline <script> tags, then sx.js renders everything client-side. CSS rules are pre-scanned and injected."))))

View File

@@ -1,18 +1,18 @@
;; SX docs utility components
(defcomp ~doc-placeholder (&key (id :as string))
(defcomp ~docs/placeholder (&key (id :as string))
(div :id id
(div :class "bg-stone-100 rounded p-4 mt-3"
(p :class "text-stone-400 italic text-sm"
"Trigger the demo to see the actual content."))))
(defcomp ~doc-oob-code (&key (target-id :as string) (text :as string))
(defcomp ~docs/oob-code (&key (target-id :as string) (text :as string))
(div :id target-id :sx-swap-oob "innerHTML"
(div :class "not-prose bg-stone-100 rounded p-4 mt-3"
(pre :class "text-sm whitespace-pre-wrap break-words"
(code text)))))
(defcomp ~doc-attr-table (&key (title :as string) rows)
(defcomp ~docs/attr-table (&key (title :as string) rows)
(div :class "space-y-3"
(h3 :class "text-xl font-semibold text-stone-700" title)
(div :class "overflow-x-auto rounded border border-stone-200"
@@ -23,7 +23,7 @@
(th :class "px-3 py-2 font-medium text-stone-600 text-center w-20" "In sx?")))
(tbody rows)))))
(defcomp ~doc-headers-table (&key (title :as string) rows)
(defcomp ~docs/headers-table (&key (title :as string) rows)
(div :class "space-y-3"
(h3 :class "text-xl font-semibold text-stone-700" title)
(div :class "overflow-x-auto rounded border border-stone-200"
@@ -34,7 +34,7 @@
(th :class "px-3 py-2 font-medium text-stone-600" "Description")))
(tbody rows)))))
(defcomp ~doc-headers-row (&key (name :as string) (value :as string) (description :as string) (href :as string?))
(defcomp ~docs/headers-row (&key (name :as string) (value :as string) (description :as string) (href :as string?))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm whitespace-nowrap"
(if href
@@ -46,7 +46,7 @@
(td :class "px-3 py-2 font-mono text-sm text-stone-500" value)
(td :class "px-3 py-2 text-stone-700 text-sm" description)))
(defcomp ~doc-two-col-row (&key (name :as string) (description :as string) (href :as string?))
(defcomp ~docs/two-col-row (&key (name :as string) (description :as string) (href :as string?))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm whitespace-nowrap"
(if href
@@ -57,7 +57,7 @@
(span :class "text-violet-700" name)))
(td :class "px-3 py-2 text-stone-700 text-sm" description)))
(defcomp ~doc-two-col-table (&key (title :as string?) (intro :as string?) (col1 :as string?) (col2 :as string?) rows)
(defcomp ~docs/two-col-table (&key (title :as string?) (intro :as string?) (col1 :as string?) (col2 :as string?) rows)
(div :class "space-y-3"
(when title (h3 :class "text-xl font-semibold text-stone-700" title))
(when intro (p :class "text-stone-600 mb-6" intro))
@@ -68,10 +68,10 @@
(th :class "px-3 py-2 font-medium text-stone-600" (or col2 "Description"))))
(tbody rows)))))
(defcomp ~sx-docs-label ()
(defcomp ~docs/label ()
(span :class "font-mono" "(<sx>)"))
(defcomp ~doc-clear-cache-btn ()
(defcomp ~docs/clear-cache-btn ()
(button :onclick "localStorage.removeItem('sx-components-hash');localStorage.removeItem('sx-components-src');var e=Sx.getEnv();Object.keys(e).forEach(function(k){if(k.charAt(0)==='~')delete e[k]});var b=this;b.textContent='Cleared!';setTimeout(function(){b.textContent='Clear component cache'},2000)"
:class "text-xs text-stone-400 hover:text-stone-600 border border-stone-200 rounded px-2 py-1 transition-colors"
"Clear component cache"))
@@ -82,10 +82,10 @@
;; Build attr table from a list of {name, desc, exists, href} dicts.
;; Replaces _attr_table_sx() in utils.py.
(defcomp ~doc-attr-table-from-data (&key (title :as string) (attrs :as list))
(~doc-attr-table :title title
(defcomp ~docs/attr-table-from-data (&key (title :as string) (attrs :as list))
(~docs/attr-table :title title
:rows (<> (map (fn (a)
(~doc-attr-row
(~docs/attr-row
:attr (get a "name")
:description (get a "desc")
:exists (get a "exists")
@@ -94,10 +94,10 @@
;; Build headers table from a list of {name, value, desc} dicts.
;; Replaces _headers_table_sx() in utils.py.
(defcomp ~doc-headers-table-from-data (&key (title :as string) (headers :as list))
(~doc-headers-table :title title
(defcomp ~docs/headers-table-from-data (&key (title :as string) (headers :as list))
(~docs/headers-table :title title
:rows (<> (map (fn (h)
(~doc-headers-row
(~docs/headers-row
:name (get h "name")
:value (get h "value")
:description (get h "desc")
@@ -106,10 +106,10 @@
;; Build two-col table from a list of {name, desc} dicts.
;; Replaces the _reference_events_sx / _reference_js_api_sx builders.
(defcomp ~doc-two-col-table-from-data (&key (title :as string?) (intro :as string?) (col1 :as string?) (col2 :as string?) (items :as list))
(~doc-two-col-table :title title :intro intro :col1 col1 :col2 col2
(defcomp ~docs/two-col-table-from-data (&key (title :as string?) (intro :as string?) (col1 :as string?) (col2 :as string?) (items :as list))
(~docs/two-col-table :title title :intro intro :col1 col1 :col2 col2
:rows (<> (map (fn (item)
(~doc-two-col-row
(~docs/two-col-row
:name (get item "name")
:description (get item "desc")
:href (get item "href")))
@@ -117,27 +117,27 @@
;; Build all primitives category tables from a {category: [prim, ...]} dict.
;; Replaces _primitives_section_sx() in utils.py.
(defcomp ~doc-primitives-tables (&key (primitives :as dict))
(defcomp ~docs/primitives-tables (&key (primitives :as dict))
(<> (map (fn (cat)
(~doc-primitives-table
(~docs/primitives-table
:category cat
:primitives (get primitives cat)))
(keys primitives))))
;; Build all special form category sections from a {category: [form, ...]} dict.
(defcomp ~doc-special-forms-tables (&key (forms :as dict))
(defcomp ~docs/special-forms-tables (&key (forms :as dict))
(<> (map (fn (cat)
(~doc-special-forms-category
(~docs/special-forms-category
:category cat
:forms (get forms cat)))
(keys forms))))
(defcomp ~doc-special-forms-category (&key (category :as string) (forms :as list))
(defcomp ~docs/special-forms-category (&key (category :as string) (forms :as list))
(div :class "space-y-4"
(h3 :class "text-xl font-semibold text-stone-800 border-b border-stone-200 pb-2" category)
(div :class "space-y-4"
(map (fn (f)
(~doc-special-form-card
(~docs/special-form-card
:name (get f "name")
:syntax (get f "syntax")
:doc (get f "doc")
@@ -145,7 +145,7 @@
:example (get f "example")))
forms))))
(defcomp ~doc-special-form-card (&key (name :as string) (syntax :as string) (doc :as string) (tail-position :as string) (example :as string))
(defcomp ~docs/special-form-card (&key (name :as string) (syntax :as string) (doc :as string) (tail-position :as string) (example :as string))
(div :class "not-prose border border-stone-200 rounded-lg p-4 space-y-3"
(div :class "flex items-baseline gap-3"
(code :class "text-lg font-bold text-violet-700" name)
@@ -159,4 +159,4 @@
(p :class "text-xs text-stone-500"
(span :class "font-semibold" "Tail position: ") tail-position))
(when (not (= example ""))
(~doc-code :code (highlight example "lisp")))))
(~docs/code :code (highlight example "lisp")))))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -2,11 +2,11 @@
;; The Hegelian Synthesis of Hypertext and Reactivity
;; ---------------------------------------------------------------------------
(defcomp ~essay-hegelian-synthesis ()
(~doc-page :title "The Hegelian Synthesis"
(defcomp ~essays/hegelian-synthesis/essay-hegelian-synthesis ()
(~docs/page :title "The Hegelian Synthesis"
(p :class "text-stone-500 text-sm italic mb-8"
"On the dialectical resolution of the hypertext/reactive contradiction.")
(~doc-section :title "I. Thesis: The server renders" :id "thesis"
(~docs/section :title "I. Thesis: The server renders" :id "thesis"
(p :class "text-stone-600"
"In the beginning was the hyperlink. The web was born as a system of documents connected by references. A page was a " (em "representation") " — complete, self-contained, delivered whole by the server. The browser was a thin client. It received, it rendered, it followed links. The server was the sole author of state.")
(p :class "text-stone-600"
@@ -19,7 +19,7 @@
"The beauty of the thesis is its simplicity. One source of truth. One rendering pipeline. No synchronisation problems. No stale state. No " (code "useEffect") " cleanup. Every request produces a fresh representation, and the representation " (em "is") " the application. There is nothing hidden behind it, no shadow state, no ghost in the machine.")
(p :class "text-stone-600"
"But the thesis has a limit. The server cannot know everything the client experiences. It cannot know that the user's mouse is hovering over a button. It cannot know that a drag is in progress. It cannot know that a counter should increment " (em "now") ", this millisecond, without a round trip. The thesis renders the world from the server's perspective — and the client's perspective, its " (em "Erlebnis") ", its lived experience, is absent."))
(~doc-section :title "II. Antithesis: The client reacts" :id "antithesis"
(~docs/section :title "II. Antithesis: The client reacts" :id "antithesis"
(p :class "text-stone-600"
"React arrived as the negation of the server-rendered web. Where the thesis said " (em "the server knows") ", React said " (em "the client knows better") ". Where the thesis treated the browser as a display surface, React treated it as an application runtime. Where the thesis sent documents, React sent " (em "programs") ".")
(p :class "text-stone-600"
@@ -32,7 +32,7 @@
"Worse, the antithesis destroys what the thesis had achieved. The representation is no longer self-contained. A React SPA sends a JavaScript bundle — a program, not a document. The server sends an empty " (code "<div id=\"root\">") " and a prayer. The browser must compile, execute, fetch data, and construct the interface from scratch. The document — the web's primordial unit of meaning — is hollowed out. What arrives is not a representation but an " (em "instruction to construct one") ".")
(p :class "text-stone-600"
"Hegel would diagnose this as the antithesis's characteristic failure: it achieves freedom (client autonomy) at the cost of substance (server authority). The SPA is the " (em "beautiful soul") " of web development — pure subjectivity that has cut itself off from the objective world and wonders why everything is so complicated."))
(~doc-section :title "III. The contradiction in practice" :id "contradiction"
(~docs/section :title "III. The contradiction in practice" :id "contradiction"
(p :class "text-stone-600"
"The practical manifestation of the dialectic is visible in every web team's daily life. The server-rendered camp says: " (em "just use HTML and htmx, it's simpler") ". The React camp says: " (em "you can't build a real app without client state") ". Both are correct. Both are incomplete.")
(p :class "text-stone-600"
@@ -41,7 +41,7 @@
"The React camp cannot deliver a page without JavaScript. Cannot render on first load without a server-rendering framework bolted on top. Cannot cache a representation because there is no stable representation to cache. Cannot inspect a page without devtools because the document is an empty shell. Every improvement — server components, streaming SSR, partial hydration — is an attempt to recover what the thesis already had: server-authored, self-contained documents.")
(p :class "text-stone-600"
"The two camps are not in disagreement about different things. They are in disagreement about " (em "the same thing") ": where should state live? The thesis says: on the server. The antithesis says: on the client. Neither can accommodate the obvious truth that " (strong "some state belongs on the server and some belongs on the client") ", and that a coherent architecture must handle both without privileging either."))
(~doc-section :title "IV. Synthesis: The island in the lake" :id "synthesis"
(~docs/section :title "IV. Synthesis: The island in the lake" :id "synthesis"
(p :class "text-stone-600"
"Hegel's dialectic does not end in compromise. The synthesis is not half-thesis, half-antithesis. It is a new category that " (em "sublates") " — " (em "aufhebt") " — both: preserving what is true in each while resolving the contradiction between them. The synthesis contains the thesis and antithesis as " (em "moments") " within a higher unity.")
(p :class "text-stone-600"
@@ -50,12 +50,12 @@
"But the crucial move — the one that makes this a genuine Hegelian synthesis rather than a mere juxtaposition — is " (strong "the morph") ". When the server sends new content and the client merges it into the existing DOM, hydrated islands are " (em "preserved") ". The server updates the lake. The islands keep their state. The server's new representation flows around the islands like water around rocks. The client's interiority survives the server's authority.")
(p :class "text-stone-600"
"This is " (em "Aufhebung") " in its precise meaning: cancellation, preservation, and elevation. The thesis (server authority) is " (em "cancelled") " — the server no longer has total control over the page. It is " (em "preserved") " — the server still renders the document, still determines structure, still delivers representations. It is " (em "elevated") " — the server now renders " (em "around") " reactive islands, acknowledging their autonomy. Simultaneously, the antithesis (client autonomy) is cancelled (the client no longer controls the whole page), preserved (islands keep their state), and elevated (client state now coexists with server-driven updates).")
(~doc-code :code (highlight ";; The island: reactive state coexists with server lakes.\n;; Lakes are server-morphable slots — the water within the island.\n\n(defisland ~sx-header ()\n (let ((families (list \"violet\" \"rose\" \"blue\" \"emerald\"))\n (idx (signal 0))\n (current (computed (fn ()\n (nth families (mod (deref idx) (len families)))))))\n (a :href \"/\" :sx-get \"/\" :sx-target \"#main-panel\"\n ;; Lake: server can update the logo\n (lake :id \"logo\"\n (span :style (cssx ...) \"(<sx>)\"))\n ;; Reactive: signal-bound, NOT in a lake\n (span :style (cssx (:text (colour (deref current) 500)))\n :on-click (fn (e) (swap! idx inc))\n \"reactive\")\n ;; Lake: server can update the copyright\n (lake :id \"copyright\"\n (p \"© 2026\")))))\n\n;; Click: colour changes (client state)\n;; Server sends new page — morph enters the island\n;; Lakes update from server content\n;; Reactive span keeps its colour — state survives" "lisp"))
(~docs/code :code (highlight ";; The island: reactive state coexists with server lakes.\n;; Lakes are server-morphable slots — the water within the island.\n\n(defisland ~essays/hegelian-synthesis/header ()\n (let ((families (list \"violet\" \"rose\" \"blue\" \"emerald\"))\n (idx (signal 0))\n (current (computed (fn ()\n (nth families (mod (deref idx) (len families)))))))\n (a :href \"/\" :sx-get \"/\" :sx-target \"#main-panel\"\n ;; Lake: server can update the logo\n (lake :id \"logo\"\n (span :style (cssx ...) \"(<sx>)\"))\n ;; Reactive: signal-bound, NOT in a lake\n (span :style (cssx (:text (colour (deref current) 500)))\n :on-click (fn (e) (swap! idx inc))\n \"reactive\")\n ;; Lake: server can update the copyright\n (lake :id \"copyright\"\n (p \"© 2026\")))))\n\n;; Click: colour changes (client state)\n;; Server sends new page — morph enters the island\n;; Lakes update from server content\n;; Reactive span keeps its colour — state survives" "lisp"))
(p :class "text-stone-600"
"The " (code "lake") " tag is the key. Inside the island, " (code "(lake :id \"logo\" ...)") " marks a region as server territory — the server can update its content during a morph. The reactive " (code "span") " with its signal-bound style is " (em "not") " in a lake — it is island territory, untouchable by the morph. The morph enters the island, finds the lakes, updates them from the server's new representation, and " (em "flows around") " the reactive nodes like water around rocks.")
(p :class "text-stone-600"
"Click the word " (em "reactive") " in the header. The colour changes. Navigate to another page. The morph enters the island, updates the lakes (logo, copyright), but the reactive span — with its colour signal — " (em "persists") ". The client's inner life survives the server's outer renewal."))
(~doc-section :title "V. Spirit: The self-knowing page" :id "spirit"
(~docs/section :title "V. Spirit: The self-knowing page" :id "spirit"
(p :class "text-stone-600"
"Hegel's system does not end with synthesis. Synthesis becomes a new thesis, which generates its own antithesis, and the dialectic continues. The island architecture is not a final resting place. It is a " (em "moment") " in the self-development of the web.")
(p :class "text-stone-600"
@@ -66,7 +66,7 @@
"The morph algorithm is the phenomenological crux. It is the mechanism by which the page achieves continuity of experience through discontinuity of content. The server sends a completely new representation — new HTML, new structure, new text. The morph walks the old and new DOM trees, reconciling them. Where it finds an island — a locus of client subjectivity — it preserves it. Where it finds static content — server substance — it updates it. The result is a page that is simultaneously the same (the island's state persists) and different (the surrounding content has changed).")
(p :class "text-stone-600"
"This is Hegel's " (em "identity of identity and difference") ". The page after the morph is the same page (same islands, same signals, same DOM nodes) and a different page (new server content, new navigation state, new URL). The dialectic is not resolved by eliminating one side. It is resolved by maintaining both simultaneously — and the morph is the concrete mechanism that achieves this."))
(~doc-section :title "VI. The speculative proposition" :id "speculative"
(~docs/section :title "VI. The speculative proposition" :id "speculative"
(p :class "text-stone-600"
"Hegel distinguished " (em "ordinary") " propositions from " (em "speculative") " ones. An ordinary proposition has a fixed subject and a predicate attached to it from outside: " (em "the rose is red") ". A speculative proposition is one where the predicate reflects back on the subject and transforms it: " (em "the actual is the rational") ".")
(p :class "text-stone-600"

View File

@@ -1,2 +1,2 @@
(defcomp ~essay-htmx-react-hybrid ()
(~doc-page :title "The htmx/React Hybrid" (~doc-section :title "Two good ideas" :id "ideas" (p :class "text-stone-600" "htmx: the server should render HTML. The client should swap it in. No client-side routing. No virtual DOM. No state management.") (p :class "text-stone-600" "React: UI should be composed from reusable components with parameters. Components encapsulate structure, style, and behavior.") (p :class "text-stone-600" "sx tries to combine both: server-rendered s-expressions with hypermedia attributes AND a component model with caching and composition.")) (~doc-section :title "What sx keeps from htmx" :id "from-htmx" (ul :class "space-y-2 text-stone-600" (li "Server generates the UI — no client-side data fetching or state") (li "Hypermedia attributes (sx-get, sx-target, sx-swap) on any element") (li "Partial page updates via swap/OOB — no full page reloads") (li "Works with standard HTTP — no WebSocket or custom protocol required"))) (~doc-section :title "What sx adds from React" :id "from-react" (ul :class "space-y-2 text-stone-600" (li "defcomp — named, parameterized, composable components") (li "Client-side rendering — server sends source, client renders DOM") (li "Component caching — definitions cached in localStorage across navigations") (li "On-demand CSS — only ship the rules that are used"))) (~doc-section :title "What sx gives up" :id "gives-up" (ul :class "space-y-2 text-stone-600" (li "No HTML output — sx sends s-expressions, not HTML. JS required.") (li "Custom parser — the client needs sx.js to understand responses") (li "Niche — no ecosystem, no community, no third-party support") (li "Learning curve — s-expression syntax is unfamiliar to most web developers")))))
(defcomp ~essays/htmx-react-hybrid/essay-htmx-react-hybrid ()
(~docs/page :title "The htmx/React Hybrid" (~docs/section :title "Two good ideas" :id "ideas" (p :class "text-stone-600" "htmx: the server should render HTML. The client should swap it in. No client-side routing. No virtual DOM. No state management.") (p :class "text-stone-600" "React: UI should be composed from reusable components with parameters. Components encapsulate structure, style, and behavior.") (p :class "text-stone-600" "sx tries to combine both: server-rendered s-expressions with hypermedia attributes AND a component model with caching and composition.")) (~docs/section :title "What sx keeps from htmx" :id "from-htmx" (ul :class "space-y-2 text-stone-600" (li "Server generates the UI — no client-side data fetching or state") (li "Hypermedia attributes (sx-get, sx-target, sx-swap) on any element") (li "Partial page updates via swap/OOB — no full page reloads") (li "Works with standard HTTP — no WebSocket or custom protocol required"))) (~docs/section :title "What sx adds from React" :id "from-react" (ul :class "space-y-2 text-stone-600" (li "defcomp — named, parameterized, composable components") (li "Client-side rendering — server sends source, client renders DOM") (li "Component caching — definitions cached in localStorage across navigations") (li "On-demand CSS — only ship the rules that are used"))) (~docs/section :title "What sx gives up" :id "gives-up" (ul :class "space-y-2 text-stone-600" (li "No HTML output — sx sends s-expressions, not HTML. JS required.") (li "Custom parser — the client needs sx.js to understand responses") (li "Niche — no ecosystem, no community, no third-party support") (li "Learning curve — s-expression syntax is unfamiliar to most web developers")))))

View File

@@ -1,5 +1,5 @@
(defcomp ~essays-index-content ()
(~doc-page :title "Essays"
(defcomp ~essays/index/essays-index-content ()
(~docs/page :title "Essays"
(div :class "space-y-4"
(p :class "text-lg text-stone-600 mb-4"
"Opinions, rationales, and explorations around SX and the ideas behind it.")

View File

@@ -1,15 +1,15 @@
(defcomp ~essay-no-alternative ()
(~doc-page :title "There Is No Alternative"
(defcomp ~essays/no-alternative/essay-no-alternative ()
(~docs/page :title "There Is No Alternative"
(p :class "text-stone-500 text-sm italic mb-8"
"Every attempt to escape s-expressions leads back to s-expressions. This is not an accident.")
(~doc-section :title "The claim" :id "claim"
(~docs/section :title "The claim" :id "claim"
(p :class "text-stone-600"
"SX uses s-expressions. When people encounter this, the first reaction is usually: " (em "why not use something more modern?") " Fair question. The answer is that there is nothing more modern. There are only things that are more " (em "familiar") " — and familiarity is not the same as fitness.")
(p :class "text-stone-600"
"This essay examines what SX actually needs from its representation, surveys the alternatives, and shows that every candidate either fails to meet the requirements or converges toward s-expressions under a different name. The conclusion is uncomfortable but unavoidable: for what SX does, there is no alternative."))
(~doc-section :title "The requirements" :id "requirements"
(~docs/section :title "The requirements" :id "requirements"
(p :class "text-stone-600"
"SX is not just a templating language. It is a language that serves simultaneously as:")
(ul :class "space-y-2 text-stone-600"
@@ -30,9 +30,9 @@
(li (strong "Token-efficient") " — minimal syntactic overhead, because the representation travels over the network and is processed by LLMs with finite context windows")
(li (strong "Composable by nesting") " — no special composition mechanisms, because the same operation (putting a list inside a list) must work for markup, logic, and data")))
(~doc-section :title "The candidates" :id "candidates"
(~docs/section :title "The candidates" :id "candidates"
(~doc-subsection :title "XML / HTML"
(~docs/subsection :title "XML / HTML"
(p :class "text-stone-600"
"The obvious first thought. XML is a tree. HTML is markup. Why not use angle brackets?")
(p :class "text-stone-600"
@@ -42,16 +42,16 @@
(p :class "text-stone-600"
"XSLT attempted to make XML a programming language. The result is universally regarded as a cautionary tale. Trying to express conditionals and iteration in a format designed for document markup produces something that is bad at both."))
(~doc-subsection :title "JSON"
(~docs/subsection :title "JSON"
(p :class "text-stone-600"
"JSON is data notation. It has objects, arrays, strings, numbers, booleans, and null. It parses in one pass. It validates structurally. It is ubiquitous.")
(p :class "text-stone-600"
"JSON is not homoiconic because it has no concept of evaluation. It is " (em "inert") " data. To make JSON a programming language, you must invent a convention for representing code — and every such convention reinvents s-expressions with worse ergonomics:")
(~doc-code :code (highlight ";; JSON \"code\" (actual example from various JSON-based DSLs)\n{\"if\": [{\">\": [\"$.count\", 0]},\n {\"map\": [\"$.items\", {\"fn\": [\"item\", {\"get\": [\"item\", \"name\"]}]}]},\n {\"literal\": \"No items\"}]}\n\n;; The same thing in s-expressions\n(if (> count 0)\n (map (fn (item) (get item \"name\")) items)\n \"No items\")" "lisp"))
(~docs/code :code (highlight ";; JSON \"code\" (actual example from various JSON-based DSLs)\n{\"if\": [{\">\": [\"$.count\", 0]},\n {\"map\": [\"$.items\", {\"fn\": [\"item\", {\"get\": [\"item\", \"name\"]}]}]},\n {\"literal\": \"No items\"}]}\n\n;; The same thing in s-expressions\n(if (> count 0)\n (map (fn (item) (get item \"name\")) items)\n \"No items\")" "lisp"))
(p :class "text-stone-600"
"The JSON version is an s-expression encoded in JSON's syntax — lists-of-lists with a head element that determines semantics. It has strictly more punctuation (colons, commas, braces, brackets, quotes around keys) and strictly less readability. Every JSON-based DSL that reaches sufficient complexity converges on this pattern and then wishes it had just used s-expressions."))
(~doc-subsection :title "YAML"
(~docs/subsection :title "YAML"
(p :class "text-stone-600"
"YAML is the other common data notation. It adds indentation sensitivity, anchors, aliases, multi-line strings, type coercion, and a " (a :href "https://yaml.org/spec/1.2.2/" :class "text-violet-600 hover:underline" "specification") " that is 240 pages long. The spec for SX's parser is 200 lines.")
(p :class "text-stone-600"
@@ -59,7 +59,7 @@
(p :class "text-stone-600"
"YAML is not homoiconic. It has no evaluation model. Like JSON, any attempt to encode logic in YAML produces s-expressions with worse syntax."))
(~doc-subsection :title "JSX / Template literals"
(~docs/subsection :title "JSX / Template literals"
(p :class "text-stone-600"
"JSX is the closest mainstream technology to what SX does — it embeds markup in a programming language. But JSX is not a representation; it is a compile target. " (code "<Card title=\"Hi\">content</Card>") " compiles to " (code "React.createElement(Card, {title: \"Hi\"}, \"content\")") ". The angle-bracket syntax is sugar that does not survive to runtime.")
(p :class "text-stone-600"
@@ -67,7 +67,7 @@
(p :class "text-stone-600"
"Template literals (tagged templates in JavaScript, Jinja, ERB, etc.) are string interpolation. They embed code in strings or strings in code, depending on which layer you consider primary. Neither direction produces a homoiconic representation. You cannot write a macro that reads a template literal and transforms it as data, because the template literal is a string — opaque, uninspectable, and unstructured."))
(~doc-subsection :title "Tcl"
(~docs/subsection :title "Tcl"
(p :class "text-stone-600"
"Tcl is the most interesting near-miss. \"Everything is a string\" is a radical simplification. The syntax is minimal: commands are words separated by spaces, braces group without substitution, brackets evaluate. Tcl is effectively homoiconic — code is strings, strings are code, and " (code "eval") " is the universal mechanism.")
(p :class "text-stone-600"
@@ -75,7 +75,7 @@
(p :class "text-stone-600"
"Tcl also lacks native tree structure. Lists are flat strings that are parsed on demand. Nested structure exists by convention, not by grammar. This makes composition more fragile than s-expressions, where nesting is the fundamental structural primitive."))
(~doc-subsection :title "Rebol / Red"
(~docs/subsection :title "Rebol / Red"
(p :class "text-stone-600"
"Rebol is the strongest alternative. It is homoiconic — code is data. It has minimal syntax. It has dialecting — the ability to create domain-specific languages within the language. It is a single representation for code, data, and markup. " (a :href "https://en.wikipedia.org/wiki/Rebol" :class "text-violet-600 hover:underline" "Carl Sassenrath") " designed it explicitly to solve the problems that SX also targets.")
(p :class "text-stone-600"
@@ -83,7 +83,7 @@
(p :class "text-stone-600"
"Rebol demonstrates that the design space around s-expressions has room for variation. But the variations add complexity without adding expressiveness — and in the current landscape, complexity kills AI compatibility and adoption equally."))
(~doc-subsection :title "Forth / stack-based"
(~docs/subsection :title "Forth / stack-based"
(p :class "text-stone-600"
"Forth has the most minimal syntax imaginable: words separated by spaces. No parentheses, no brackets, no delimiters. The program is a flat sequence of tokens. This is simpler than s-expressions.")
(p :class "text-stone-600"
@@ -91,7 +91,7 @@
(p :class "text-stone-600"
"For markup, this is fatal. " (code "3 1 + 4 * 2 /") " is arithmetic. Now imagine a page layout expressed as stack operations. The nesting that makes " (code "(div (h2 \"Title\") (p \"Body\"))") " self-evident becomes an exercise in mental bookkeeping. UI is trees. Stack languages are not.")))
(~doc-section :title "The convergence" :id "convergence"
(~docs/section :title "The convergence" :id "convergence"
(p :class "text-stone-600"
"Every alternative either fails to meet the requirements or reinvents s-expressions:")
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
@@ -163,7 +163,7 @@
(p :class "text-stone-600"
"No candidate achieves all five properties. The closest — Rebol — fails on AI trainability, which is not a theoretical concern but a practical one: a representation that AI cannot generate reliably is a representation that cannot participate in the coming decade of software development."))
(~doc-section :title "Why not invent something new?" :id "invent"
(~docs/section :title "Why not invent something new?" :id "invent"
(p :class "text-stone-600"
"The objection might be: fine, existing alternatives fall short, but why not design a new representation that has all these properties without the parentheses?")
(p :class "text-stone-600"
@@ -177,7 +177,7 @@
(p :class "text-stone-600"
"Every path through the design space either arrives at parenthesized prefix notation — s-expressions — or introduces complexity that violates one of the requirements. This is not a failure of imagination. It is a consequence of the requirements being simultaneously demanding and precise. The solution space has one optimum, and McCarthy found it in 1958."))
(~doc-section :title "The parentheses objection" :id "parentheses"
(~docs/section :title "The parentheses objection" :id "parentheses"
(p :class "text-stone-600"
"The real objection to s-expressions is not technical. It is aesthetic. People do not like parentheses. They look unfamiliar. They feel old. They trigger memories of computer science lectures about recursive descent parsers.")
(p :class "text-stone-600"
@@ -187,7 +187,7 @@
(p :class "text-stone-600"
"The author of SX has never opened the codebase in an editor. Every file was created through " (a :href "https://claude.ai/" :class "text-violet-600 hover:underline" "Claude") " in a terminal. The parentheses are between the human and the machine, and neither one minds them."))
(~doc-section :title "The conclusion" :id "conclusion"
(~docs/section :title "The conclusion" :id "conclusion"
(p :class "text-stone-600"
"S-expressions are the minimal tree representation. They are the only widely-known homoiconic notation. They have trivial structural validation, maximum token efficiency, and native composability. They are well-represented in AI training data. Every alternative either fails on one of these criteria or converges toward s-expressions under a different name.")
(p :class "text-stone-600"

View File

@@ -1,2 +1,2 @@
(defcomp ~essay-on-demand-css ()
(~doc-page :title "On-Demand CSS: Killing the Tailwind Bundle" (~doc-section :title "The problem" :id "problem" (p :class "text-stone-600" "Tailwind CSS generates a utility class for every possible combination. The full CSS file is ~4MB. The purged output for a typical site is 20-50KB. Purging requires a build step that scans your source files for class names. This means: a build tool, a config file, a CI step, and a prayer that the scanner finds all your dynamic classes.")) (~doc-section :title "The sx approach" :id "approach" (p :class "text-stone-600" "sx takes a different path. At server startup, the full Tailwind CSS file is parsed into a dictionary keyed by class name. When rendering a response, sx scans the s-expression source for :class attribute values and looks up only those classes. The result: exact CSS, zero build step.") (p :class "text-stone-600" "Component definitions are pre-scanned at registration time. Page-specific sx is scanned at request time. The union of classes is resolved to CSS rules.")) (~doc-section :title "Incremental delivery" :id "incremental" (p :class "text-stone-600" "After the first page load, the client tracks which CSS classes it already has. On subsequent navigations, it sends a hash of its known classes in the SX-Css header. The server computes the diff and sends only new rules. A typical navigation adds 0-10 new rules — a few hundred bytes at most.")) (~doc-section :title "The tradeoff" :id "tradeoff" (p :class "text-stone-600" "The server holds ~4MB of parsed CSS in memory. Regex scanning is not perfect — dynamically constructed class names will not be found. In practice this rarely matters because sx components use mostly static class strings."))))
(defcomp ~essays/on-demand-css/essay-on-demand-css ()
(~docs/page :title "On-Demand CSS: Killing the Tailwind Bundle" (~docs/section :title "The problem" :id "problem" (p :class "text-stone-600" "Tailwind CSS generates a utility class for every possible combination. The full CSS file is ~4MB. The purged output for a typical site is 20-50KB. Purging requires a build step that scans your source files for class names. This means: a build tool, a config file, a CI step, and a prayer that the scanner finds all your dynamic classes.")) (~docs/section :title "The sx approach" :id "approach" (p :class "text-stone-600" "sx takes a different path. At server startup, the full Tailwind CSS file is parsed into a dictionary keyed by class name. When rendering a response, sx scans the s-expression source for :class attribute values and looks up only those classes. The result: exact CSS, zero build step.") (p :class "text-stone-600" "Component definitions are pre-scanned at registration time. Page-specific sx is scanned at request time. The union of classes is resolved to CSS rules.")) (~docs/section :title "Incremental delivery" :id "incremental" (p :class "text-stone-600" "After the first page load, the client tracks which CSS classes it already has. On subsequent navigations, it sends a hash of its known classes in the SX-Css header. The server computes the diff and sends only new rules. A typical navigation adds 0-10 new rules — a few hundred bytes at most.")) (~docs/section :title "The tradeoff" :id "tradeoff" (p :class "text-stone-600" "The server holds ~4MB of parsed CSS in memory. Regex scanning is not perfect — dynamically constructed class names will not be found. In practice this rarely matters because sx components use mostly static class strings."))))

View File

@@ -4,8 +4,8 @@
;; Philosophy section content
;; ---------------------------------------------------------------------------
(defcomp ~philosophy-index-content ()
(~doc-page :title "Philosophy"
(defcomp ~essays/philosophy-index/content ()
(~docs/page :title "Philosophy"
(div :class "space-y-4"
(p :class "text-lg text-stone-600 mb-4"
"The deeper ideas behind SX — manifestos, self-reference, and the philosophical traditions that shaped the language.")

View File

@@ -2,11 +2,11 @@
;; React is Hypermedia
;; ---------------------------------------------------------------------------
(defcomp ~essay-react-is-hypermedia ()
(~doc-page :title "React is Hypermedia"
(defcomp ~essays/react-is-hypermedia/essay-react-is-hypermedia ()
(~docs/page :title "React is Hypermedia"
(p :class "text-stone-500 text-sm italic mb-8"
"A React Island is a hypermedia control. Its behavior is specified in SX.")
(~doc-section :title "I. The argument" :id "argument"
(~docs/section :title "I. The argument" :id "argument"
(p :class "text-stone-600"
"React is not hypermedia. Everyone knows this. React is a JavaScript UI library. It renders components to a virtual DOM. It diffs. It patches. It manages state. It does none of the things that define " (a :href "https://en.wikipedia.org/wiki/Hypermedia" :class "text-violet-600 hover:underline" "hypermedia") " — server-driven content, links as the primary interaction mechanism, representations that carry their own controls.")
(p :class "text-stone-600"
@@ -19,7 +19,7 @@
(li "It does not fetch data. It does not route. It does not manage application state outside its boundary."))
(p :class "text-stone-600"
"This is a " (a :href "https://en.wikipedia.org/wiki/Hypermedia#Controls" :class "text-violet-600 hover:underline" "hypermedia control") ". It is a region of a hypermedia document that responds to user input. Like a " (code "<form>") ". Like an " (code "<a>") ". Like an " (code "<input>") ". The difference is that a form's behavior is specified by the browser and the HTTP protocol. An island's behavior is specified in SX."))
(~doc-section :title "II. What makes something hypermedia" :id "hypermedia"
(~docs/section :title "II. What makes something hypermedia" :id "hypermedia"
(p :class "text-stone-600"
"Roy " (a :href "https://en.wikipedia.org/wiki/Roy_Fielding" :class "text-violet-600 hover:underline" "Fielding") "'s " (a :href "https://en.wikipedia.org/wiki/Representational_state_transfer" :class "text-violet-600 hover:underline" "REST") " thesis defines hypermedia by a constraint: " (em "hypermedia as the engine of application state") " (HATEOAS). The server sends representations that include controls — links, forms — and the client's state transitions are driven by those controls. The client does not need out-of-band knowledge of what actions are available. The representation " (em "is") " the interface.")
(p :class "text-stone-600"
@@ -28,12 +28,12 @@
"An SX page does not violate this. The server sends a complete representation — an s-expression tree — that includes all controls. Some controls are plain HTML: " (code "(a :href \"/about\" :sx-get \"/about\")") ". Some controls are reactive islands: " (code "(defisland counter (let ((count (signal 0))) ...))") ". Both are embedded in the representation. Both are delivered by the server. The client does not decide what controls exist — the server does, by including them in the document.")
(p :class "text-stone-600"
"The island is not separate from the hypermedia. The island " (em "is") " part of the hypermedia. It is a control that the server chose to include, whose behavior the server specified, in the same format as the rest of the page."))
(~doc-section :title "III. The SX specification layer" :id "spec-layer"
(~docs/section :title "III. The SX specification layer" :id "spec-layer"
(p :class "text-stone-600"
"A " (code "<form>") "'s behavior is specified in HTML + HTTP: " (code "method=\"POST\"") ", " (code "action=\"/submit\"") ". The browser reads the specification and executes it — serialise the inputs, make the request, handle the response. The form does not contain JavaScript. Its behavior is declared.")
(p :class "text-stone-600"
"An SX island's behavior is specified in SX:")
(~doc-code :lang "lisp" :code
(~docs/code :lang "lisp" :code
"(defisland todo-adder\n (let ((text (signal \"\")))\n (form :on-submit (fn (e)\n (prevent-default e)\n (emit-event \"todo:add\" (deref text))\n (reset! text \"\"))\n (input :type \"text\"\n :bind text\n :placeholder \"What needs doing?\")\n (button :type \"submit\" \"Add\"))))")
(p :class "text-stone-600"
"This is a " (em "declaration") ", not a program. It declares: there is a signal holding text. There is a form. When submitted, it emits an event and resets the signal. There is an input bound to the signal. There is a button.")
@@ -41,7 +41,7 @@
"The s-expression " (em "is") " the specification. It is not compiled to JavaScript and then executed as an opaque blob. It is parsed, evaluated, and rendered by a transparent evaluator whose own semantics are specified in the same format (" (code "eval.sx") "). The island's behavior is as inspectable as a form's " (code "action") " attribute — you can read it, quote it, transform it, analyse it. You can even send it over the wire and have a different client render it.")
(p :class "text-stone-600"
"A form says " (em "what to do") " in HTML attributes. An island says " (em "what to do") " in s-expressions. Both are declarative. Both are part of the hypermedia document. The difference is expressiveness: forms can collect inputs and POST them. Islands can maintain local state, compute derived values, animate transitions, handle errors, and render dynamic lists — all declared in the same markup language as the page that contains them."))
(~doc-section :title "IV. The four levels" :id "four-levels"
(~docs/section :title "IV. The four levels" :id "four-levels"
(p :class "text-stone-600"
"SX reactive islands exist at four levels of complexity, from pure hypermedia to full client reactivity. Each level is a superset of the one before:")
(ul :class "list-disc pl-6 space-y-2 text-stone-600"
@@ -51,7 +51,7 @@
(li (span :class "font-semibold" "L3 — Island communication.") " Islands talk to each other and to the htmx-like \"lake\" via DOM events. " (code "(emit-event \"cart:updated\" count)") " and " (code "(on-event \"cart:updated\" handler)") ". Still no global state. Still no client-side routing. The page is still a server document with embedded controls."))
(p :class "text-stone-600"
"At every level, the architecture is hypermedia. The server produces the document. The document contains controls. The controls are specified in SX. The jump from L1 to L2 is not a jump from hypermedia to SPA — it is a jump from " (em "simple controls") " (links and forms) to " (em "richer controls") " (reactive islands). The paradigm does not change. The expressiveness does."))
(~doc-section :title "V. Why not just React?" :id "why-not-react"
(~docs/section :title "V. Why not just React?" :id "why-not-react"
(p :class "text-stone-600"
"If an island behaves like a React component — local state, event handlers, conditional rendering — why not use React?")
(p :class "text-stone-600"
@@ -62,18 +62,18 @@
"This matters because hypermedia's core property is " (em "self-description") ". A hypermedia representation carries its own controls and its own semantics. An HTML form is self-describing: the browser reads the " (code "action") " and " (code "method") " and knows what to do. A compiled React component is not self-describing: it is a function that was once source code, compiled away into instructions that only the React runtime can interpret.")
(p :class "text-stone-600"
"SX islands are self-describing. The source is the artifact. The representation carries its own semantics. This is what makes them hypermedia controls — not because they avoid JavaScript (they don't), but because the behavior specification travels with the document, in the same format as the document."))
(~doc-section :title "VI. The bridge pattern" :id "bridge"
(~docs/section :title "VI. The bridge pattern" :id "bridge"
(p :class "text-stone-600"
"In practice, the hypermedia and the islands coexist through a pattern: the htmx \"lake\" surrounds the reactive \"islands.\" The lake handles navigation, form submission, content loading — classic hypermedia. The islands handle local interaction — counters, toggles, filters, input validation, animations.")
(p :class "text-stone-600"
"Communication between lake and islands uses DOM events. An island can " (code "emit-event") " to tell the page something happened. A server-rendered button can " (code "bridge-event") " to poke an island when clicked. The DOM — the shared medium — is the only coupling.")
(~doc-code :lang "lisp" :code
(~docs/code :lang "lisp" :code
";; Server-rendered lake button dispatches to island\n(button :sx-get \"/api/refresh\"\n :sx-target \"#results\"\n :on-click (bridge-event \"search:clear\")\n \"Reset\")\n\n;; Island listens for the event\n(defisland search-filter\n (let ((query (signal \"\")))\n (on-event \"search:clear\" (fn () (reset! query \"\")))\n (input :bind query :placeholder \"Filter...\")))")
(p :class "text-stone-600"
"The lake button does its hypermedia thing — fetches HTML, swaps it in. Simultaneously, it dispatches a DOM event. The island hears the event and clears its state. Neither knows about the other's implementation. They communicate through the hypermedia document's event system — the DOM.")
(p :class "text-stone-600"
"This is not a hybrid architecture bolting two incompatible models together. It is a single model — hypermedia — with controls of varying complexity. Some controls are links. Some are forms. Some are reactive islands. All are specified in the document. All are delivered by the server."))
(~doc-section :title "VII. The specification is the specification" :id "specification"
(~docs/section :title "VII. The specification is the specification" :id "specification"
(p :class "text-stone-600"
"The deepest claim is not architectural but philosophical. A React Island — the kind with signals and effects and computed values — is a " (em "behavior specification") ". It specifies: when this signal changes, recompute this derived value, re-render this DOM subtree. When this event fires, update this state. When this input changes, validate against this pattern.")
(p :class "text-stone-600"

View File

@@ -1,9 +1,9 @@
(defcomp ~essay-reflexive-web ()
(~doc-page :title "The Reflexive Web"
(defcomp ~essays/reflexive-web/essay-reflexive-web ()
(~docs/page :title "The Reflexive Web"
(p :class "text-stone-500 text-sm italic mb-8"
"What happens when the web can read, modify, and reason about itself — and AI is a native participant.")
(~doc-section :title "The missing property" :id "missing-property"
(~docs/section :title "The missing property" :id "missing-property"
(p :class "text-stone-600"
"Every web technology stack shares one structural limitation: the system cannot inspect itself. A " (a :href "https://en.wikipedia.org/wiki/React_(software)" :class "text-violet-600 hover:underline" "React") " component tree is opaque at runtime. An " (a :href "https://en.wikipedia.org/wiki/HTML" :class "text-violet-600 hover:underline" "HTML") " page cannot read its own structure and generate a new page from it. A " (a :href "https://en.wikipedia.org/wiki/JavaScript" :class "text-violet-600 hover:underline" "JavaScript") " bundle is compiled, minified, and sealed — the running code bears no resemblance to the source that produced it.")
(p :class "text-stone-600"
@@ -11,9 +11,9 @@
(p :class "text-stone-600"
"SX is a complete Lisp. It has " (a :href "https://en.wikipedia.org/wiki/Homoiconicity" :class "text-violet-600 hover:underline" "homoiconicity") " — code is data, data is code. It has a " (a :href "/sx/(language.(spec.core))" :class "text-violet-600 hover:underline" "self-hosting specification") " — SX defined in SX. It has " (code "eval") " and " (code "quote") " and " (a :href "https://en.wikipedia.org/wiki/Macro_(computer_science)#Syntactic_macros" :class "text-violet-600 hover:underline" "macros") ". And it runs on the wire — the format that travels between server and client IS the language. This combination has consequences."))
(~doc-section :title "What homoiconicity changes" :id "homoiconicity"
(~docs/section :title "What homoiconicity changes" :id "homoiconicity"
(p :class "text-stone-600"
(code "(defcomp ~card (&key title body) (div :class \"p-4\" (h2 title) (p body)))") " — this is simultaneously a program that renders a card AND a list that can be inspected, transformed, and composed by other programs. The " (code "defcomp") " is not compiled away. It is not transpiled into something else. It persists as data at every stage: definition, transmission, evaluation, and rendering.")
(code "(defcomp ~essays/reflexive-web/card (&key title body) (div :class \"p-4\" (h2 title) (p body)))") " — this is simultaneously a program that renders a card AND a list that can be inspected, transformed, and composed by other programs. The " (code "defcomp") " is not compiled away. It is not transpiled into something else. It persists as data at every stage: definition, transmission, evaluation, and rendering.")
(p :class "text-stone-600"
"This means:")
(ul :class "space-y-2 text-stone-600 mt-2"
@@ -21,7 +21,7 @@
(li (strong "Programs can write programs.") " A " (a :href "https://en.wikipedia.org/wiki/Macro_(computer_science)#Syntactic_macros" :class "text-violet-600 hover:underline" "macro") " takes a list and returns a list. The returned list is code. The macro runs at expansion time and produces new components, new page definitions, new routing rules — indistinguishable from hand-written ones.")
(li (strong "The wire format is inspectable.") " What the server sends to the client is not a blob of serialized state. It is s-expressions that any system — browser, AI, another server — can parse, reason about, and act on.")))
(~doc-section :title "AI as a native speaker" :id "ai-native"
(~docs/section :title "AI as a native speaker" :id "ai-native"
(p :class "text-stone-600"
"Current AI integration with the web is mediated through layers of indirection. An " (a :href "https://en.wikipedia.org/wiki/Large_language_model" :class "text-violet-600 hover:underline" "LLM") " generates " (a :href "https://en.wikipedia.org/wiki/React_(software)" :class "text-violet-600 hover:underline" "React") " components as strings that must be compiled, bundled, and deployed. It interacts with APIs through " (a :href "https://en.wikipedia.org/wiki/JSON" :class "text-violet-600 hover:underline" "JSON") " endpoints that require separate documentation. It reads HTML by scraping, because the markup was never meant to be machine-readable in a computational sense.")
(p :class "text-stone-600"
@@ -29,9 +29,9 @@
(p :class "text-stone-600"
"An AI that understands SX understands the " (a :href "/sx/(language.(spec.core))" :class "text-violet-600 hover:underline" "spec") ". And the spec is written in SX. So the AI understands the definition of the language it is using, in the language it is using. This " (a :href "https://en.wikipedia.org/wiki/Reflexivity_(social_theory)" :class "text-violet-600 hover:underline" "reflexive") " property means the AI does not need a separate mental model for \"the web\" and \"the language\" — they are the same thing."))
(~doc-section :title "Live system modification" :id "live-modification"
(~docs/section :title "Live system modification" :id "live-modification"
(p :class "text-stone-600"
"Because code is data and the wire format is the language, modifying a running system is not deployment — it is evaluation. An AI reads " (code "(defcomp ~checkout-form ...)") ", understands what it does (because the semantics are specified in SX), modifies the expression, and sends it back. The system evaluates the new definition. No build step. No deploy pipeline. No container restart.")
"Because code is data and the wire format is the language, modifying a running system is not deployment — it is evaluation. An AI reads " (code "(defcomp ~essays/reflexive-web/checkout-form ...)") ", understands what it does (because the semantics are specified in SX), modifies the expression, and sends it back. The system evaluates the new definition. No build step. No deploy pipeline. No container restart.")
(p :class "text-stone-600"
"This is not theoretical — it is how " (a :href "https://en.wikipedia.org/wiki/Lisp_(programming_language)" :class "text-violet-600 hover:underline" "Lisp") " development has always worked. You modify a function in the running image. The change takes effect immediately. What is new is putting this on the wire, across a network, with the AI as a participant rather than a tool.")
(p :class "text-stone-600"
@@ -39,7 +39,7 @@
(p :class "text-stone-600"
"More radically: the distinction between \"development\" and \"operation\" dissolves. If the running system is a set of s-expressions, and those expressions can be inspected and modified at runtime, then there is no separate development environment. There is just the system, and agents — human or artificial — that interact with it."))
(~doc-section :title "Federated intelligence" :id "federated-intelligence"
(~docs/section :title "Federated intelligence" :id "federated-intelligence"
(p :class "text-stone-600"
(a :href "https://en.wikipedia.org/wiki/ActivityPub" :class "text-violet-600 hover:underline" "ActivityPub") " carries activities between nodes. If those activities contain s-expressions, then what travels between servers is not just data — it is " (em "behaviour") ". Node A sends a component definition to Node B. Node B evaluates it. The result is rendered. The sender's intent is executable on the receiver's hardware.")
(p :class "text-stone-600"
@@ -47,7 +47,7 @@
(p :class "text-stone-600"
"For AI agents in a federated network, this means an agent on one node can send " (em "capabilities") " to another node, not just requests. A component that renders a specific visualization. A macro that transforms data into a particular format. A function that implements a protocol. The network becomes a shared computational substrate where intelligence is distributed as executable expressions."))
(~doc-section :title "Programs writing programs writing programs" :id "meta-programs"
(~docs/section :title "Programs writing programs writing programs" :id "meta-programs"
(p :class "text-stone-600"
"A macro is a function that takes code and returns code. An AI generating macros is writing programs that write programs. With " (code "eval") ", those generated programs can generate more programs at runtime. This is not a metaphor — it is the literal mechanism.")
(p :class "text-stone-600"
@@ -59,7 +59,7 @@
(li (strong "Generative composition.") " Given a data schema and a design intent, an AI generates not just a component but the " (em "macros") " that generate families of components. The macro is a template for templates. The output scales combinatorially.")
(li (strong "Cross-system reasoning.") " An AI reads component definitions from multiple federated nodes, identifies common patterns, and synthesizes abstractions that work across all of them. The shared language makes cross-system analysis trivial — it is all s-expressions.")))
(~doc-section :title "The sandbox is everything" :id "sandbox"
(~docs/section :title "The sandbox is everything" :id "sandbox"
(p :class "text-stone-600"
"The same " (a :href "https://en.wikipedia.org/wiki/Homoiconicity" :class "text-violet-600 hover:underline" "homoiconicity") " that makes this powerful makes it dangerous. Code-as-data means an AI can inject " (em "behaviour") ", not just content. A malicious expression evaluated in the wrong context could exfiltrate data, modify other components, or disrupt the system.")
(p :class "text-stone-600"
@@ -69,7 +69,7 @@
(p :class "text-stone-600"
"This matters enormously for AI. An AI agent that can modify the running system must be constrained by the same sandbox mechanism that constrains any other expression. The security model does not distinguish between human-authored code and AI-generated code — both are s-expressions, both are evaluated by the same evaluator, both are subject to the same primitive restrictions."))
(~doc-section :title "Not self-aware — reflexive" :id "reflexive"
(~docs/section :title "Not self-aware — reflexive" :id "reflexive"
(p :class "text-stone-600"
"Is this a \"self-aware web\"? Probably not in the " (a :href "https://en.wikipedia.org/wiki/Consciousness" :class "text-violet-600 hover:underline" "consciousness") " sense. But the word we keep reaching for has a precise meaning: " (a :href "https://en.wikipedia.org/wiki/Reflexivity_(social_theory)" :class "text-violet-600 hover:underline" "reflexivity") ". A reflexive system can represent itself, reason about its own structure, and modify itself based on that reasoning.")
(p :class "text-stone-600"
@@ -79,7 +79,7 @@
(p :class "text-stone-600"
"What AI adds to this is not awareness but " (em "agency") ". The system has always been reflexive — Lisp has been reflexive for seven decades. What is new is having an agent that can exploit that reflexivity at scale: reading the entire system state as data, reasoning about it, generating modifications, and evaluating the results — all in the native language of the system itself."))
(~doc-section :title "The Lisp that escaped the REPL" :id "escaped-repl"
(~docs/section :title "The Lisp that escaped the REPL" :id "escaped-repl"
(p :class "text-stone-600"
(a :href "https://en.wikipedia.org/wiki/Lisp_(programming_language)" :class "text-violet-600 hover:underline" "Lisp") " has been reflexive since " (a :href "https://en.wikipedia.org/wiki/John_McCarthy_(computer_scientist)" :class "text-violet-600 hover:underline" "McCarthy") ". What kept it contained was the boundary of the " (a :href "https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop" :class "text-violet-600 hover:underline" "REPL") " — a single process, a single machine, a single user. The s-expressions lived inside Emacs, inside a Clojure JVM, inside a Scheme interpreter. They did not travel.")
(p :class "text-stone-600"
@@ -89,7 +89,7 @@
(p :class "text-stone-600"
"Whether this constitutes anything approaching awareness is a philosophical question. What is not philosophical is the engineering consequence: a web built on s-expressions is a web that AI can participate in as a " (em "native citizen") ", not as a tool bolted onto the side. The language is the interface. The interface is the language. And the language can describe itself."))
(~doc-section :title "What this opens up" :id "possibilities"
(~docs/section :title "What this opens up" :id "possibilities"
(p :class "text-stone-600"
"Concretely:")
(ul :class "space-y-3 text-stone-600 mt-2"

View File

@@ -1,8 +1,8 @@
(defcomp ~essay-s-existentialism ()
(~doc-page :title "S-Existentialism"
(defcomp ~essays/s-existentialism/essay-s-existentialism ()
(~docs/page :title "S-Existentialism"
(p :class "text-stone-500 text-sm italic mb-8"
"Existence precedes essence — and s-expressions exist before anything gives them meaning.")
(~doc-section :title "I. Existence precedes essence" :id "existence-precedes-essence"
(~docs/section :title "I. Existence precedes essence" :id "existence-precedes-essence"
(p :class "text-stone-600"
"In 1946, Jean-Paul " (a :href "https://en.wikipedia.org/wiki/Jean-Paul_Sartre" :class "text-violet-600 hover:underline" "Sartre") " gave a lecture called \"" (a :href "https://en.wikipedia.org/wiki/Existentialism_Is_a_Humanism" :class "text-violet-600 hover:underline" "Existentialism Is a Humanism") ".\" Its central claim: " (em "existence precedes essence") ". A paper knife is designed before it exists — someone conceives its purpose, then builds it. A human being is the opposite — we exist first, then define ourselves through our choices. There is no blueprint. There is no human nature that precedes the individual human.")
(p :class "text-stone-600"
@@ -11,9 +11,9 @@
"An s-expression exists before any essence is assigned to it. " (code "(div :class \"card\" (h2 title))") " is a list. That is all it is. It has no inherent meaning. It is not a component, not a template, not a function call — not yet. It is raw existence: a nested structure of symbols, keywords, and other lists, waiting.")
(p :class "text-stone-600"
"The evaluator gives it essence. " (code "render-to-html") " makes it HTML. " (code "render-to-dom") " makes it DOM nodes. " (code "aser") " makes it wire format. " (code "quote") " keeps it as data. The same expression, the same existence, can receive different essences depending on what acts on it. The expression does not know what it is. It becomes what it is used for.")
(~doc-code :lang "lisp" :code
(~docs/code :lang "lisp" :code
";; The same existence, different essences:\n(define expr '(div :class \"card\" (h2 \"Hello\")))\n\n(render-to-html expr) ;; → <div class=\"card\"><h2>Hello</h2></div>\n(render-to-dom expr) ;; → [DOM Element]\n(aser expr) ;; → (div :class \"card\" (h2 \"Hello\"))\n(length expr) ;; → 4 (it's just a list)\n\n;; The expression existed before any of these.\n;; It has no essence until you give it one."))
(~doc-section :title "II. Condemned to be free" :id "condemned"
(~docs/section :title "II. Condemned to be free" :id "condemned"
(p :class "text-stone-600"
"\"Man is condemned to be free,\" Sartre wrote in " (a :href "https://en.wikipedia.org/wiki/Being_and_Nothingness" :class "text-violet-600 hover:underline" "Being and Nothingness") ". Not free as a gift. Free as a sentence. You did not choose to be free. You cannot escape it. Every attempt to deny your freedom — by deferring to authority, convention, or nature — is " (a :href "https://en.wikipedia.org/wiki/Bad_faith_(existentialism)" :class "text-violet-600 hover:underline" "bad faith") ". You are responsible for everything you make of yourself, and the weight of that responsibility is the human condition.")
(p :class "text-stone-600"
@@ -24,7 +24,7 @@
"The SX developer has no commandments. " (code "defcomp") " is a suggestion, not a requirement — you can build components with raw lambdas if you prefer. " (code "defmacro") " gives you the power to reshape the language itself. There are no rules of hooks because there are no hooks. There are no lifecycle methods because there is no lifecycle. There is only evaluation: an expression goes in, a value comes out. What the expression contains, how the value is used — that is up to you.")
(p :class "text-stone-600"
"This is not comfortable. Freedom never is. Sartre did not say freedom was pleasant. He said it was inescapable."))
(~doc-section :title "III. Bad faith" :id "bad-faith"
(~docs/section :title "III. Bad faith" :id "bad-faith"
(p :class "text-stone-600"
(a :href "https://en.wikipedia.org/wiki/Bad_faith_(existentialism)" :class "text-violet-600 hover:underline" "Bad faith") " is Sartre's term for the lie you tell yourself to escape freedom. The waiter who plays at being a waiter — performing the role so thoroughly that he forgets he chose it. The woman who pretends not to notice a man's intentions — denying her own awareness to avoid making a decision. Bad faith is not deception of others. It is self-deception about one's own freedom.")
(p :class "text-stone-600"
@@ -37,7 +37,7 @@
"\"Nobody uses s-expressions for web development.\" Bad faith. " (em "You") " do not use s-expressions for web development. That is a fact about you, not a fact about web development. Transforming your personal preference into a universal law is the quintessential act of bad faith.")
(p :class "text-stone-600"
"SX does not prevent bad faith — nothing can. But it makes bad faith harder. When the entire language is fifty primitives and a page of special forms, you cannot pretend that the complexity is necessary. When there is no build step, you cannot pretend that the build step is inevitable. When the same source runs on server and client, you cannot pretend that the server-client divide is ontological. SX strips away the excuses. What remains is your choices."))
(~doc-section :title "IV. Nausea" :id "nausea"
(~docs/section :title "IV. Nausea" :id "nausea"
(p :class "text-stone-600"
"In " (a :href "https://en.wikipedia.org/wiki/Nausea_(novel)" :class "text-violet-600 hover:underline" "Nausea") " (1938), Sartre's Roquentin sits in a park and stares at the root of a chestnut tree. He sees it — really sees it — stripped of all the concepts and categories that normally make it comprehensible. It is not a \"root.\" It is not \"brown.\" It is not \"gnarled.\" It simply " (em "is") " — a brute, opaque, superfluous existence. The nausea is the vertigo of confronting existence without essence.")
(p :class "text-stone-600"
@@ -46,7 +46,7 @@
"SX has its own nausea. Stare at a page of s-expressions long enough and the same vertigo hits. Parentheses. Symbols. Lists inside lists inside lists. There is nothing behind them — no hidden runtime, no compiled intermediate form, no framework magic. Just parentheses. The s-expression is Roquentin's chestnut root: it simply " (em "is") ". You cannot unsee it.")
(p :class "text-stone-600"
"But SX's nausea is honest. The chestnut root is really there — it exists, bare and exposed. The " (code "node_modules") " nausea is different: it is nausea at something that should not exist, that has no reason to exist, that exists only because of accumulated accidents of dependency resolution. SX's nausea is existential — the dizziness of confronting raw structure. The JavaScript ecosystem's nausea is absurd — the dizziness of confronting unnecessary complexity that no one chose but everyone maintains."))
(~doc-section :title "V. The absurd" :id "absurd"
(~docs/section :title "V. The absurd" :id "absurd"
(p :class "text-stone-600"
(a :href "https://en.wikipedia.org/wiki/Albert_Camus" :class "text-violet-600 hover:underline" "Camus") " defined the " (a :href "https://en.wikipedia.org/wiki/Absurdism" :class "text-violet-600 hover:underline" "absurd") " as the gap between human longing for meaning and the universe's silence. We want the world to make sense. It does not. The absurd is not in us or in the world — it is in the confrontation between the two.")
(p :class "text-stone-600"
@@ -59,7 +59,7 @@
"Most developers commit philosophical suicide. They adopt a framework, declare it The Way, and stop questioning. React is the truth. TypeScript is salvation. The build step is destiny. The absurd disappears — not because it has been resolved, but because they have stopped looking at it.")
(p :class "text-stone-600"
"SX is revolt. It does not resolve the absurd. It does not pretend that s-expressions are the answer, that parentheses will save the web, that the industry will come around. It simply continues — writing components, specifying evaluators, bootstrapping to new targets — with full awareness that the project may never matter to anyone. This is the only honest response to the absurd."))
(~doc-section :title "VI. Sisyphus" :id "sisyphus"
(~docs/section :title "VI. Sisyphus" :id "sisyphus"
(p :class "text-stone-600"
"\"" (a :href "https://en.wikipedia.org/wiki/The_Myth_of_Sisyphus" :class "text-violet-600 hover:underline" "One must imagine Sisyphus happy") ".\"")
(p :class "text-stone-600"
@@ -70,7 +70,7 @@
"The SX developer is conscious Sisyphus. The boulder is obvious: writing a Lisp for the web is absurd. The hill is obvious: nobody will use it. But consciousness changes everything. Camus's Sisyphus is happy not because the task has meaning but because " (em "he") " has chosen it. The choice — the revolt — is the meaning. Not the outcome.")
(p :class "text-stone-600"
"One must imagine the s-expressionist happy."))
(~doc-section :title "VII. Thrownness" :id "thrownness"
(~docs/section :title "VII. Thrownness" :id "thrownness"
(p :class "text-stone-600"
(a :href "https://en.wikipedia.org/wiki/Martin_Heidegger" :class "text-violet-600 hover:underline" "Heidegger's") " " (a :href "https://en.wikipedia.org/wiki/Thrownness" :class "text-violet-600 hover:underline" "Geworfenheit") " — thrownness — describes the condition of finding yourself already in a world you did not choose. You did not pick your language, your culture, your body, your historical moment. You were " (em "thrown") " into them. Authenticity is not escaping thrownness but owning it — relating to your situation as yours, rather than pretending it was inevitable or that you could have been elsewhere.")
(p :class "text-stone-600"
@@ -81,7 +81,7 @@
"SX owns its thrownness. It runs in the browser's JavaScript engine — not because JavaScript is good, but because the browser is the world it was thrown into. It produces DOM nodes — not because the DOM is elegant, but because the DOM is what exists. It sends HTTP responses — not because HTTP is ideal, but because HTTP is the wire. SX does not build a virtual DOM to escape the real DOM. It does not invent a type system to escape JavaScript's types. It evaluates s-expressions in the given environment and produces what the environment requires.")
(p :class "text-stone-600"
"The s-expression is itself a kind of primordial thrownness. It did not choose to be the minimal recursive data structure. It simply is. Open paren, atoms, close paren. It was not designed by committee, not optimised by industry, not evolved through market pressure. It was " (a :href "https://en.wikipedia.org/wiki/Lisp_(programming_language)#History" :class "text-violet-600 hover:underline" "discovered in 1958") " as a notational convenience and turned out to be the bedrock. SX was thrown into s-expressions the way humans are thrown into bodies — not by choice, but by the nature of what it is."))
(~doc-section :title "VIII. The Other" :id "the-other"
(~docs/section :title "VIII. The Other" :id "the-other"
(p :class "text-stone-600"
"Sartre's account of " (a :href "https://en.wikipedia.org/wiki/Being_and_Nothingness#The_Other_and_the_Look" :class "text-violet-600 hover:underline" "the Other") " in Being and Nothingness: I am alone in a park. I am the centre of my world. Then I see another person. Suddenly I am seen. I am no longer just a subject — I am an object in someone else's world. The Other's gaze transforms me. \"Hell is other people,\" Sartre wrote in " (a :href "https://en.wikipedia.org/wiki/No_Exit" :class "text-violet-600 hover:underline" "No Exit") " — not because others are cruel, but because they see you, and their seeing limits your freedom to define yourself.")
(p :class "text-stone-600"
@@ -90,7 +90,7 @@
"SX has no Other. There is no competing s-expression web framework to define itself against. There is no benchmark to win, no market to capture, no conference talk to rebut. This is either pathetic (no ecosystem, no community, no relevance) or liberating (no gaze, no comparison, no borrowed identity). Sartre would say it is both.")
(p :class "text-stone-600"
"But there is another sense of the Other that matters more. The Other " (em "evaluator") ". SX's self-hosting spec means that the language encounters itself as Other. " (code "eval.sx") " is written in SX — the language looking at itself, seeing itself from outside. The bootstrap compiler reads this self-description and produces a working evaluator. The language has been seen by its own gaze, and the seeing has made it real. This is Sartre's intersubjectivity turned reflexive: the subject and the Other are the same entity."))
(~doc-section :title "IX. Authenticity" :id "authenticity"
(~docs/section :title "IX. Authenticity" :id "authenticity"
(p :class "text-stone-600"
"For both Heidegger and Sartre, " (a :href "https://en.wikipedia.org/wiki/Authenticity_(philosophy)" :class "text-violet-600 hover:underline" "authenticity") " means facing your situation — your freedom, your thrownness, your mortality — without evasion. The inauthentic person hides in the crowd, adopts the crowd's values, speaks the crowd's language. Heidegger called this " (a :href "https://en.wikipedia.org/wiki/Heideggerian_terminology#Das_Man" :class "text-violet-600 hover:underline" "das Man") " — the \"They.\" \"They say React is best.\" \"They use TypeScript.\" \"They have build steps.\" The They is not a conspiracy. It is the comfortable anonymity of doing what everyone does.")
(p :class "text-stone-600"

View File

@@ -1,9 +1,9 @@
(defcomp ~essay-self-defining-medium ()
(~doc-page :title "The True Hypermedium Must Define Itself With Itself"
(defcomp ~essays/self-defining-medium/essay-self-defining-medium ()
(~docs/page :title "The True Hypermedium Must Define Itself With Itself"
(p :class "text-stone-500 text-sm italic mb-8"
"On ontological uniformity, the metacircular web, and why the address of a thing and the thing itself should be made of the same stuff.")
(~doc-section :title "The test" :id "the-test"
(~docs/section :title "The test" :id "the-test"
(p :class "text-stone-600"
"There is a simple test for whether a medium is truly a medium or merely a carrier. Can it define itself? Can it describe its own semantics, in its own language, and have that description be executable?")
(p :class "text-stone-600"
@@ -11,7 +11,7 @@
(p :class "text-stone-600"
"This is not a mere inconvenience. It is a structural fact that determines what is and is not possible. A medium that cannot describe itself cannot reason about itself, cannot modify itself, cannot generate itself from itself. It is " (em "inert") " — a carrying wave, not a thinking substance."))
(~doc-section :title "What Lisp solved" :id "what-lisp-solved"
(~docs/section :title "What Lisp solved" :id "what-lisp-solved"
(p :class "text-stone-600"
(a :href "https://en.wikipedia.org/wiki/John_McCarthy_(computer_scientist)" :class "text-violet-600 hover:underline" "McCarthy") " solved this for computation in 1960. The " (a :href "https://en.wikipedia.org/wiki/Lisp_(programming_language)#Connection_to_artificial_intelligence" :class "text-violet-600 hover:underline" "metacircular evaluator") " is an evaluator written in the language it evaluates. " (code "eval") " takes a list and returns a value. The definition of " (code "eval") " is itself a list. So " (code "eval") " can evaluate its own definition. The serpent eats its tail.")
(p :class "text-stone-600"
@@ -19,7 +19,7 @@
(p :class "text-stone-600"
"The consequence was immediate and permanent: Lisp became the only language family where the specification, the implementation, the IDE, and the program are all the same kind of thing. Every other language has a boundary between the language and its meta-description. Lisp has none."))
(~doc-section :title "The web failed the test" :id "web-failed"
(~docs/section :title "The web failed the test" :id "web-failed"
(p :class "text-stone-600"
"The web has never had this property. Consider:")
(ul :class "space-y-2 text-stone-600 mt-2"
@@ -31,7 +31,7 @@
(p :class "text-stone-600"
"Every layer of the web stack requires stepping outside the medium to define the medium. This is ontological heterogeneity: the thing and the description of the thing are made of different stuff. The map is not the territory, and the map cannot even be " (em "drawn") " in the territory."))
(~doc-section :title "Ontological uniformity" :id "ontological-uniformity"
(~docs/section :title "Ontological uniformity" :id "ontological-uniformity"
(p :class "text-stone-600"
"The property we need has a name: " (strong "ontological uniformity") ". Address, verb, query, response, rendering instruction, and specification are all the same kind of thing.")
(p :class "text-stone-600"
@@ -39,23 +39,23 @@
(ul :class "space-y-2 text-stone-600 mt-2"
(li (strong "A URL is an s-expression.") " " (code "/sx/(language.(doc.introduction))") " is not an opaque string — it is a parseable, composable expression. It can be decomposed into parts, transformed by functions, and reasoned about by the same evaluator that renders pages.")
(li (strong "A response is an s-expression.") " What the server sends is " (code "(div :class \"p-4\" (h2 \"Hello\"))") " — the same notation as the component that produced it. The wire format is the language.")
(li (strong "A component is an s-expression.") " " (code "(defcomp ~card (&key title) (div (h2 title)))") " is simultaneously a definition, a value, and data that can be inspected and transformed.")
(li (strong "A component is an s-expression.") " " (code "(defcomp ~essays/self-defining-medium/card (&key title) (div (h2 title)))") " is simultaneously a definition, a value, and data that can be inspected and transformed.")
(li (strong "A query is an s-expression.") " The URL " (code "/sx/(language.(spec.core))") " is a function call. The response is the return value. Routing is evaluation.")
(li (strong "The specification is s-expressions.") " The " (a :href "/sx/(language.(spec.core))" :class "text-violet-600 hover:underline" "SX spec") " is written in SX. The evaluator is defined in the language it evaluates. The parser is defined in the language it parses."))
(p :class "text-stone-600"
"There is " (em "one") " kind of stuff. Everything is made of it. The address of a thing and the thing itself are the same kind of thing."))
(~doc-section :title "What this makes possible" :id "what-this-enables"
(~docs/section :title "What this makes possible" :id "what-this-enables"
(p :class "text-stone-600"
"When the medium is uniform, operations that were impossible become trivial:")
(ul :class "space-y-3 text-stone-600 mt-2"
(li (strong "URLs compose.") " If " (code "(language (doc introduction))") " and " (code "(language (spec core))") " are both expressions, then " (code "(diff (language (spec signals)) (language (spec eval)))") " is a natural composition. Two queries, side by side. The URL algebra falls out of the expression algebra. You do not need to design it separately — it was always there.")
(li (strong "The site can show its own source.") " " (code "/sx/(source.(~essay-self-defining-medium))") " returns the component definition of this essay. Not a screenshot. Not a prettified view. The actual s-expression that, when evaluated, produces what you are reading now. The page and its source code are the same kind of thing, so displaying one as the other is just evaluation.")
(li (strong "The site can show its own source.") " " (code "/sx/(source.(~essays/self-defining-medium/essay-self-defining-medium))") " returns the component definition of this essay. Not a screenshot. Not a prettified view. The actual s-expression that, when evaluated, produces what you are reading now. The page and its source code are the same kind of thing, so displaying one as the other is just evaluation.")
(li (strong "The spec is executable documentation.") " The " (a :href "/sx/(language.(bootstrapper.self-hosting))" :class "text-violet-600 hover:underline" "self-hosting bootstrapper") " reads the SX spec (written in SX) and produces a working evaluator. The documentation is the implementation. The implementation is the documentation. There is no drift because there is no gap.")
(li (strong "Inspection is free.") " " (code "/sx/(inspect.(language.(doc.primitives)))") " can show the dependency graph, CSS footprint, and render plan of any page — because the page is data, and data can be walked, analysed, and reported on by the same system that renders it.")
(li (strong "AI is a native speaker.") " An AI reading SX reads the same notation as the server, the client, the wire, and the spec. There is no translation layer. The AI does not generate code that must be compiled and deployed — it generates expressions that are evaluated. The medium is shared between human, machine, and network.")))
(~doc-section :title "The metacircular web" :id "metacircular-web"
(~docs/section :title "The metacircular web" :id "metacircular-web"
(p :class "text-stone-600"
"McCarthy's " (a :href "https://en.wikipedia.org/wiki/Lisp_(programming_language)#Connection_to_artificial_intelligence" :class "text-violet-600 hover:underline" "metacircular evaluator") " proved that a computing language can define itself. SX extends this proof to a networked hypermedium:")
(ul :class "space-y-2 text-stone-600 mt-2"
@@ -67,17 +67,17 @@
(p :class "text-stone-600"
"At every level, the description and the described are the same kind of thing. The specification is not " (em "about") " the system — it " (em "is") " the system. This is not metaphor. It is the literal architecture."))
(~doc-section :title "Why it matters" :id "why-it-matters"
(~docs/section :title "Why it matters" :id "why-it-matters"
(p :class "text-stone-600"
"A hypermedium that cannot define itself with itself is a hypermedium that depends on something else for its definition. It is parasitic on external authority — standards bodies, specification documents, reference implementations in foreign languages. Every layer of indirection is a layer where the medium's identity is borrowed rather than intrinsic.")
(p :class "text-stone-600"
"This dependency has practical consequences. When HTML needs a new element, a committee must convene, a specification must be written (in English, in a PDF), browser vendors must implement it (in C++), and the ecosystem must wait. The medium cannot extend itself. It is extended " (em "by others") ", in " (em "other languages") ", on " (em "other timescales") ".")
(p :class "text-stone-600"
"A self-defining medium extends itself by evaluating new definitions. " (code "(defcomp ~new-element (&key attrs children) ...)") " — this is not a proposal to a standards body. It is an expression that, when evaluated, adds a new element to the medium. The medium grows by the same mechanism it operates: evaluation of expressions.")
"A self-defining medium extends itself by evaluating new definitions. " (code "(defcomp ~essays/self-defining-medium/new-element (&key attrs children) ...)") " — this is not a proposal to a standards body. It is an expression that, when evaluated, adds a new element to the medium. The medium grows by the same mechanism it operates: evaluation of expressions.")
(p :class "text-stone-600"
"This is the deepest consequence of ontological uniformity. The medium is not just " (em "described by") " itself — it " (em "grows from") " itself. New components, new routing patterns, new wire formats, new rendering modes — all are expressions evaluated by the evaluator that is itself an expression. The system is " (a :href "https://en.wikipedia.org/wiki/Autopoiesis" :class "text-violet-600 hover:underline" "autopoietic") ": it produces and maintains itself through the same operations it performs."))
(~doc-section :title "The test, revisited" :id "test-revisited"
(~docs/section :title "The test, revisited" :id "test-revisited"
(p :class "text-stone-600"
"Can the medium define itself with itself?")
(p :class "text-stone-600"

View File

@@ -1,15 +1,15 @@
(defcomp ~essay-separation-of-concerns ()
(~doc-page :title "Separate your Own Concerns"
(defcomp ~essays/separation-of-concerns/essay-separation-of-concerns ()
(~docs/page :title "Separate your Own Concerns"
(p :class "text-stone-500 text-sm italic mb-8"
"The web's canonical separation — HTML, CSS, JavaScript — separates the framework's concerns, not yours. Real separation of concerns is domain-specific and cannot be prescribed by a platform.")
(~doc-section :title "The orthodoxy" :id "orthodoxy"
(~docs/section :title "The orthodoxy" :id "orthodoxy"
(p :class "text-stone-600"
"Web development has an article of faith: separate your concerns. Put structure in HTML. Put presentation in CSS. Put behavior in JavaScript. Three languages, three files, three concerns. This is presented as a universal engineering principle — the web platform's gift to good architecture.")
(p :class "text-stone-600"
"It is nothing of the sort. It is the " (em "framework's") " separation of concerns, not the " (em "application's") ". The web platform needs an HTML parser, a CSS engine, and a JavaScript runtime. These are implementation boundaries internal to the browser. Elevating them to an architectural principle for application developers is like telling a novelist to keep their nouns in one file, verbs in another, and adjectives in a third — because that's how the compiler organises its grammar."))
(~doc-section :title "What is a concern?" :id "what-is-a-concern"
(~docs/section :title "What is a concern?" :id "what-is-a-concern"
(p :class "text-stone-600"
"A concern is a cohesive unit of functionality that can change independently. In a shopping application, concerns might be: the product card, the cart, the checkout flow, the search bar. Each of these has structure, style, and behavior that change together. When you redesign the product card, you change its markup, its CSS, and its click handlers — simultaneously, for the same reason, in response to the same requirement.")
(p :class "text-stone-600"
@@ -17,7 +17,7 @@
(p :class "text-stone-600"
"This is not separation of concerns. It is " (strong "commingling") " of concerns, organized by language rather than by meaning."))
(~doc-section :title "The framework's concerns are not yours" :id "framework-concerns"
(~docs/section :title "The framework's concerns are not yours" :id "framework-concerns"
(p :class "text-stone-600"
"The browser has good reasons to separate HTML, CSS, and JavaScript. The HTML parser builds a DOM tree. The CSS engine resolves styles and computes layout. The JS runtime manages execution contexts, event loops, and garbage collection. These are distinct subsystems with distinct performance characteristics, security models, and parsing strategies.")
(p :class "text-stone-600"
@@ -25,7 +25,7 @@
(p :class "text-stone-600"
"When a framework tells you to separate by technology — HTML here, CSS there, JS over there — it is asking you to organize your application around " (em "its") " architecture, not around your problem domain. You are serving the framework's interests. The framework is not serving yours."))
(~doc-section :title "React understood the problem" :id "react"
(~docs/section :title "React understood the problem" :id "react"
(p :class "text-stone-600"
"React's most radical insight was not the virtual DOM or one-way data flow. It was the assertion that a component — markup, style, behavior, all co-located — is the right unit of abstraction for UI. JSX was controversial precisely because it violated the orthodoxy. You are putting HTML in your JavaScript! The concerns are not separated!")
(p :class "text-stone-600"
@@ -33,7 +33,7 @@
(p :class "text-stone-600"
"CSS-in-JS libraries followed the same logic. If styles belong to a component, they should live with that component. Not in a global stylesheet where any selector can collide with any other. The backlash — \"you're mixing concerns!\" — betrayed a fundamental confusion between " (em "technologies") " and " (em "concerns") "."))
(~doc-section :title "Separation of concerns is domain-specific" :id "domain-specific"
(~docs/section :title "Separation of concerns is domain-specific" :id "domain-specific"
(p :class "text-stone-600"
"Here is the key point: " (strong "no framework can tell you what your concerns are") ". Concerns are determined by your domain, your requirements, and your rate of change. A medical records system has different concerns from a social media feed. An e-commerce checkout has different concerns from a real-time dashboard. The boundaries between concerns are discovered through building the application, not prescribed in advance by a platform specification.")
(p :class "text-stone-600"
@@ -41,10 +41,10 @@
(p :class "text-stone-600"
"The right question is never \"are your HTML, CSS, and JS in separate files?\" The right question is: \"when a requirement changes, how many files do you touch, and how many of those changes are unrelated to each other?\" If you touch three files and all three changes serve the same requirement, your concerns are not separated — they are scattered."))
(~doc-section :title "What SX does differently" :id "sx-approach"
(~docs/section :title "What SX does differently" :id "sx-approach"
(p :class "text-stone-600"
"An SX component is a single expression that contains its structure, its style (as keyword-resolved CSS classes), and its behavior (event bindings, conditionals, data flow). Nothing is in a separate file unless it genuinely represents a separate concern.")
(~doc-code :code "(defcomp ~product-card (&key product on-add)
(~docs/code :code "(defcomp ~essays/separation-of-concerns/product-card (&key product on-add)
(div :class \"rounded-lg border border-stone-200 p-4 hover:shadow-md transition-shadow\"
(img :src (get product \"image\") :alt (get product \"name\")
:class \"w-full h-48 object-cover rounded\")
@@ -64,7 +64,7 @@
(p :class "text-stone-600"
"This is not a rejection of separation of concerns. It is separation of concerns taken seriously — by the domain, not by the framework."))
(~doc-section :title "When real separation matters" :id "real-separation"
(~docs/section :title "When real separation matters" :id "real-separation"
(p :class "text-stone-600"
"Genuine separation of concerns still applies, but at the right boundaries:")
(ul :class "space-y-2 text-stone-600"
@@ -75,7 +75,7 @@
(p :class "text-stone-600"
"These boundaries emerge from the application's actual structure. They happen to cut across HTML, CSS, and JavaScript freely — because those categories were never meaningful to begin with."))
(~doc-section :title "The cost of the wrong separation" :id "cost"
(~docs/section :title "The cost of the wrong separation" :id "cost"
(p :class "text-stone-600"
"The HTML/CSS/JS separation has real costs that have been absorbed so thoroughly they are invisible:")
(ul :class "space-y-2 text-stone-600"
@@ -86,7 +86,7 @@
(p :class "text-stone-600"
"Every one of these problems vanishes when style, structure, and behavior are co-located in a component. Delete the component, and its styles, markup, and handlers are gone. No orphans. No archaeology."))
(~doc-section :title "The principle, stated plainly" :id "principle"
(~docs/section :title "The principle, stated plainly" :id "principle"
(p :class "text-stone-600"
"Separation of concerns is a domain-specific design decision. It cannot be imposed by a framework. The web platform's HTML/CSS/JS split is an implementation detail of the browser, not an architectural principle for applications. Treating it as one has cost the industry decades of unnecessary complexity, tooling, and convention.")
(p :class "text-stone-600"

View File

@@ -1,9 +1,9 @@
(defcomp ~essay-server-architecture ()
(~doc-page :title "Server Architecture"
(defcomp ~essays/server-architecture/essay-server-architecture ()
(~docs/page :title "Server Architecture"
(p :class "text-stone-500 text-sm italic mb-8"
"How SX enforces the boundary between host language and embedded language, why that boundary matters, and what it looks like across different target languages.")
(~doc-section :title "The island constraint" :id "island"
(~docs/section :title "The island constraint" :id "island"
(p :class "text-stone-600"
"SX is an embedded language. It runs inside a host language — for example Python on the server, JavaScript in the browser. The central architectural constraint is that SX is a " (strong "pure island") ": the evaluator sees values in and values out. No host objects leak into the SX environment. No SX expressions reach into host internals. Every interaction between SX and the host passes through a declared, validated boundary.")
(p :class "text-stone-600"
@@ -11,7 +11,7 @@
(p :class "text-stone-600"
"The constraint: " (strong "nothing crosses the boundary unless it is declared in a spec file and its type is one of the boundary types") "."))
(~doc-section :title "Three tiers" :id "tiers"
(~docs/section :title "Three tiers" :id "tiers"
(p :class "text-stone-600"
"Host functions that SX can call are organized into three tiers, each with different trust levels and declaration requirements:")
(div :class "space-y-4"
@@ -28,16 +28,16 @@
(p :class "text-stone-600 text-sm"
"Also declared in " (code :class "text-violet-700 text-sm" "boundary.sx") ". Service-scoped Python functions registered via " (code :class "text-violet-700 text-sm" "register_page_helpers()") ". They provide data for specific page types — syntax highlighting, reference table data, bootstrapper output. Each helper is bound to a specific service and available only in that service's page evaluation environment."))))
(~doc-section :title "Boundary types" :id "types"
(~docs/section :title "Boundary types" :id "types"
(p :class "text-stone-600"
"Only these types may cross the host-SX boundary:")
(~doc-code :code (highlight "(define-boundary-types\n (list \"number\" \"string\" \"boolean\" \"nil\" \"keyword\"\n \"list\" \"dict\" \"sx-source\" \"style-value\"))" "lisp"))
(~docs/code :code (highlight "(define-boundary-types\n (list \"number\" \"string\" \"boolean\" \"nil\" \"keyword\"\n \"list\" \"dict\" \"sx-source\" \"style-value\"))" "lisp"))
(p :class "text-stone-600"
"No Python " (code :class "text-violet-700 text-sm" "datetime") " objects. No ORM models. No Quart request objects. If a host function returns a " (code :class "text-violet-700 text-sm" "datetime") ", it must convert to an ISO string before crossing. If it returns a database row, it must convert to a plain dict. The boundary validation checks this recursively — lists and dicts have their elements checked too.")
(p :class "text-stone-600"
"The " (code :class "text-violet-700 text-sm" "sx-source") " type is SX source text wrapped in an " (code :class "text-violet-700 text-sm" "SxExpr") " marker. It allows the host to pass pre-rendered SX markup into the tree — but only the host can create it. SX code cannot construct SxExpr values; it can only receive them from the boundary."))
(~doc-section :title "Enforcement" :id "enforcement"
(~docs/section :title "Enforcement" :id "enforcement"
(p :class "text-stone-600"
"The boundary contract is enforced at three points, each corresponding to a tier:")
(div :class "space-y-3"
@@ -53,15 +53,15 @@
(p :class "text-stone-600"
"All three checks are controlled by the " (code :class "text-violet-700 text-sm" "SX_BOUNDARY_STRICT") " environment variable. With " (code :class "text-violet-700 text-sm" "\"1\"") " (the production default), violations crash at startup. Without it, they log warnings. The strict mode is set in both " (code :class "text-violet-700 text-sm" "docker-compose.yml") " and " (code :class "text-violet-700 text-sm" "docker-compose.dev.yml") "."))
(~doc-section :title "The SX-in-Python rule" :id "sx-in-python"
(~docs/section :title "The SX-in-Python rule" :id "sx-in-python"
(p :class "text-stone-600"
"One enforcement that is not automated but equally important: " (strong "SX source code must not be constructed as Python strings") ". S-expressions belong in " (code :class "text-violet-700 text-sm" ".sx") " files. Python belongs in " (code :class "text-violet-700 text-sm" ".py") " files. If you see a Python f-string that builds " (code :class "text-violet-700 text-sm" "(div :class ...)") ", that is a boundary violation.")
(p :class "text-stone-600"
"The correct pattern: Python returns " (strong "data") " (dicts, lists, strings). " (code :class "text-violet-700 text-sm" ".sx") " files receive data via keyword args and compose the markup. The only exception is " (code :class "text-violet-700 text-sm" "SxExpr") " wrappers for pre-rendered fragments — and those should be built with " (code :class "text-violet-700 text-sm" "sx_call()") " or " (code :class "text-violet-700 text-sm" "_sx_fragment()") ", never with f-strings.")
(~doc-code :code (highlight ";; CORRECT: .sx file composes markup from data\n(defcomp ~my-page (&key items)\n (div :class \"space-y-4\"\n (map (fn (item)\n (div :class \"border rounded p-3\"\n (h3 (get item \"title\"))\n (p (get item \"desc\"))))\n items)))" "lisp"))
(~doc-code :code (highlight "# CORRECT: Python returns data\ndef _my_page_data():\n return {\"items\": [{\"title\": \"A\", \"desc\": \"B\"}]}\n\n# WRONG: Python builds SX source\ndef _my_page_data():\n return SxExpr(f'(div (h3 \"{title}\"))') # NO" "python")))
(~docs/code :code (highlight ";; CORRECT: .sx file composes markup from data\n(defcomp ~essays/server-architecture/my-page (&key items)\n (div :class \"space-y-4\"\n (map (fn (item)\n (div :class \"border rounded p-3\"\n (h3 (get item \"title\"))\n (p (get item \"desc\"))))\n items)))" "lisp"))
(~docs/code :code (highlight "# CORRECT: Python returns data\ndef _my_page_data():\n return {\"items\": [{\"title\": \"A\", \"desc\": \"B\"}]}\n\n# WRONG: Python builds SX source\ndef _my_page_data():\n return SxExpr(f'(div (h3 \"{title}\"))') # NO" "python")))
(~doc-section :title "Why this matters for multiple languages" :id "languages"
(~docs/section :title "Why this matters for multiple languages" :id "languages"
(p :class "text-stone-600"
"The boundary contract is target-agnostic. " (code :class "text-violet-700 text-sm" "boundary.sx") " and " (code :class "text-violet-700 text-sm" "primitives.sx") " declare what crosses the boundary. Each bootstrapper reads those declarations and emits the strongest enforcement the target language supports:")
(div :class "overflow-x-auto rounded border border-stone-200 my-4"
@@ -96,7 +96,7 @@
(p :class "text-stone-600"
"This is the payoff of the pure island constraint. Because SX never touches host internals, a bootstrapper for a new target only needs to implement the declared primitives and boundary functions. The evaluator, renderer, parser, and all components work unchanged. One spec, every target, same guarantees."))
(~doc-section :title "The spec as contract" :id "contract"
(~docs/section :title "The spec as contract" :id "contract"
(p :class "text-stone-600"
"The boundary enforcement files form a closed contract:")
(ul :class "space-y-2 text-stone-600"

View File

@@ -1,9 +1,9 @@
(defcomp ~essay-sx-and-ai ()
(~doc-page :title "SX and AI"
(defcomp ~essays/sx-and-ai/essay-sx-and-ai ()
(~docs/page :title "SX and AI"
(p :class "text-stone-500 text-sm italic mb-8"
"Why s-expressions are the most AI-friendly representation for web interfaces — and what that means for how software gets built.")
(~doc-section :title "The syntax tax" :id "syntax-tax"
(~docs/section :title "The syntax tax" :id "syntax-tax"
(p :class "text-stone-600"
"Every programming language imposes a syntax tax on AI code generation. The model must produce output that satisfies a grammar — matching braces, semicolons in the right places, operator precedence, indentation rules, closing tags that match opening tags. The more complex the grammar, the more tokens the model wastes on syntactic bookkeeping instead of semantic intent.")
(p :class "text-stone-600"
@@ -13,16 +13,16 @@
(p :class "text-stone-600"
"The syntax tax for SX is essentially zero. An AI that can count parentheses can produce syntactically valid SX. This is not a small advantage — it is a categorical one. The model spends its capacity on " (em "what") " to generate, not " (em "how") " to format it."))
(~doc-section :title "One representation for everything" :id "one-representation"
(~docs/section :title "One representation for everything" :id "one-representation"
(p :class "text-stone-600"
"A typical web project requires the AI to context-switch between HTML (angle brackets, void elements, boolean attributes), CSS (selectors, properties, at-rules, a completely different syntax from HTML), JavaScript (statements, expressions, classes, closures, async/await), and whatever templating language glues them together (Jinja delimiters, ERB tags, JSX interpolation). Each is a separate grammar. Each has edge cases. Each interacts with the others in ways that are hard to predict.")
(p :class "text-stone-600"
"In SX, structure, style, logic, and data are all s-expressions:")
(~doc-code :code (highlight ";; Structure\n(div :class \"card\" (h2 title) (p body))\n\n;; Style\n(cssx card-style\n :bg white :rounded-lg :shadow-md :p 6)\n\n;; Logic\n(if (> (length items) 0)\n (map render-item items)\n (p \"No items found.\"))\n\n;; Data\n{:name \"Alice\" :role \"admin\" :active true}\n\n;; Component definition\n(defcomp ~user-card (&key user)\n (div :class \"card\"\n (h2 (get user \"name\"))\n (span :class \"badge\" (get user \"role\"))))" "lisp"))
(~docs/code :code (highlight ";; Structure\n(div :class \"card\" (h2 title) (p body))\n\n;; Style\n(cssx card-style\n :bg white :rounded-lg :shadow-md :p 6)\n\n;; Logic\n(if (> (length items) 0)\n (map render-item items)\n (p \"No items found.\"))\n\n;; Data\n{:name \"Alice\" :role \"admin\" :active true}\n\n;; Component definition\n(defcomp ~essays/sx-and-ai/user-card (&key user)\n (div :class \"card\"\n (h2 (get user \"name\"))\n (span :class \"badge\" (get user \"role\"))))" "lisp"))
(p :class "text-stone-600"
"The AI learns one syntax and applies it everywhere. The mental model does not fragment across subsystems. A " (code "div") " and an " (code "if") " and a " (code "defcomp") " are all lists. The model that generates one can generate all three, because they are the same thing."))
(~doc-section :title "The spec fits in a context window" :id "spec-fits"
(~docs/section :title "The spec fits in a context window" :id "spec-fits"
(p :class "text-stone-600"
"The complete SX language specification — evaluator, parser, renderer, primitives — lives in four files totalling roughly 3,000 lines. An AI model with a 200k token context window can hold the " (em "entire language definition") " alongside the user's codebase and still have room to work. Compare this to JavaScript (the " (a :href "https://ecma-international.org/publications-and-standards/standards/ecma-262/" :class "text-violet-600 hover:underline" "ECMAScript specification") " is 900+ pages), or the combined specifications for HTML, CSS, and the DOM.")
(p :class "text-stone-600"
@@ -30,7 +30,7 @@
(p :class "text-stone-600"
"The spec is also written in SX. " (code "eval.sx") " defines the evaluator as s-expressions. " (code "parser.sx") " defines the parser as s-expressions. The language the AI is generating is the same language the spec is written in. There is no translation gap between \"understanding the language\" and \"using the language\" — they are the same act of reading s-expressions."))
(~doc-section :title "Structural validation is trivial" :id "structural-validation"
(~docs/section :title "Structural validation is trivial" :id "structural-validation"
(p :class "text-stone-600"
"Validating AI output before executing it is a critical safety concern. With conventional languages, validation means running a full parser, type checker, and linter — each with their own error recovery modes and edge cases. With SX, structural validation is: " (em "do the parentheses balance?") " That is it. If they balance, the expression parses. If it parses, it can be evaluated.")
(p :class "text-stone-600"
@@ -38,37 +38,37 @@
(p :class "text-stone-600"
"Beyond parsing, the SX " (a :href "/sx/(language.(spec.primitives))" :class "text-violet-600 hover:underline" "boundary system") " provides semantic validation. A pure component cannot call IO primitives — not by convention, but by the evaluator refusing to resolve them. An AI generating a component can produce whatever expressions it wants; the sandbox ensures only permitted operations execute. Validation is not a separate step bolted onto the pipeline. It is the language."))
(~doc-section :title "Components are self-documenting" :id "self-documenting"
(~docs/section :title "Components are self-documenting" :id "self-documenting"
(p :class "text-stone-600"
"A React component's interface is spread across prop types (or TypeScript interfaces), JSDoc comments, Storybook stories, and whatever documentation someone wrote. An AI reading a component must synthesize information from multiple sources to understand what it accepts and what it produces.")
(p :class "text-stone-600"
"An SX component declares everything in one expression:")
(~doc-code :code (highlight "(defcomp ~product-card (&key title price image &rest children)\n (div :class \"rounded border p-4\"\n (img :src image :alt title)\n (h3 :class \"font-bold\" title)\n (span :class \"text-lg\" (format-price price))\n children))" "lisp"))
(~docs/code :code (highlight "(defcomp ~essays/sx-and-ai/product-card (&key title price image &rest children)\n (div :class \"rounded border p-4\"\n (img :src image :alt title)\n (h3 :class \"font-bold\" title)\n (span :class \"text-lg\" (format-price price))\n children))" "lisp"))
(p :class "text-stone-600"
"The AI reads this and knows: it takes " (code "title") ", " (code "price") ", and " (code "image") " as keyword arguments, and " (code "children") " as rest arguments. It knows the output structure — a " (code "div") " with an image, heading, price, and slot for children. It knows this because the definition " (em "is") " the documentation. There is no separate spec to consult, no type file to find, no ambiguity about which props are required.")
(p :class "text-stone-600"
"This self-describing property scales across the entire component environment. An AI can " (code "(map ...)") " over every component in the registry, extract all parameter signatures, build a complete map of the UI vocabulary — and generate compositions that use it correctly, because the interface is declared in the same language the AI is generating."))
(~doc-section :title "Token efficiency" :id "token-efficiency"
(~docs/section :title "Token efficiency" :id "token-efficiency"
(p :class "text-stone-600"
"LLMs operate on tokens. Every token costs compute, latency, and money. The information density of a representation — how much semantics per token — directly affects how much an AI can see, generate, and reason about within its context window and output budget.")
(p :class "text-stone-600"
"Compare equivalent UI definitions:")
(~doc-code :code (highlight ";; SX: 42 tokens\n(div :class \"card p-4\"\n (h2 :class \"font-bold\" title)\n (p body)\n (when footer\n (div :class \"mt-4 border-t pt-2\" footer)))" "lisp"))
(~doc-code :code (highlight "// React/JSX: ~75 tokens\n<div className=\"card p-4\">\n <h2 className=\"font-bold\">{title}</h2>\n <p>{body}</p>\n {footer && (\n <div className=\"mt-4 border-t pt-2\">{footer}</div>\n )}\n</div>" "python"))
(~docs/code :code (highlight ";; SX: 42 tokens\n(div :class \"card p-4\"\n (h2 :class \"font-bold\" title)\n (p body)\n (when footer\n (div :class \"mt-4 border-t pt-2\" footer)))" "lisp"))
(~docs/code :code (highlight "// React/JSX: ~75 tokens\n<div className=\"card p-4\">\n <h2 className=\"font-bold\">{title}</h2>\n <p>{body}</p>\n {footer && (\n <div className=\"mt-4 border-t pt-2\">{footer}</div>\n )}\n</div>" "python"))
(p :class "text-stone-600"
"The SX version is roughly 40% fewer tokens for equivalent semantics. No closing tags. No curly-brace interpolation. No " (code "className") " vs " (code "class") " distinction. Every token carries meaning. Over an entire application — dozens of components, hundreds of expressions — this compounds into significantly more code visible per context window and significantly less output the model must generate."))
(~doc-section :title "Composability is free" :id "composability"
(~docs/section :title "Composability is free" :id "composability"
(p :class "text-stone-600"
"The hardest thing for AI to get right in conventional frameworks is composition — how pieces fit together. React has rules about hooks. Vue has template vs script vs style sections. Angular has modules, declarations, and dependency injection. Each framework's composition model is a set of conventions the AI must learn and apply correctly.")
(p :class "text-stone-600"
"S-expressions compose by nesting. A list inside a list is a composition. There are no rules beyond this:")
(~doc-code :code (highlight ";; Compose components by nesting — that's it\n(~page-layout :title \"Dashboard\"\n (~sidebar\n (~nav-menu :items menu-items))\n (~main-content\n (map ~user-card users)\n (~pagination :page current-page :total total-pages)))" "lisp"))
(~docs/code :code (highlight ";; Compose components by nesting — that's it\n(~page-layout :title \"Dashboard\"\n (~sidebar\n (~nav-menu :items menu-items))\n (~main-content\n (map ~essays/sx-and-ai/user-card users)\n (~pagination :page current-page :total total-pages)))" "lisp"))
(p :class "text-stone-600"
"No imports to manage. No registration steps. No render props, higher-order components, or composition APIs. The AI generates a nested structure and it works, because nesting is the only composition mechanism. This eliminates an entire class of errors that plague AI-generated code in conventional frameworks — the kind where each piece works in isolation but the assembly is wrong."))
(~doc-section :title "The feedback loop" :id "feedback-loop"
(~docs/section :title "The feedback loop" :id "feedback-loop"
(p :class "text-stone-600"
"SX has no build step. Generated s-expressions can be evaluated immediately — in the browser, on the server, in a test harness. The AI generates an expression, the system evaluates it, the result is visible. If it is wrong, the AI reads the result (also an s-expression), adjusts, and regenerates. The loop is:")
(ol :class "list-decimal pl-5 text-stone-600 space-y-1 mt-2"
@@ -82,17 +82,17 @@
(p :class "text-stone-600"
"The SX loop is also " (em "uniform") ". The input is s-expressions. The output is s-expressions. The error messages are s-expressions. The AI never needs to parse a stack trace format or extract meaning from a webpack error. Everything is the same data structure, all the way down."))
(~doc-section :title "This site is the proof" :id "proof"
(~docs/section :title "This site is the proof" :id "proof"
(p :class "text-stone-600"
"This is not theoretical. Everything you are looking at — every page, every component, every line of this essay — was produced by agentic AI. Not \"AI-assisted\" in the polite sense of autocomplete suggestions. " (em "Produced.") " The SX language specification. The parser. The evaluator. The renderer. The bootstrappers that transpile the spec to JavaScript and Python. The boundary enforcement system. The dependency analyser. The on-demand CSS engine. The client-side router. The component bundler. The syntax highlighter. This documentation site. The Docker deployment. All of it.")
(p :class "text-stone-600"
"The human driving this has never written a line of Lisp. Not Common Lisp. Not Scheme. Not Clojure. Not Emacs Lisp. Has never opened the codebase in VS Code, vi, or any other editor. Every file was created and modified through " (a :href "https://claude.ai/" :class "text-violet-600 hover:underline" "Claude") " running in a terminal — reading files, writing files, running commands, iterating on errors. The development environment is a conversation.")
(p :class "text-stone-600"
"That this works at all is a testament to s-expressions. The AI generates " (code "(defcomp ~card (&key title) (div :class \"p-4\" (h2 title)))") " and it is correct on the first attempt, because there is almost nothing to get wrong. The AI generates a 300-line spec file defining evaluator semantics and every parenthesis balances, because balancing parentheses is the " (em "only") " syntactic constraint. The AI writes a bootstrapper that reads " (code "eval.sx") " and emits JavaScript, and the output runs in the browser, because the source and target are both trees.")
"That this works at all is a testament to s-expressions. The AI generates " (code "(defcomp ~essays/sx-and-ai/card (&key title) (div :class \"p-4\" (h2 title)))") " and it is correct on the first attempt, because there is almost nothing to get wrong. The AI generates a 300-line spec file defining evaluator semantics and every parenthesis balances, because balancing parentheses is the " (em "only") " syntactic constraint. The AI writes a bootstrapper that reads " (code "eval.sx") " and emits JavaScript, and the output runs in the browser, because the source and target are both trees.")
(p :class "text-stone-600"
"Try this with React. Try generating a complete component framework — parser, evaluator, renderer, type system, macro expander, CSS engine, client router — through pure conversation with an AI, never touching an editor. The syntax tax alone would be fatal. JSX irregularities, hook ordering rules, import resolution, TypeScript generics, webpack configuration, CSS module scoping — each is a class of errors that burns tokens and breaks the flow. S-expressions eliminate all of them."))
(~doc-section :title "The development loop" :id "dev-loop"
(~docs/section :title "The development loop" :id "dev-loop"
(p :class "text-stone-600"
"The workflow looks like this: describe what you want. The AI reads the existing code — because it can, because s-expressions are transparent to any reader. It generates new expressions. It writes them to disk. It runs the server. It checks the output. If something breaks, it reads the error, adjusts, and regenerates. The human steers with intent; the AI handles the syntax, the structure, and the mechanical correctness.")
(p :class "text-stone-600"
@@ -100,7 +100,7 @@
(p :class "text-stone-600"
"The " (a :href "/sx/(etc.(essay.sx-sucks))" :class "text-violet-600 hover:underline" "sx sucks") " essay copped to the AI authorship and framed it as a weakness — microwave dinner on a nice plate. But the framing was wrong. If a language is so well-suited to machine generation that one person with no Lisp experience can build a self-hosting language, a multi-target bootstrapper, a reactive component framework, and a full documentation site through pure agentic AI — that is not a weakness of the language. That is the language working exactly as it should."))
(~doc-section :title "What this changes" :id "what-changes"
(~docs/section :title "What this changes" :id "what-changes"
(p :class "text-stone-600"
"The question is not whether AI will generate user interfaces. It already does. The question is what representation makes that generation most reliable, most efficient, and most safe. S-expressions — with their zero-syntax-tax grammar, uniform structure, self-describing components, structural validation, and sandboxed evaluation — are a strong answer.")
(p :class "text-stone-600"

View File

@@ -1,8 +1,8 @@
(defcomp ~essay-sx-and-dennett ()
(~doc-page :title "SX and Dennett"
(defcomp ~essays/sx-and-dennett/essay-sx-and-dennett ()
(~docs/page :title "SX and Dennett"
(p :class "text-stone-500 text-sm italic mb-8"
"Real patterns, multiple drafts, and the intentional stance — a philosopher of mind meets a language that thinks about itself.")
(~doc-section :title "I. The intentional stance" :id "intentional-stance"
(~docs/section :title "I. The intentional stance" :id "intentional-stance"
(p :class "text-stone-600"
"Daniel " (a :href "https://en.wikipedia.org/wiki/Daniel_Dennett" :class "text-violet-600 hover:underline" "Dennett") " spent fifty years arguing that the mind is not what it seems. His central method is the " (a :href "https://en.wikipedia.org/wiki/Intentional_stance" :class "text-violet-600 hover:underline" "intentional stance") " — a strategy for predicting a system's behaviour by treating it " (em "as if") " it has beliefs, desires, and intentions, whether or not it \"really\" does.")
(p :class "text-stone-600"
@@ -10,25 +10,25 @@
(p :class "text-stone-600"
"Web frameworks enforce a single stance. React's mental model is the design stance: components are functions, props go in, JSX comes out. You reason about the system by reasoning about its design. If you need the physical stance (what is actually in the DOM right now?), you reach for " (code "useRef") ". If you need the intentional stance (what does this component " (em "mean") "?), you read the documentation. Each stance requires a different tool, a different context switch.")
(p :class "text-stone-600"
"SX lets you shift stances without shifting languages. The physical stance: " (code "(div :class \"card\" (h2 \"Title\"))") " — this is exactly the DOM structure that will be produced. One list, one element. The design stance: " (code "(defcomp ~card (&key title) (div title))") " — this is how the component is built, its contract. The intentional stance: " (code "(~card :title \"Hello\")") " — this " (em "means") " \"render a card with this title,\" and you can reason about it at that level without knowing the implementation.")
(~doc-code :lang "lisp" :code
";; Physical stance — the literal structure\n(div :class \"card\" (h2 \"Title\"))\n\n;; Design stance — how it's built\n(defcomp ~card (&key title) (div :class \"card\" (h2 title)))\n\n;; Intentional stance — what it means\n(~card :title \"Title\")\n\n;; All three are s-expressions.\n;; All three can be inspected, transformed, quoted.\n;; Shifting stance = changing which expression you look at.")
"SX lets you shift stances without shifting languages. The physical stance: " (code "(div :class \"card\" (h2 \"Title\"))") " — this is exactly the DOM structure that will be produced. One list, one element. The design stance: " (code "(defcomp ~essays/sx-and-dennett/card (&key title) (div title))") " — this is how the component is built, its contract. The intentional stance: " (code "(~essays/sx-and-dennett/card :title \"Hello\")") " — this " (em "means") " \"render a card with this title,\" and you can reason about it at that level without knowing the implementation.")
(~docs/code :lang "lisp" :code
";; Physical stance — the literal structure\n(div :class \"card\" (h2 \"Title\"))\n\n;; Design stance — how it's built\n(defcomp ~essays/sx-and-dennett/card (&key title) (div :class \"card\" (h2 title)))\n\n;; Intentional stance — what it means\n(~essays/sx-and-dennett/card :title \"Title\")\n\n;; All three are s-expressions.\n;; All three can be inspected, transformed, quoted.\n;; Shifting stance = changing which expression you look at.")
(p :class "text-stone-600"
"The key insight: all three stances are expressed in the same medium. You do not need a debugger for the physical stance, a type system for the design stance, and documentation for the intentional stance. You need lists. The stances are not different tools — they are different ways of reading the same data."))
(~doc-section :title "II. Real patterns" :id "real-patterns"
(~docs/section :title "II. Real patterns" :id "real-patterns"
(p :class "text-stone-600"
"Dennett's 1991 paper \"" (a :href "https://en.wikipedia.org/wiki/Real_Patterns" :class "text-violet-600 hover:underline" "Real Patterns") "\" makes a deceptively simple argument: a pattern is real if it lets you compress data — if recognising the pattern gives you predictive leverage that you would not have otherwise. Patterns are not " (em "in the mind") " of the observer. They are not " (em "in the object") " independently of any observer. They are real features of the world that exist at a particular level of description.")
(p :class "text-stone-600"
"Consider a bitmap of noise. If you describe it pixel by pixel, the description is as long as the image. No compression. No pattern. Now consider a bitmap of a checkerboard. You can say \"alternating black and white squares, 8x8\" — vastly shorter than the pixel-by-pixel description. The checkerboard pattern is " (em "real") ". It exists in the data. Recognising it gives you compression.")
(p :class "text-stone-600"
"Components are real patterns. " (code "(~card :title \"Hello\")") " compresses " (code "(div :class \"card\" (h2 \"Hello\"))") " — and more importantly, it compresses every instance of card-like structure across the application into a single abstraction. The component is not a convenient fiction. It is a real pattern in the codebase: a regularity that gives you predictive power. When you see " (code "~card") ", you know the structure, the styling, the contract — without expanding the definition.")
"Components are real patterns. " (code "(~essays/sx-and-dennett/card :title \"Hello\")") " compresses " (code "(div :class \"card\" (h2 \"Hello\"))") " — and more importantly, it compresses every instance of card-like structure across the application into a single abstraction. The component is not a convenient fiction. It is a real pattern in the codebase: a regularity that gives you predictive power. When you see " (code "~essays/sx-and-dennett/card") ", you know the structure, the styling, the contract — without expanding the definition.")
(p :class "text-stone-600"
"Macros are real patterns at a higher level. A macro like " (code "defcomp") " captures the pattern of \"name, parameters, body\" that every component shares. It compresses the regularity of component definition itself. The macro is real in exactly Dennett's sense — it captures a genuine pattern, and that pattern gives you leverage.")
(p :class "text-stone-600"
"Now here is where SX makes Dennett's argument concrete. In most languages, the reality of patterns is debatable — are classes real? Are interfaces real? Are design patterns real? You can argue either way because the patterns exist at a different level from the code. In SX, patterns " (em "are") " code. A component is a list. A macro is a function over lists. The pattern and the data it describes are the same kind of thing — s-expressions. There is no level-of-description gap. The pattern is as real as the data it compresses, because they inhabit the same ontological plane.")
(~doc-code :lang "lisp" :code
";; The data (expanded)\n(div :class \"card\"\n (h2 \"Pattern\")\n (p \"A real one.\"))\n\n;; The pattern (compressed)\n(~card :title \"Pattern\" (p \"A real one.\"))\n\n;; The meta-pattern (the definition)\n(defcomp ~card (&key title &rest children)\n (div :class \"card\" (h2 title) children))\n\n;; All three levels: data, pattern, meta-pattern.\n;; All three are lists. All three are real."))
(~doc-section :title "III. Multiple Drafts" :id "multiple-drafts"
(~docs/code :lang "lisp" :code
";; The data (expanded)\n(div :class \"card\"\n (h2 \"Pattern\")\n (p \"A real one.\"))\n\n;; The pattern (compressed)\n(~essays/sx-and-dennett/card :title \"Pattern\" (p \"A real one.\"))\n\n;; The meta-pattern (the definition)\n(defcomp ~essays/sx-and-dennett/card (&key title &rest children)\n (div :class \"card\" (h2 title) children))\n\n;; All three levels: data, pattern, meta-pattern.\n;; All three are lists. All three are real."))
(~docs/section :title "III. Multiple Drafts" :id "multiple-drafts"
(p :class "text-stone-600"
"In " (a :href "https://en.wikipedia.org/wiki/Consciousness_Explained" :class "text-violet-600 hover:underline" "Consciousness Explained") " (1991), Dennett proposed the " (a :href "https://en.wikipedia.org/wiki/Multiple_drafts_model" :class "text-violet-600 hover:underline" "Multiple Drafts model") " of consciousness. There is no " (a :href "https://en.wikipedia.org/wiki/Cartesian_theater" :class "text-violet-600 hover:underline" "Cartesian theater") " — no single place in the brain where \"it all comes together\" for a central observer. Instead, multiple parallel processes generate content simultaneously. Various drafts of narrative are in process at any time, some getting revised, some abandoned, some incorporated into the ongoing story. There is no master draft. There is no final audience. There is just the process of revision itself.")
(p :class "text-stone-600"
@@ -44,51 +44,51 @@
"Each draft is a complete s-expression. Each is meaningful on its own terms. No single process \"sees\" the whole page — the server doesn't see the DOM, the client doesn't see the Python context, the browser's layout engine doesn't see the s-expressions. The page emerges from the drafting process, not from a central reconciler.")
(p :class "text-stone-600"
"This is not a metaphor stretched over engineering. It is the actual architecture. There is no virtual DOM because there is no need for a Cartesian theater. The multiple drafts model works because each draft is in the same format — s-expressions — so revision is natural. A draft can be inspected, compared, serialised, sent somewhere else, and revised further. Dennett's insight was that consciousness works this way. SX's insight is that rendering can too."))
(~doc-section :title "IV. Heterophenomenology" :id "heterophenomenology"
(~docs/section :title "IV. Heterophenomenology" :id "heterophenomenology"
(p :class "text-stone-600"
(a :href "https://en.wikipedia.org/wiki/Heterophenomenology" :class "text-violet-600 hover:underline" "Heterophenomenology") " is Dennett's method for studying consciousness. Instead of asking \"what is it like to be a bat?\" — a question we cannot answer — we ask the bat to tell us, and then we study " (em "the report") ". We take the subject's testimony seriously, catalogue it rigorously, but we do not take it as infallible. The report is data. We are scientists of the report.")
(p :class "text-stone-600"
"Most programming languages cannot report on themselves. JavaScript can " (code "toString()") " a function, but the result is a string — opaque, unparseable, implementation-dependent. Python can inspect a function's AST via " (code "ast.parse(inspect.getsource(f))") " — but the AST is a separate data structure, disconnected from the running code. The language's self-report is in a different format from the language itself. Studying it requires tools, transformations, bridges.")
(p :class "text-stone-600"
"SX is natively heterophenomenological. The language's self-report " (em "is") " the language. " (code "eval.sx") " is the evaluator reporting on how evaluation works — in the same s-expressions that it evaluates. " (code "parser.sx") " is the parser reporting on how parsing works — in the same syntax it parses. You study the report by reading it. You verify the report by running it. The report and the reality are the same object.")
(~doc-code :lang "lisp" :code
(~docs/code :lang "lisp" :code
";; The evaluator's self-report (from eval.sx):\n(define eval-expr\n (fn (expr env)\n (cond\n (number? expr) expr\n (string? expr) expr\n (symbol? expr) (env-get env expr)\n (list? expr) (eval-list expr env)\n :else (error \"Unknown expression type\"))))\n\n;; This is simultaneously:\n;; 1. A specification (what eval-expr does)\n;; 2. A program (it runs)\n;; 3. A report (the evaluator describing itself)\n;; Heterophenomenology without the hetero.")
(p :class "text-stone-600"
"Dennett insisted that heterophenomenology is the only honest method. First-person reports are unreliable — introspection gets things wrong. Third-person observation misses the subject's perspective. The middle path is to take the report as data and study it rigorously. SX's self-hosting spec is this middle path enacted in code: neither a first-person account (\"trust me, this is how it works\") nor a third-person observation (English prose describing the implementation), but a structured report that can be verified, compiled, and run."))
(~doc-section :title "V. Where am I?" :id "where-am-i"
(~docs/section :title "V. Where am I?" :id "where-am-i"
(p :class "text-stone-600"
"Dennett's thought experiment \"" (a :href "https://en.wikipedia.org/wiki/Where_Am_I%3F_(Dennett)" :class "text-violet-600 hover:underline" "Where Am I?") "\" imagines his brain removed from his body, connected by radio. His body walks around; his brain sits in a vat. Where is Dennett? Where the brain is? Where the body is? The question has no clean answer because identity is not located in a single place — it is distributed across the system.")
(p :class "text-stone-600"
"Where is an SX component? On the server, it is a Python object — a closure with a body and bound environment. On the wire, it is text: " (code "(~card :title \"Hello\")") ". In the browser, it is a JavaScript function registered in the component environment. In the DOM, it is a tree of elements. Which one is the \"real\" component? All of them. None of them. The component is not located in one runtime — it is the pattern that persists across all of them.")
"Where is an SX component? On the server, it is a Python object — a closure with a body and bound environment. On the wire, it is text: " (code "(~essays/sx-and-dennett/card :title \"Hello\")") ". In the browser, it is a JavaScript function registered in the component environment. In the DOM, it is a tree of elements. Which one is the \"real\" component? All of them. None of them. The component is not located in one runtime — it is the pattern that persists across all of them.")
(p :class "text-stone-600"
"This is Dennett's point about personal identity applied to software identity. The SX component " (code "~card") " is defined in a " (code ".sx") " file, compiled by the Python bootstrapper into the server evaluator, transmitted as SX wire format to the browser, compiled by the JavaScript bootstrapper into the client evaluator, and rendered into DOM. At every stage, it is " (code "~card") ". At no single stage is it " (em "the") " " (code "~card") ". The identity is the pattern, not the substrate.")
"This is Dennett's point about personal identity applied to software identity. The SX component " (code "~essays/sx-and-dennett/card") " is defined in a " (code ".sx") " file, compiled by the Python bootstrapper into the server evaluator, transmitted as SX wire format to the browser, compiled by the JavaScript bootstrapper into the client evaluator, and rendered into DOM. At every stage, it is " (code "~essays/sx-and-dennett/card") ". At no single stage is it " (em "the") " " (code "~essays/sx-and-dennett/card") ". The identity is the pattern, not the substrate.")
(p :class "text-stone-600"
"Most frameworks bind component identity to a substrate. A React component is a JavaScript function. Full stop. It cannot exist outside the JavaScript runtime. Its identity is its implementation. SX components have substrate independence — the same definition runs on any host that implements the SX platform interface. The component's identity is its specification, not its execution."))
(~doc-section :title "VI. Competence without comprehension" :id "competence"
(~docs/section :title "VI. Competence without comprehension" :id "competence"
(p :class "text-stone-600"
"Dennett argued in " (a :href "https://en.wikipedia.org/wiki/From_Bacteria_to_Bach_and_Back" :class "text-violet-600 hover:underline" "From Bacteria to Bach and Back") " (2017) that evolution produces " (em "competence without comprehension") ". Termites build elaborate mounds without understanding architecture. Neurons produce consciousness without understanding thought. The competence is real — the mound regulates temperature, the brain solves problems — but there is no comprehension anywhere in the system. No termite has a blueprint. No neuron knows it is thinking.")
(p :class "text-stone-600"
"A macro is competence without comprehension. " (code "defcomp") " expands into a component registration — it " (em "does") " the right thing — but it does not \"know\" what a component is. It is a pattern-matching function on lists that produces other lists. The expansion is mechanical, local, uncomprehending. Yet the result is a fully functional component that participates in the rendering pipeline, responds to props, composes with other components. Competence. No comprehension.")
(~doc-code :lang "lisp" :code
(~docs/code :lang "lisp" :code
";; defcomp is a macro — mechanical list transformation\n(defmacro defcomp (name params &rest body)\n `(define ,name (make-component ,params ,@body)))\n\n;; It does not \"understand\" components.\n;; It rearranges symbols according to a rule.\n;; The resulting component works perfectly.\n;; Competence without comprehension.")
(p :class "text-stone-600"
"The bootstrap compiler is another level of the same phenomenon. " (code "bootstrap_js.py") " reads " (code "eval.sx") " and emits JavaScript. It does not understand SX semantics — it applies mechanical transformation rules to s-expression ASTs. Yet its output is a correct, complete SX evaluator. The compiler is competent (it produces working code) without being comprehending (it has no model of what SX expressions mean).")
(p :class "text-stone-600"
"Dennett used this insight to deflate the mystery of intelligence: you do not need a homunculus — a little man inside the machine who \"really\" understands — you just need enough competence at each level. SX embodies this architecturally. No part of the system comprehends the whole. The parser does not know about rendering. The evaluator does not know about HTTP. The bootstrap compiler does not know about the DOM. Each part is a competent specialist. The system works because the parts compose, not because any part understands the composition."))
(~doc-section :title "VII. Intuition pumps" :id "intuition-pumps"
(~docs/section :title "VII. Intuition pumps" :id "intuition-pumps"
(p :class "text-stone-600"
"Dennett called his thought experiments \"" (a :href "https://en.wikipedia.org/wiki/Intuition_pump" :class "text-violet-600 hover:underline" "intuition pumps") "\" — devices for moving your intuitions from one place to another, making the unfamiliar familiar by analogy. Not proofs. Not arguments. Machines for changing how you see.")
(p :class "text-stone-600"
"SX components are intuition pumps. A " (code "defcomp") " definition is not just executable code — it is a device for showing someone what a piece of UI " (em "is") ". Reading " (code "(defcomp ~card (&key title &rest children) (div :class \"card\" (h2 title) children))") " tells you the contract, the structure, and the output in a single expression. It pumps your intuition about what \"card\" means in this application.")
"SX components are intuition pumps. A " (code "defcomp") " definition is not just executable code — it is a device for showing someone what a piece of UI " (em "is") ". Reading " (code "(defcomp ~essays/sx-and-dennett/card (&key title &rest children) (div :class \"card\" (h2 title) children))") " tells you the contract, the structure, and the output in a single expression. It pumps your intuition about what \"card\" means in this application.")
(p :class "text-stone-600"
"Compare this to a React component:")
(~doc-code :lang "lisp" :code
";; React: you must simulate the runtime in your head\nfunction Card({ title, children }) {\n return (\n <div className=\"card\">\n <h2>{title}</h2>\n {children}\n </div>\n );\n}\n\n;; SX: the definition IS the output\n(defcomp ~card (&key title &rest children)\n (div :class \"card\"\n (h2 title)\n children))")
(~docs/code :lang "lisp" :code
";; React: you must simulate the runtime in your head\nfunction Card({ title, children }) {\n return (\n <div className=\"card\">\n <h2>{title}</h2>\n {children}\n </div>\n );\n}\n\n;; SX: the definition IS the output\n(defcomp ~essays/sx-and-dennett/card (&key title &rest children)\n (div :class \"card\"\n (h2 title)\n children))")
(p :class "text-stone-600"
"The React version requires you to know that JSX compiles to createElement calls, that className maps to the class attribute, that curly braces switch to JavaScript expressions, and that the function return value becomes the rendered output. You must simulate a compiler in your head. The SX version requires you to know that lists are expressions and keywords are attributes. The gap between the definition and what it produces is smaller. The intuition pump is more efficient — fewer moving parts, less machinery between the reader and the meaning.")
(p :class "text-stone-600"
"Dennett valued intuition pumps because philosophy is full of false intuitions. The Cartesian theater feels right — of course there is a place where consciousness happens. But it is wrong. Intuition pumps help you " (em "see") " that it is wrong by giving you a better picture. SX is an intuition pump for web development: of course you need a build step, of course you need a virtual DOM, of course you need separate languages for structure and style and behaviour. But you don't. The s-expression is the better picture."))
(~doc-section :title "VIII. The Joycean machine" :id "joycean-machine"
(~docs/section :title "VIII. The Joycean machine" :id "joycean-machine"
(p :class "text-stone-600"
"In Consciousness Explained, Dennett describes the brain as a \"" (a :href "https://en.wikipedia.org/wiki/Consciousness_Explained" :class "text-violet-600 hover:underline" "Joycean machine") "\" — a virtual machine running on the parallel hardware of the brain, producing the serial narrative of conscious experience. Just as a word processor is a virtual machine running on silicon, consciousness is a virtual machine running on neurons. The virtual machine is real — it does real work, produces real effects — even though it is implemented in a substrate that knows nothing about it.")
(p :class "text-stone-600"
@@ -97,7 +97,7 @@
"The deeper parallel: Dennett argued that the Joycean machine is " (em "not an illusion") ". The serial narrative of consciousness is not fake — it is the real output of real processing, even though the underlying hardware is parallel and narrativeless. Similarly, SX's component model is not a convenient fiction layered over \"real\" HTML. It is the real structure of the application. The components are the thing. The HTML is the substrate, not the reality.")
(p :class "text-stone-600"
"And like Dennett's Joycean machine, SX's virtual machine can reflect on itself. It can inspect its own running code, define its own evaluator, test its own semantics. The virtual machine is aware of itself — not in the sense of consciousness, but in the functional sense of self-modelling. The spec models the evaluator. The evaluator runs the spec. The virtual machine contains a description of itself, and that description works."))
(~doc-section :title "IX. Quining" :id "quining"
(~docs/section :title "IX. Quining" :id "quining"
(p :class "text-stone-600"
"Dennett borrowed the term \"" (a :href "https://en.wikipedia.org/wiki/Qualia#Dennett's_criticism" :class "text-violet-600 hover:underline" "quining") "\" from the logician " (a :href "https://en.wikipedia.org/wiki/Willard_Van_Orman_Quine" :class "text-violet-600 hover:underline" "W. V. O. Quine") " — a philosopher who argued that many seemingly deep concepts dissolve under scrutiny. Dennett \"quined\" qualia — the supposedly irreducible subjective qualities of experience — arguing that they are not what they seem, that the intuition of an inner experiential essence is a philosophical illusion.")
(p :class "text-stone-600"

View File

@@ -1,8 +1,8 @@
(defcomp ~essay-sx-and-wittgenstein ()
(~doc-page :title "SX and Wittgenstein"
(defcomp ~essays/sx-and-wittgenstein/essay-sx-and-wittgenstein ()
(~docs/page :title "SX and Wittgenstein"
(p :class "text-stone-500 text-sm italic mb-8"
"The limits of my language are the limits of my world.")
(~doc-section :title "I. Language games" :id "language-games"
(~docs/section :title "I. Language games" :id "language-games"
(p :class "text-stone-600"
"In 1953, Ludwig " (a :href "https://en.wikipedia.org/wiki/Ludwig_Wittgenstein" :class "text-violet-600 hover:underline" "Wittgenstein") " published " (a :href "https://en.wikipedia.org/wiki/Philosophical_Investigations" :class "text-violet-600 hover:underline" "Philosophical Investigations") " — a book that dismantled the theory of language he had built in his own earlier work. The " (a :href "https://en.wikipedia.org/wiki/Tractatus_Logico-Philosophicus" :class "text-violet-600 hover:underline" "Tractatus") " had argued that language pictures the world: propositions mirror facts, and the structure of a sentence corresponds to the structure of reality. The Investigations abandoned this. Language does not picture anything. Language is " (em "used") ".")
(p :class "text-stone-600"
@@ -11,7 +11,7 @@
"Web development is a proliferation of language games. HTML is one game — a markup game where tags denote structure. CSS is another — a declaration game where selectors denote style. JavaScript is a third — an imperative game where statements denote behaviour. JSX is a fourth game layered on top of the third, pretending to be the first. TypeScript is a fifth game that annotates the third. Each has its own grammar, its own rules, its own way of meaning.")
(p :class "text-stone-600"
"SX collapses these into a single game. " (code "(div :class \"p-4\" (h2 title))") " is simultaneously structure (a div containing an h2), style (the class attribute), and behaviour (the symbol " (code "title") " is evaluated). There is one syntax, one set of rules, one way of meaning. Not because the distinctions between structure, style, and behaviour have been erased — they haven't — but because they are all expressed in the same language game."))
(~doc-section :title "II. The limits of my language" :id "limits"
(~docs/section :title "II. The limits of my language" :id "limits"
(p :class "text-stone-600"
"\"" (a :href "https://en.wikipedia.org/wiki/Tractatus_Logico-Philosophicus" :class "text-violet-600 hover:underline" "Die Grenzen meiner Sprache bedeuten die Grenzen meiner Welt") "\" — the limits of my language mean the limits of my world. This is proposition 5.6 of the Tractatus, and it is the most important sentence Wittgenstein ever wrote.")
(p :class "text-stone-600"
@@ -20,31 +20,31 @@
"If your language is React, your world is components that re-render. You can compose components. You can pass props. You cannot inspect a component's structure at runtime without React DevTools. You cannot serialize a component tree to a format another framework can consume. You cannot send a component over HTTP and have it work on the other side without the same React runtime. The language has composition but not portability, so your world has composition but not portability.")
(p :class "text-stone-600"
"If your language is s-expressions, your world is " (em "expressions") ". An expression can represent a DOM node, a function call, a style declaration, a macro transformation, a component definition, a wire-format payload, or a specification of the evaluator itself. The language has no built-in limits on what can be expressed, because the syntax — the list — can represent anything. The limits of the language are only the limits of what you choose to evaluate.")
(~doc-code :lang "lisp" :code
";; The same syntax expresses everything:\n(div :class \"card\" (h2 \"Title\")) ;; structure\n(css :flex :gap-4 :p-2) ;; style\n(defcomp ~card (&key title) (div title)) ;; abstraction\n(defmacro ~log (x) `(console.log ,x)) ;; metaprogramming\n(quote (div :class \"card\" (h2 \"Title\"))) ;; data about structure")
(~docs/code :lang "lisp" :code
";; The same syntax expresses everything:\n(div :class \"card\" (h2 \"Title\")) ;; structure\n(css :flex :gap-4 :p-2) ;; style\n(defcomp ~essays/sx-and-wittgenstein/card (&key title) (div title)) ;; abstraction\n(defmacro ~log (x) `(console.log ,x)) ;; metaprogramming\n(quote (div :class \"card\" (h2 \"Title\"))) ;; data about structure")
(p :class "text-stone-600"
"Wittgenstein's proposition cuts both ways. A language that limits you to documents limits your world to documents. A language that can express anything — because its syntax is the minimal recursive structure — limits your world to " (em "everything") "."))
(~doc-section :title "III. Whereof one cannot speak" :id "silence"
(~docs/section :title "III. Whereof one cannot speak" :id "silence"
(p :class "text-stone-600"
"The Tractatus ends: \"" (a :href "https://en.wikipedia.org/wiki/Tractatus_Logico-Philosophicus#Proposition_7" :class "text-violet-600 hover:underline" "Whereof one cannot speak, thereof one must be silent") ".\" Proposition 7. The things that cannot be said in a language simply do not exist within that language's world.")
(p :class "text-stone-600"
"HTML cannot speak of composition. It is silent on components. You cannot define " (code "~card") " in HTML. You can define " (code "<template>") " and " (code "<slot>") " in Web Components, but that requires JavaScript — you have left HTML's language game and entered another.")
"HTML cannot speak of composition. It is silent on components. You cannot define " (code "~essays/sx-and-wittgenstein/card") " in HTML. You can define " (code "<template>") " and " (code "<slot>") " in Web Components, but that requires JavaScript — you have left HTML's language game and entered another.")
(p :class "text-stone-600"
"CSS cannot speak of conditions. It is silent on logic. You cannot say \"if the user is logged in, use this colour.\" You can use " (code ":has()") " and " (code "@container") " queries, but these are conditions about " (em "the document") ", not conditions about " (em "the application") ". CSS can only speak of what CSS can see.")
(p :class "text-stone-600"
"JavaScript can speak of almost everything — but it speaks in statements, not expressions. The difference matters. A statement executes and is gone. An expression evaluates to a value. Values compose. Statements require sequencing. React discovered this when it moved from class components (imperative, statement-oriented) to hooks (closer to expressions, but not quite — hence the rules of hooks).")
(p :class "text-stone-600"
"S-expressions are pure expression. Every form evaluates to a value. There is nothing that cannot be spoken, because lists can nest arbitrarily and symbols can name anything. There is no proposition 7 for s-expressions — no enforced silence, no boundary where the language gives out. The programmer decides where to draw the line, not the syntax."))
(~doc-section :title "IV. Family resemblance" :id "family-resemblance"
(~docs/section :title "IV. Family resemblance" :id "family-resemblance"
(p :class "text-stone-600"
"Wittgenstein argued that concepts do not have sharp definitions. What is a \"" (a :href "https://en.wikipedia.org/wiki/Family_resemblance" :class "text-violet-600 hover:underline" "game") "\"? Chess, football, solitaire, ring-a-ring-o'-roses — they share no single essential feature. Instead, they form a network of overlapping similarities. A " (a :href "https://en.wikipedia.org/wiki/Family_resemblance" :class "text-violet-600 hover:underline" "family resemblance") ".")
(p :class "text-stone-600"
"Web frameworks have this property. What is a \"component\"? In React, it is a function that returns JSX. In Vue, it is an object with a template property. In Svelte, it is a " (code ".svelte") " file. In Web Components, it is a class that extends HTMLElement. In Angular, it is a TypeScript class with a decorator. These are not the same thing. They share a family resemblance — they all produce reusable UI — but their definitions are incompatible. A React component cannot be used in Vue. A Svelte component cannot be used in Angular. The family does not communicate.")
(p :class "text-stone-600"
"In SX, a component is a list whose first element is a symbol beginning with " (code "~") ". That is the complete definition. It is not a function, not a class, not a file, not a decorator. It is a " (em "naming convention on a data structure") ". Any system that can process lists can process SX components. Python evaluates them on the server. JavaScript evaluates them in the browser. A future Rust evaluator could evaluate them on an embedded device. The family resemblance sharpens into actual identity: a component is a component is a component, because the representation is the same everywhere.")
(~doc-code :lang "lisp" :code
";; This is a component in every SX evaluator:\n(defcomp ~greeting (&key name)\n (div :class \"p-4\"\n (h2 (str \"Hello, \" name))))\n\n;; The same s-expression is:\n;; - parsed by the same parser\n;; - evaluated by the same eval rules\n;; - rendered by the same render spec\n;; - on every host, in every context"))
(~doc-section :title "V. Private language" :id "private-language"
(~docs/code :lang "lisp" :code
";; This is a component in every SX evaluator:\n(defcomp ~essays/sx-and-wittgenstein/greeting (&key name)\n (div :class \"p-4\"\n (h2 (str \"Hello, \" name))))\n\n;; The same s-expression is:\n;; - parsed by the same parser\n;; - evaluated by the same eval rules\n;; - rendered by the same render spec\n;; - on every host, in every context"))
(~docs/section :title "V. Private language" :id "private-language"
(p :class "text-stone-600"
"The " (a :href "https://en.wikipedia.org/wiki/Private_language_argument" :class "text-violet-600 hover:underline" "private language argument") " is one of Wittgenstein's most provocative claims: there can be no language whose words refer to the speaker's private sensations and nothing else. Language requires public criteria — shared rules that others can check. A word that means something only to you is not a word at all.")
(p :class "text-stone-600"
@@ -53,7 +53,7 @@
"S-expressions are radically public. The syntax is universal: open paren, atoms, close paren. Any Lisp, any s-expression processor, any JSON-to-sexp converter can read them. The SX evaluator adds meaning — " (code "defcomp") ", " (code "defmacro") ", " (code "if") ", " (code "let") " — but these meanings are specified in s-expressions themselves (" (code "eval.sx") "), readable by anyone. There is no private knowledge. There is no compilation step that transforms the public syntax into a private intermediate form. The source is the artefact.")
(p :class "text-stone-600"
"This is why SX can be self-hosting. A private language cannot define itself — it would need a second private language to define the first, and a third to define the second. A public language, one whose rules are expressible in its own terms, can close the loop. " (code "eval.sx") " defines SX in SX. The language defines itself publicly, in a form that any reader can inspect."))
(~doc-section :title "VI. Showing and saying" :id "showing-saying"
(~docs/section :title "VI. Showing and saying" :id "showing-saying"
(p :class "text-stone-600"
"The Tractatus makes a crucial distinction between what can be " (em "said") " and what can only be " (em "shown") ". Logic, Wittgenstein argued, cannot be said — it can only be shown by the structure of propositions. You cannot step outside logic to make statements about logic; you can only exhibit logical structure by using it.")
(p :class "text-stone-600"
@@ -62,14 +62,14 @@
"SX " (em "shows") " its semantics. The specification is not a description of the language — it is the language operating on itself. " (code "eval.sx") " does not say \"the evaluator dispatches on the type of expression.\" It " (em "is") " an evaluator that dispatches on the type of expression. " (code "parser.sx") " does not say \"strings are delimited by double quotes.\" It " (em "is") " a parser that recognises double-quoted strings.")
(p :class "text-stone-600"
"This is exactly the distinction Wittgenstein drew. What can be said (described, documented, specified in English) is limited. What can be shown (exhibited, demonstrated, enacted) goes further. SX's self-hosting spec shows its semantics by " (em "being") " them — the strongest form of specification possible. The spec cannot be wrong, because the spec runs."))
(~doc-section :title "VII. The beetle in the box" :id "beetle"
(~docs/section :title "VII. The beetle in the box" :id "beetle"
(p :class "text-stone-600"
"Wittgenstein's " (a :href "https://en.wikipedia.org/wiki/Private_language_argument#The_beetle_in_a_box" :class "text-violet-600 hover:underline" "beetle-in-a-box") " thought experiment: suppose everyone has a box, and everyone calls what's inside their box a \"beetle.\" No one can look in anyone else's box. The word \"beetle\" refers to whatever is in the box — but since no one can check, the contents might be different for each person, or the box might even be empty. The word gets its meaning from the " (em "game") " it plays in public, not from the private contents of the box.")
(p :class "text-stone-600"
"A web component is a beetle in a box. You call it " (code "<my-button>") " but what's inside — Shadow DOM, event listeners, internal state, style encapsulation — is private. Two components with the same tag name might do completely different things. Two frameworks with the same concept of \"component\" might mean completely different things by it. The word \"component\" functions in the language game of developer conversation, but the actual contents are private to each implementation.")
(p :class "text-stone-600"
"In SX, you can open the box. Components are data. " (code "(defcomp ~card (&key title) (div title))") " — the entire definition is visible, inspectable, serializable. There is no shadow DOM, no hidden state machine, no encapsulated runtime. The component's body is an s-expression. You can quote it, transform it, analyse it, send it over HTTP, evaluate it in a different context. The beetle is on the table."))
(~doc-section :title "VIII. The fly-bottle" :id "fly-bottle"
"In SX, you can open the box. Components are data. " (code "(defcomp ~essays/sx-and-wittgenstein/card (&key title) (div title))") " — the entire definition is visible, inspectable, serializable. There is no shadow DOM, no hidden state machine, no encapsulated runtime. The component's body is an s-expression. You can quote it, transform it, analyse it, send it over HTTP, evaluate it in a different context. The beetle is on the table."))
(~docs/section :title "VIII. The fly-bottle" :id "fly-bottle"
(p :class "text-stone-600"
"\"What is your aim in philosophy?\" Wittgenstein was asked. \"" (a :href "https://en.wikipedia.org/wiki/Philosophical_Investigations#Meaning_and_definition" :class "text-violet-600 hover:underline" "To show the fly the way out of the fly-bottle") ".\"")
(p :class "text-stone-600"
@@ -80,7 +80,7 @@
"\"How do we share state between server and client?\" Fly-bottle. This is hard when the server speaks one language (Python templates) and the client speaks another (JavaScript). When both speak s-expressions, and the wire format IS the source syntax, state transfer is serialisation — which is identity for s-expressions. " (code "aser") " serialises an SX expression as an SX expression. The server and client share state by sharing code.")
(p :class "text-stone-600"
"SX does not solve these problems. It dissolves them — by removing the language confusion that created them. Wittgenstein's method, applied to web development: the problems were never real. They were artefacts of speaking in the wrong language."))
(~doc-section :title "IX. Back to rough ground" :id "rough-ground"
(~docs/section :title "IX. Back to rough ground" :id "rough-ground"
(p :class "text-stone-600"
"\"We have got on to slippery ice where there is no friction and so in a certain sense the conditions are ideal, but also, just because of that, we are unable to walk. We want to walk: so we need " (a :href "https://en.wikipedia.org/wiki/Philosophical_Investigations" :class "text-violet-600 hover:underline" "friction") ". Back to the rough ground!\"")
(p :class "text-stone-600"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +1,2 @@
(defcomp ~essay-sx-sucks ()
(~doc-page :title "sx sucks" (~doc-section :title "The parentheses" :id "parens" (p :class "text-stone-600" "S-expressions are parentheses. Lots of parentheses. You thought LISP was dead? No, someone just decided to use it for HTML templates. Your IDE will need a parenthesis counter. Your code reviews will be 40% closing parens. Every merge conflict will be about whether a paren belongs on this line or the next.")) (~doc-section :title "Nobody asked for this" :id "nobody-asked" (p :class "text-stone-600" "The JavaScript ecosystem has React, Vue, Svelte, Solid, Qwik, and approximately 47,000 other frameworks. htmx proved you can skip them all. sx looked at this landscape and said: you know what this needs? A Lisp dialect. For HTML. Over HTTP.") (p :class "text-stone-600" "Nobody was asking for this. The zero GitHub stars confirm it. It is not even on GitHub.")) (~doc-section :title "The author has never written a line of LISP" :id "no-lisp" (p :class "text-stone-600" "The author of sx has never written a single line of actual LISP. Not Common Lisp. Not Scheme. Not Clojure. Not even Emacs Lisp. The entire s-expression evaluator was written by someone whose mental model of LISP comes from reading the first three chapters of SICP and then closing the tab.") (p :class "text-stone-600" "This is like building a sushi restaurant when your only experience with Japanese cuisine is eating supermarket California rolls.")) (~doc-section :title "AI wrote most of it" :id "ai" (p :class "text-stone-600" "A significant portion of sx — the evaluator, the parser, the primitives, the CSS scanner, this very documentation site — was written with AI assistance. The author typed prompts. Claude typed code. This is not artisanal hand-crafted software. This is the software equivalent of a microwave dinner presented on a nice plate.") (p :class "text-stone-600" "He adds features by typing stuff like \"is there rom for macros within sx.js? what benefits m,ight that bring?\", skim-reading the response, and then entering \"crack on then!\" This is not software engineering. This is improv comedy with a compiler.") (p :class "text-stone-600" "Is that bad? Maybe. Is it honest? Yes. Is this paragraph also AI-generated? You will never know.")) (~doc-section :title "No ecosystem" :id "ecosystem" (p :class "text-stone-600" "npm has 2 million packages. PyPI has 500,000. sx has zero packages, zero plugins, zero middleware, zero community, zero Stack Overflow answers, and zero conference talks. If you get stuck, your options are: read the source, or ask the one person who wrote it.") (p :class "text-stone-600" "That person is busy. Good luck.")) (~doc-section :title "Zero jobs" :id "jobs" (p :class "text-stone-600" "Adding sx to your CV will not get you hired. It will get you questioned.") (p :class "text-stone-600" "The interview will end shortly after.")) (~doc-section :title "The creator thinks s-expressions are a personality trait" :id "personality" (p :class "text-stone-600" "Look at this documentation site. It has a violet colour scheme. It has credits to htmx. It has a future possibilities page about hypothetical sx:// protocol schemes. The creator built an entire microservice — with Docker, Redis, and a custom entrypoint script — just to serve documentation about a rendering engine that runs one website.") (p :class "text-stone-600" "This is not engineering. This is a personality disorder expressed in YAML."))))
(defcomp ~essays/sx-sucks/essay-sx-sucks ()
(~docs/page :title "sx sucks" (~docs/section :title "The parentheses" :id "parens" (p :class "text-stone-600" "S-expressions are parentheses. Lots of parentheses. You thought LISP was dead? No, someone just decided to use it for HTML templates. Your IDE will need a parenthesis counter. Your code reviews will be 40% closing parens. Every merge conflict will be about whether a paren belongs on this line or the next.")) (~docs/section :title "Nobody asked for this" :id "nobody-asked" (p :class "text-stone-600" "The JavaScript ecosystem has React, Vue, Svelte, Solid, Qwik, and approximately 47,000 other frameworks. htmx proved you can skip them all. sx looked at this landscape and said: you know what this needs? A Lisp dialect. For HTML. Over HTTP.") (p :class "text-stone-600" "Nobody was asking for this. The zero GitHub stars confirm it. It is not even on GitHub.")) (~docs/section :title "The author has never written a line of LISP" :id "no-lisp" (p :class "text-stone-600" "The author of sx has never written a single line of actual LISP. Not Common Lisp. Not Scheme. Not Clojure. Not even Emacs Lisp. The entire s-expression evaluator was written by someone whose mental model of LISP comes from reading the first three chapters of SICP and then closing the tab.") (p :class "text-stone-600" "This is like building a sushi restaurant when your only experience with Japanese cuisine is eating supermarket California rolls.")) (~docs/section :title "AI wrote most of it" :id "ai" (p :class "text-stone-600" "A significant portion of sx — the evaluator, the parser, the primitives, the CSS scanner, this very documentation site — was written with AI assistance. The author typed prompts. Claude typed code. This is not artisanal hand-crafted software. This is the software equivalent of a microwave dinner presented on a nice plate.") (p :class "text-stone-600" "He adds features by typing stuff like \"is there rom for macros within sx.js? what benefits m,ight that bring?\", skim-reading the response, and then entering \"crack on then!\" This is not software engineering. This is improv comedy with a compiler.") (p :class "text-stone-600" "Is that bad? Maybe. Is it honest? Yes. Is this paragraph also AI-generated? You will never know.")) (~docs/section :title "No ecosystem" :id "ecosystem" (p :class "text-stone-600" "npm has 2 million packages. PyPI has 500,000. sx has zero packages, zero plugins, zero middleware, zero community, zero Stack Overflow answers, and zero conference talks. If you get stuck, your options are: read the source, or ask the one person who wrote it.") (p :class "text-stone-600" "That person is busy. Good luck.")) (~docs/section :title "Zero jobs" :id "jobs" (p :class "text-stone-600" "Adding sx to your CV will not get you hired. It will get you questioned.") (p :class "text-stone-600" "The interview will end shortly after.")) (~docs/section :title "The creator thinks s-expressions are a personality trait" :id "personality" (p :class "text-stone-600" "Look at this documentation site. It has a violet colour scheme. It has credits to htmx. It has a future possibilities page about hypothetical sx:// protocol schemes. The creator built an entire microservice — with Docker, Redis, and a custom entrypoint script — just to serve documentation about a rendering engine that runs one website.") (p :class "text-stone-600" "This is not engineering. This is a personality disorder expressed in YAML."))))

File diff suppressed because one or more lines are too long

View File

@@ -2,12 +2,12 @@
;; The Art Chain
;; ---------------------------------------------------------------------------
(defcomp ~essay-the-art-chain ()
(~doc-page :title "The Art Chain"
(defcomp ~essays/the-art-chain/essay-the-art-chain ()
(~docs/page :title "The Art Chain"
(p :class "text-stone-500 text-sm italic mb-8"
"On making, self-making, and the chain of artifacts that produces itself.")
(~doc-section :title "I. Ars" :id "ars"
(~docs/section :title "I. Ars" :id "ars"
(p :class "text-stone-600"
"The Latin word " (em "ars") " means something made with skill. Not art as in paintings on gallery walls. Art as in " (em "artifice") ", " (em "artifact") ", " (em "artisan") ". The made thing. The Greek " (em "techne") " is the same word — craft, skill, the knowledge of how to make. There was no distinction between art and engineering because there was no distinction to make.")
(p :class "text-stone-600"
@@ -15,16 +15,16 @@
(p :class "text-stone-600"
"Software is " (em "ars") ". Obviously. It is the most " (em "ars") " thing we have ever built — pure made-ness, structure conjured from nothing, shaped entirely by the maker's skill and intent. There is no raw material. No marble to chisel, no pigment to mix. Just thought, made concrete in symbols."))
(~doc-section :title "II. The spec at the centre" :id "spec"
(~docs/section :title "II. The spec at the centre" :id "spec"
(p :class "text-stone-600"
"SX has a peculiar architecture. At its centre sits a specification — a set of s-expression files that define the language. Not a description of the language. Not documentation " (em "about") " the language. The specification " (em "is") " the language. It is simultaneously a formal definition and executable code. You can read it as a document or run it as a program. It does not describe how to build an SX evaluator; it " (em "is") " an SX evaluator, expressed in the language it defines.")
(p :class "text-stone-600"
"This is the nucleus. Everything else radiates outward from it.")
(~doc-code :code (highlight ";; The spec defines eval-expr\n;; eval-expr evaluates the spec\n;; The spec is an artifact that makes itself\n\n(define eval-expr\n (fn (expr env)\n (cond\n (number? expr) expr\n (string? expr) expr\n (symbol? expr) (env-get env (symbol-name expr))\n (list? expr) (eval-list expr env)\n :else expr)))" "lisp"))
(~docs/code :code (highlight ";; The spec defines eval-expr\n;; eval-expr evaluates the spec\n;; The spec is an artifact that makes itself\n\n(define eval-expr\n (fn (expr env)\n (cond\n (number? expr) expr\n (string? expr) expr\n (symbol? expr) (env-get env (symbol-name expr))\n (list? expr) (eval-list expr env)\n :else expr)))" "lisp"))
(p :class "text-stone-600"
"From this nucleus, concentric rings unfurl:"))
(~doc-section :title "III. The rings" :id "rings"
(~docs/section :title "III. The rings" :id "rings"
(p :class "text-stone-600"
"The first ring is the " (strong "bootstrapper") ". It reads the spec and emits a native implementation — JavaScript, Python, or any other target. The bootstrapper is a translator: it takes the made thing (the spec) and makes another thing (an implementation) that behaves identically. The spec's knowledge is preserved in the translation. Nothing is added, nothing is lost.")
(p :class "text-stone-600"
@@ -36,7 +36,7 @@
(p :class "text-stone-600"
"The fifth ring is " (strong "this website") " — which renders the spec's source code using the runtime the spec produced, displayed in components written in the language the spec defines, navigated by an engine the spec specifies. The documentation is the thing documenting itself."))
(~doc-section :title "IV. The chain" :id "chain"
(~docs/section :title "IV. The chain" :id "chain"
(p :class "text-stone-600"
"Each ring is an artifact — a made thing. And each artifact is made " (em "by") " the artifact inside it. The spec makes the bootstrapper's output. The runtime makes the application's output. The application makes the page the user sees. It is a chain of making.")
(p :class "text-stone-600"
@@ -50,7 +50,7 @@
(p :class "text-stone-600"
"These three properties together — content addressing, deterministic derivation, self-verification — are what a blockchain provides. But here there is no proof-of-work, no tokens, no artificial scarcity, no consensus mechanism between untrusted parties. The \"mining\" is bootstrapping. The \"consensus\" is mathematical proof. The \"value\" is that anyone can take the spec, derive an implementation, and " (em "know") " it is correct."))
(~doc-section :title "V. Universal analysis" :id "analysis"
(~docs/section :title "V. Universal analysis" :id "analysis"
(p :class "text-stone-600"
"Here is the consequence that takes time to absorb: any tool that can analyse the spec can analyse " (em "everything the spec produces") ".")
(p :class "text-stone-600"
@@ -60,7 +60,7 @@
(p :class "text-stone-600"
"And the analysis tools are " (em "inside") " the chain. They are artifacts too, written in SX, subject to the same analysis they perform. The type checker can type-check itself. The prover can prove properties about itself. This is not a bug or a curiosity — it is the point. A system that cannot reason about itself is a system that must be reasoned about from outside, by tools written in other languages, maintained by other processes, trusted for other reasons. A self-analysing system closes the loop."))
(~doc-section :title "VI. The art in the chain" :id "art"
(~docs/section :title "VI. The art in the chain" :id "art"
(p :class "text-stone-600"
"So what is the art chain? It is a chain of artifacts — made things — where each link produces the next, the whole chain can verify itself, and the chain's identity is its content.")
(p :class "text-stone-600"

View File

@@ -1,2 +1,2 @@
(defcomp ~essay-why-sexps ()
(~doc-page :title "Why S-Expressions Over HTML Attributes" (~doc-section :title "The problem with HTML attributes" :id "problem" (p :class "text-stone-600" "HTML attributes are strings. You can put anything in a string. htmx puts DSLs in strings — trigger modifiers, swap strategies, CSS selectors. This works but it means you're parsing a language within a language within a language.") (p :class "text-stone-600" "S-expressions are already structured. Keywords are keywords. Lists are lists. Nested expressions nest naturally. There's no need to invent a trigger modifier syntax because the expression language already handles composition.")) (~doc-section :title "Components without a build step" :id "components" (p :class "text-stone-600" "React showed that components are the right abstraction for UI. The price: a build step, a bundler, JSX transpilation. With s-expressions, defcomp is just another form in the language. No transpiler needed. The same source runs on server and client.")) (~doc-section :title "When attributes are better" :id "better" (p :class "text-stone-600" "HTML attributes work in any HTML document. S-expressions need a runtime. If you want progressive enhancement that works with JS disabled, htmx is better. If you want to write HTML by hand in static files, htmx is better. sx only makes sense when you're already rendering server-side and want components."))))
(defcomp ~essays/why-sexps/essay-why-sexps ()
(~docs/page :title "Why S-Expressions Over HTML Attributes" (~docs/section :title "The problem with HTML attributes" :id "problem" (p :class "text-stone-600" "HTML attributes are strings. You can put anything in a string. htmx puts DSLs in strings — trigger modifiers, swap strategies, CSS selectors. This works but it means you're parsing a language within a language within a language.") (p :class "text-stone-600" "S-expressions are already structured. Keywords are keywords. Lists are lists. Nested expressions nest naturally. There's no need to invent a trigger modifier syntax because the expression language already handles composition.")) (~docs/section :title "Components without a build step" :id "components" (p :class "text-stone-600" "React showed that components are the right abstraction for UI. The price: a build step, a bundler, JSX transpilation. With s-expressions, defcomp is just another form in the language. No transpiler needed. The same source runs on server and client.")) (~docs/section :title "When attributes are better" :id "better" (p :class "text-stone-600" "HTML attributes work in any HTML document. S-expressions need a runtime. If you want progressive enhancement that works with JS disabled, htmx is better. If you want to write HTML by hand in static files, htmx is better. sx only makes sense when you're already rendering server-side and want components."))))

View File

@@ -1,9 +1,9 @@
(defcomp ~essay-zero-tooling ()
(~doc-page :title "Tools for Fools"
(defcomp ~essays/zero-tooling/essay-zero-tooling ()
(~docs/page :title "Tools for Fools"
(p :class "text-stone-500 text-sm italic mb-8"
"SX was built without a code editor. No IDE, no manual source edits, no build tools, no linters, no bundlers. The entire codebase — evaluator, renderer, spec, documentation site, test suite — was produced through conversation with an agentic AI. This is what zero-tooling web development looks like.")
(~doc-section :title "No code editor" :id "no-editor"
(~docs/section :title "No code editor" :id "no-editor"
(p :class "text-stone-600"
"This needs to be stated plainly, because it sounds like an exaggeration: " (strong "not a single line of SX source code was written by hand in a code editor") ". Every component definition, every page route, every essay (including this one), every test case, every spec file — all of it was produced through natural-language conversation with Claude Code, an agentic AI that reads, writes, and modifies files on the developer's behalf.")
(p :class "text-stone-600"
@@ -11,7 +11,7 @@
(p :class "text-stone-600"
"This is not a stunt. It is a consequence of two properties converging: a language with trivial syntax that AI can produce flawlessly, and an AI agent capable of understanding and modifying an entire codebase through conversation. Neither property alone would be sufficient. Together, they make the code editor unnecessary."))
(~doc-section :title "The toolchain that wasn't" :id "toolchain"
(~docs/section :title "The toolchain that wasn't" :id "toolchain"
(p :class "text-stone-600"
"A modern web application typically requires a stack of tooling before a single feature can be built. Consider what a React project demands:")
(ul :class "space-y-2 text-stone-600"
@@ -30,7 +30,7 @@
(p :class "text-stone-600"
"SX uses " (strong "none of them") "."))
(~doc-section :title "Why each tool is unnecessary" :id "why-unnecessary"
(~docs/section :title "Why each tool is unnecessary" :id "why-unnecessary"
(p :class "text-stone-600"
"Each tool in the conventional stack exists to solve a problem. SX eliminates the problems themselves, not just the tools.")
@@ -66,7 +66,7 @@
(p :class "text-stone-600"
"Framework CLIs exist because modern frameworks have complex setup — configuration files, directory conventions, build chains, routing configurations. SX has two declarative abstractions: " (code "defcomp") " (a component) and " (code "defpage") " (a route). Write a component in a " (code ".sx") " file, reference it from a " (code "defpage") ", and it is live. There is no scaffolding because there is nothing to scaffold."))
(~doc-section :title "The AI replaces the rest" :id "ai-replaces"
(~docs/section :title "The AI replaces the rest" :id "ai-replaces"
(p :class "text-stone-600"
"Eliminating the build toolchain still leaves the most fundamental tool: the code editor. The text editor is so basic to programming that it is invisible — questioning its necessity sounds absurd. But the traditional editor exists to serve " (em "human") " limitations:")
(ul :class "space-y-2 text-stone-600"
@@ -81,7 +81,7 @@
(p :class "text-stone-600"
"This is not hypothetical. It is how SX was built. The developer's interface is a terminal running Claude Code. The conversation goes: describe what you want, review what the AI produces, approve or redirect. The AI reads the existing code, understands the conventions, writes the new code, edits the navigation, updates the page routes, and verifies consistency. The " (em "conversation") " is the development environment."))
(~doc-section :title "Before enlightenment, write code" :id "before-enlightenment"
(~docs/section :title "Before enlightenment, write code" :id "before-enlightenment"
(p :class "text-stone-600"
"Carson Gross makes an important point in " (a :href "https://htmx.org/essays/yes-and/" :class "text-violet-600 hover:underline" "\"Yes, and...\"") ": you have to have written code in order to effectively read code. The ability to review, critique, and direct is built on the experience of having done the work yourself. You cannot skip the craft and jump straight to the oversight.")
(p :class "text-stone-600"
@@ -99,7 +99,7 @@
(p :class "text-stone-600"
"What pushed him to use agentic AI was when several of the keys on his keyboard stopped working. Too much coding! AI LLMs don't mind typos."))
(~doc-section :title "Why this only works with s-expressions" :id "why-sexps"
(~docs/section :title "Why this only works with s-expressions" :id "why-sexps"
(p :class "text-stone-600"
"This approach would not work with most web technologies. The reason is the " (strong "syntax tax") " — the overhead a language imposes on AI code generation.")
(p :class "text-stone-600"
@@ -111,7 +111,7 @@
(p :class "text-stone-600"
"SX is optimized for the agent, not the typist. This turns out to be the right trade-off when the agent is doing the typing."))
(~doc-section :title "What zero-tooling actually means" :id "what-it-means"
(~docs/section :title "What zero-tooling actually means" :id "what-it-means"
(p :class "text-stone-600"
"Zero-tooling does not mean zero software. The SX evaluator exists. The server exists. The browser runtime exists. These are " (em "the system") ", not tools for building the system. The distinction matters.")
(p :class "text-stone-600"
@@ -121,7 +121,7 @@
(p :class "text-stone-600"
"Add an agentic AI, and you do not even write the " (code ".sx") " files. You describe what you want. The AI writes the files. They run. The workflow is: intent → code → execution, with no intermediate tooling layer and no manual editing step."))
(~doc-section :title "The proof" :id "proof"
(~docs/section :title "The proof" :id "proof"
(p :class "text-stone-600"
"The evidence for zero-tooling development is not a benchmark or a whitepaper. It is this website.")
(p :class "text-stone-600"

View File

@@ -1,48 +1,48 @@
;; Example page defcomps — one per example.
;; Each calls ~example-page-content with static string data.
;; Each calls ~examples/page-content with static string data.
;; Replaces all _example_*_sx() builders in essays.py.
(defcomp ~example-click-to-load ()
(~example-page-content
(defcomp ~examples-content/example-click-to-load ()
(~examples/page-content
:title "Click to Load"
:description "The simplest sx interaction: click a button, fetch content from the server, swap it in."
:demo-description "Click the button to load server-rendered content."
:demo (~click-to-load-demo)
:demo (~examples/click-to-load-demo)
:sx-code "(button\n :sx-get \"/sx/(geography.(hypermedia.(example.(api.click))))\"\n :sx-target \"#click-result\"\n :sx-swap \"innerHTML\"\n \"Load content\")"
:handler-code (handler-source "ex-click")
:comp-placeholder-id "click-comp"
:wire-placeholder-id "click-wire"
:wire-note "The server responds with content-type text/sx. New CSS rules are prepended as a style tag. Clear the component cache to see component definitions included in the wire response."))
(defcomp ~example-form-submission ()
(~example-page-content
(defcomp ~examples-content/example-form-submission ()
(~examples/page-content
:title "Form Submission"
:description "Forms with sx-post submit via AJAX and swap the response into a target."
:demo-description "Enter a name and submit."
:demo (~form-demo)
:demo (~examples/form-demo)
:sx-code "(form\n :sx-post \"/sx/(geography.(hypermedia.(example.(api.form))))\"\n :sx-target \"#form-result\"\n :sx-swap \"innerHTML\"\n (input :type \"text\" :name \"name\")\n (button :type \"submit\" \"Submit\"))"
:handler-code (handler-source "ex-form")
:comp-placeholder-id "form-comp"
:wire-placeholder-id "form-wire"))
(defcomp ~example-polling ()
(~example-page-content
(defcomp ~examples-content/example-polling ()
(~examples/page-content
:title "Polling"
:description "Use sx-trigger with \"every\" to poll the server at regular intervals."
:demo-description "This div polls the server every 2 seconds."
:demo (~polling-demo)
:demo (~examples/polling-demo)
:sx-code "(div\n :sx-get \"/sx/(geography.(hypermedia.(example.(api.poll))))\"\n :sx-trigger \"load, every 2s\"\n :sx-swap \"innerHTML\"\n \"Loading...\")"
:handler-code (handler-source "ex-poll")
:comp-placeholder-id "poll-comp"
:wire-placeholder-id "poll-wire"
:wire-note "Updates every 2 seconds — watch the time and count change."))
(defcomp ~example-delete-row ()
(~example-page-content
(defcomp ~examples-content/example-delete-row ()
(~examples/page-content
:title "Delete Row"
:description "sx-delete with sx-swap \"outerHTML\" and an empty response removes the row from the DOM."
:demo-description "Click delete to remove a row. Uses sx-confirm for confirmation."
:demo (~delete-demo :items (list
:demo (~examples/delete-demo :items (list
(list "1" "Implement dark mode")
(list "2" "Fix login bug")
(list "3" "Write documentation")
@@ -54,113 +54,113 @@
:wire-placeholder-id "delete-wire"
:wire-note "Empty body — outerHTML swap replaces the target element with nothing."))
(defcomp ~example-inline-edit ()
(~example-page-content
(defcomp ~examples-content/example-inline-edit ()
(~examples/page-content
:title "Inline Edit"
:description "Click edit to swap a display view for an edit form. Save swaps back."
:demo-description "Click edit, modify the text, save or cancel."
:demo (~inline-edit-demo)
:sx-code ";; View mode — shows text + edit button\n(~inline-view :value \"some text\")\n\n;; Edit mode — returned by server on click\n(~inline-edit-form :value \"some text\")"
:demo (~examples/inline-edit-demo)
:sx-code ";; View mode — shows text + edit button\n(~examples/inline-view :value \"some text\")\n\n;; Edit mode — returned by server on click\n(~examples/inline-edit-form :value \"some text\")"
:handler-code (str (handler-source "ex-edit-form") "\n\n" (handler-source "ex-edit-save"))
:comp-placeholder-id "edit-comp"
:comp-heading "Components"
:handler-heading "Server handlers"
:wire-placeholder-id "edit-wire"))
(defcomp ~example-oob-swaps ()
(~example-page-content
(defcomp ~examples-content/example-oob-swaps ()
(~examples/page-content
:title "Out-of-Band Swaps"
:description "sx-swap-oob lets a single response update multiple elements anywhere in the DOM."
:demo-description "One request updates both Box A (via sx-target) and Box B (via sx-swap-oob)."
:demo (~oob-demo)
:demo (~examples/oob-demo)
:sx-code ";; Button targets Box A\n(button\n :sx-get \"/sx/(geography.(hypermedia.(example.(api.oob))))\"\n :sx-target \"#oob-box-a\"\n :sx-swap \"innerHTML\"\n \"Update both boxes\")"
:handler-code (handler-source "ex-oob")
:wire-placeholder-id "oob-wire"
:wire-note "The fragment contains both the main content and an OOB element. sx.js splits them: main content goes to sx-target, OOB elements find their targets by ID."))
(defcomp ~example-lazy-loading ()
(~example-page-content
(defcomp ~examples-content/example-lazy-loading ()
(~examples/page-content
:title "Lazy Loading"
:description "Use sx-trigger=\"load\" to fetch content as soon as the element enters the DOM. Great for deferring expensive content below the fold."
:demo-description "Content loads automatically when the page renders."
:demo (~lazy-loading-demo)
:demo (~examples/lazy-loading-demo)
:sx-code "(div\n :sx-get \"/sx/(geography.(hypermedia.(example.(api.lazy))))\"\n :sx-trigger \"load\"\n :sx-swap \"innerHTML\"\n (div :class \"animate-pulse\" \"Loading...\"))"
:handler-code (handler-source "ex-lazy")
:comp-placeholder-id "lazy-comp"
:wire-placeholder-id "lazy-wire"))
(defcomp ~example-infinite-scroll ()
(~example-page-content
(defcomp ~examples-content/example-infinite-scroll ()
(~examples/page-content
:title "Infinite Scroll"
:description "A sentinel element at the bottom uses sx-trigger=\"intersect once\" to load the next page when scrolled into view. Each response appends items and a new sentinel."
:demo-description "Scroll down in the container to load more items (5 pages total)."
:demo (~infinite-scroll-demo)
:demo (~examples/infinite-scroll-demo)
:sx-code "(div :id \"scroll-sentinel\"\n :sx-get \"/sx/(geography.(hypermedia.(example.(api.scroll))))?page=2\"\n :sx-trigger \"intersect once\"\n :sx-target \"#scroll-items\"\n :sx-swap \"beforeend\"\n \"Loading more...\")"
:handler-code (handler-source "ex-scroll")
:comp-placeholder-id "scroll-comp"
:wire-placeholder-id "scroll-wire"))
(defcomp ~example-progress-bar ()
(~example-page-content
(defcomp ~examples-content/example-progress-bar ()
(~examples/page-content
:title "Progress Bar"
:description "Start a server-side job, then poll for progress using sx-trigger=\"load delay:500ms\" on each response. The bar fills up and stops when complete."
:demo-description "Click start to begin a simulated job."
:demo (~progress-bar-demo)
:demo (~examples/progress-bar-demo)
:sx-code ";; Start the job\n(button\n :sx-post \"/sx/(geography.(hypermedia.(example.(api.progress-start))))\"\n :sx-target \"#progress-target\"\n :sx-swap \"innerHTML\")\n\n;; Each response re-polls via sx-trigger=\"load\"\n(div :sx-get \"/sx/(geography.(hypermedia.(example.(api.progress-status))))?job=ID\"\n :sx-trigger \"load delay:500ms\"\n :sx-target \"#progress-target\"\n :sx-swap \"innerHTML\")"
:handler-code (str (handler-source "ex-progress-start") "\n\n" (handler-source "ex-progress-status"))
:comp-placeholder-id "progress-comp"
:wire-placeholder-id "progress-wire"))
(defcomp ~example-active-search ()
(~example-page-content
(defcomp ~examples-content/example-active-search ()
(~examples/page-content
:title "Active Search"
:description "An input with sx-trigger=\"keyup delay:300ms changed\" debounces keystrokes and only fires when the value changes. The server filters a list of programming languages."
:demo-description "Type to search through 20 programming languages."
:demo (~active-search-demo)
:demo (~examples/active-search-demo)
:sx-code "(input :type \"text\" :name \"q\"\n :sx-get \"/sx/(geography.(hypermedia.(example.(api.search))))\"\n :sx-trigger \"keyup delay:300ms changed\"\n :sx-target \"#search-results\"\n :sx-swap \"innerHTML\"\n :placeholder \"Search...\")"
:handler-code (handler-source "ex-search")
:comp-placeholder-id "search-comp"
:wire-placeholder-id "search-wire"))
(defcomp ~example-inline-validation ()
(~example-page-content
(defcomp ~examples-content/example-inline-validation ()
(~examples/page-content
:title "Inline Validation"
:description "Validate an email field on blur. The server checks format and whether it is taken, returning green or red feedback inline."
:demo-description "Enter an email and click away (blur) to validate."
:demo (~inline-validation-demo)
:demo (~examples/inline-validation-demo)
:sx-code "(input :type \"text\" :name \"email\"\n :sx-get \"/sx/(geography.(hypermedia.(example.(api.validate))))\"\n :sx-trigger \"blur\"\n :sx-target \"#email-feedback\"\n :sx-swap \"innerHTML\"\n :placeholder \"user@example.com\")"
:handler-code (handler-source "ex-validate")
:comp-placeholder-id "validate-comp"
:wire-placeholder-id "validate-wire"))
(defcomp ~example-value-select ()
(~example-page-content
(defcomp ~examples-content/example-value-select ()
(~examples/page-content
:title "Value Select"
:description "Two linked selects: pick a category and the second select updates with matching items via sx-get."
:demo-description "Select a category to populate the item dropdown."
:demo (~value-select-demo)
:demo (~examples/value-select-demo)
:sx-code "(select :name \"category\"\n :sx-get \"/sx/(geography.(hypermedia.(example.(api.values))))\"\n :sx-trigger \"change\"\n :sx-target \"#value-items\"\n :sx-swap \"innerHTML\"\n (option \"Languages\")\n (option \"Frameworks\")\n (option \"Databases\"))"
:handler-code (handler-source "ex-values")
:comp-placeholder-id "values-comp"
:wire-placeholder-id "values-wire"))
(defcomp ~example-reset-on-submit ()
(~example-page-content
(defcomp ~examples-content/example-reset-on-submit ()
(~examples/page-content
:title "Reset on Submit"
:description "Use sx-on:afterSwap=\"this.reset()\" to clear form inputs after a successful submission."
:demo-description "Submit a message — the input resets after each send."
:demo (~reset-on-submit-demo)
:demo (~examples/reset-on-submit-demo)
:sx-code "(form :id \"reset-form\"\n :sx-post \"/sx/(geography.(hypermedia.(example.(api.reset-submit))))\"\n :sx-target \"#reset-result\"\n :sx-swap \"innerHTML\"\n :sx-on:afterSwap \"this.reset()\"\n (input :type \"text\" :name \"message\")\n (button :type \"submit\" \"Send\"))"
:handler-code (handler-source "ex-reset-submit")
:comp-placeholder-id "reset-comp"
:wire-placeholder-id "reset-wire"))
(defcomp ~example-edit-row ()
(~example-page-content
(defcomp ~examples-content/example-edit-row ()
(~examples/page-content
:title "Edit Row"
:description "Click edit to replace a table row with input fields. Save or cancel swaps back the display row. Uses sx-include to gather form values from the row."
:demo-description "Click edit on any row to modify it inline."
:demo (~edit-row-demo :rows (list
:demo (~examples/edit-row-demo :rows (list
(list "1" "Widget A" "19.99" "142")
(list "2" "Widget B" "24.50" "89")
(list "3" "Widget C" "12.00" "305")
@@ -170,12 +170,12 @@
:comp-placeholder-id "editrow-comp"
:wire-placeholder-id "editrow-wire"))
(defcomp ~example-bulk-update ()
(~example-page-content
(defcomp ~examples-content/example-bulk-update ()
(~examples/page-content
:title "Bulk Update"
:description "Select rows with checkboxes and use Activate/Deactivate buttons. sx-include gathers checkbox values from the form."
:demo-description "Check some rows, then click Activate or Deactivate."
:demo (~bulk-update-demo :users (list
:demo (~examples/bulk-update-demo :users (list
(list "1" "Alice Chen" "alice@example.com" "active")
(list "2" "Bob Rivera" "bob@example.com" "inactive")
(list "3" "Carol Zhang" "carol@example.com" "active")
@@ -186,130 +186,130 @@
:comp-placeholder-id "bulk-comp"
:wire-placeholder-id "bulk-wire"))
(defcomp ~example-swap-positions ()
(~example-page-content
(defcomp ~examples-content/example-swap-positions ()
(~examples/page-content
:title "Swap Positions"
:description "Demonstrates different swap modes: beforeend appends, afterbegin prepends, and none skips the main swap while still processing OOB updates."
:demo-description "Try each button to see different swap behaviours."
:demo (~swap-positions-demo)
:demo (~examples/swap-positions-demo)
:sx-code ";; Append to end\n(button :sx-post \"/sx/(geography.(hypermedia.(example.(api.swap-log))))?mode=beforeend\"\n :sx-target \"#swap-log\" :sx-swap \"beforeend\"\n \"Add to End\")\n\n;; Prepend to start\n(button :sx-post \"/sx/(geography.(hypermedia.(example.(api.swap-log))))?mode=afterbegin\"\n :sx-target \"#swap-log\" :sx-swap \"afterbegin\"\n \"Add to Start\")\n\n;; No swap — OOB counter update only\n(button :sx-post \"/sx/(geography.(hypermedia.(example.(api.swap-log))))?mode=none\"\n :sx-target \"#swap-log\" :sx-swap \"none\"\n \"Silent Ping\")"
:handler-code (handler-source "ex-swap-log")
:wire-placeholder-id "swap-wire"))
(defcomp ~example-select-filter ()
(~example-page-content
(defcomp ~examples-content/example-select-filter ()
(~examples/page-content
:title "Select Filter"
:description "sx-select lets the client pick a specific section from the server response by CSS selector. The server always returns the full dashboard — the client filters."
:demo-description "Different buttons select different parts of the same server response."
:demo (~select-filter-demo)
:demo (~examples/select-filter-demo)
:sx-code ";; Pick just the stats section from the response\n(button\n :sx-get \"/sx/(geography.(hypermedia.(example.(api.dashboard))))\"\n :sx-target \"#filter-target\"\n :sx-swap \"innerHTML\"\n :sx-select \"#dash-stats\"\n \"Stats Only\")\n\n;; No sx-select — get the full response\n(button\n :sx-get \"/sx/(geography.(hypermedia.(example.(api.dashboard))))\"\n :sx-target \"#filter-target\"\n :sx-swap \"innerHTML\"\n \"Full Dashboard\")"
:handler-code (handler-source "ex-dashboard")
:wire-placeholder-id "filter-wire"))
(defcomp ~example-tabs ()
(~example-page-content
(defcomp ~examples-content/example-tabs ()
(~examples/page-content
:title "Tabs"
:description "Tab navigation using sx-push-url to update the browser URL. Back/forward buttons navigate between previously visited tabs."
:demo-description "Click tabs to switch content. Watch the browser URL change."
:demo (~tabs-demo)
:demo (~examples/tabs-demo)
:sx-code "(button\n :sx-get \"/sx/(geography.(hypermedia.(example.(api.(tabs.tab1)))))\"\n :sx-target \"#tab-content\"\n :sx-swap \"innerHTML\"\n :sx-push-url \"/sx/(geography.(hypermedia.(example.tabs)))?tab=tab1\"\n \"Overview\")"
:handler-code (handler-source "ex-tabs")
:wire-placeholder-id "tabs-wire"))
(defcomp ~example-animations ()
(~example-page-content
(defcomp ~examples-content/example-animations ()
(~examples/page-content
:title "Animations"
:description "CSS animations play on swap. The component injects a style tag with a keyframe animation and applies the class. Each click picks a random background colour."
:demo-description "Click to swap in content with a fade-in animation."
:demo (~animations-demo)
:sx-code "(button\n :sx-get \"/sx/(geography.(hypermedia.(example.(api.animate))))\"\n :sx-target \"#anim-target\"\n :sx-swap \"innerHTML\"\n \"Load with animation\")\n\n;; Component uses CSS animation class\n(defcomp ~anim-result (&key color time)\n (div :class \"sx-fade-in ...\"\n (style \".sx-fade-in { animation: sxFadeIn 0.5s }\")\n (p \"Faded in!\")))"
:demo (~examples/animations-demo)
:sx-code "(button\n :sx-get \"/sx/(geography.(hypermedia.(example.(api.animate))))\"\n :sx-target \"#anim-target\"\n :sx-swap \"innerHTML\"\n \"Load with animation\")\n\n;; Component uses CSS animation class\n(defcomp ~examples-content/anim-result (&key color time)\n (div :class \"sx-fade-in ...\"\n (style \".sx-fade-in { animation: sxFadeIn 0.5s }\")\n (p \"Faded in!\")))"
:handler-code (handler-source "ex-animate")
:comp-placeholder-id "anim-comp"
:wire-placeholder-id "anim-wire"))
(defcomp ~example-dialogs ()
(~example-page-content
(defcomp ~examples-content/example-dialogs ()
(~examples/page-content
:title "Dialogs"
:description "Open a modal dialog by swapping in the dialog component. Close by swapping in empty content. Pure sx — no JavaScript library needed."
:demo-description "Click to open a modal dialog."
:demo (~dialogs-demo)
:demo (~examples/dialogs-demo)
:sx-code "(button\n :sx-get \"/sx/(geography.(hypermedia.(example.(api.dialog))))\"\n :sx-target \"#dialog-container\"\n :sx-swap \"innerHTML\"\n \"Open Dialog\")\n\n;; Dialog closes by swapping empty content\n(button\n :sx-get \"/sx/(geography.(hypermedia.(example.(api.dialog-close))))\"\n :sx-target \"#dialog-container\"\n :sx-swap \"innerHTML\"\n \"Close\")"
:handler-code (str (handler-source "ex-dialog") "\n\n" (handler-source "ex-dialog-close"))
:comp-placeholder-id "dialog-comp"
:wire-placeholder-id "dialog-wire"))
(defcomp ~example-keyboard-shortcuts ()
(~example-page-content
(defcomp ~examples-content/example-keyboard-shortcuts ()
(~examples/page-content
:title "Keyboard Shortcuts"
:description "Use sx-trigger with keyup event filters and from:body to listen for global keyboard shortcuts. The filter prevents firing when typing in inputs."
:demo-description "Press s, n, or h on your keyboard."
:demo (~keyboard-shortcuts-demo)
:demo (~examples/keyboard-shortcuts-demo)
:sx-code "(div :id \"kbd-target\"\n :sx-get \"/sx/(geography.(hypermedia.(example.(api.keyboard))))?key=s\"\n :sx-trigger \"keyup[key=='s'&&!event.target.matches('input,textarea')] from:body\"\n :sx-swap \"innerHTML\"\n \"Press a shortcut key...\")"
:handler-code (handler-source "ex-keyboard")
:comp-placeholder-id "kbd-comp"
:wire-placeholder-id "kbd-wire"))
(defcomp ~example-put-patch ()
(~example-page-content
(defcomp ~examples-content/example-put-patch ()
(~examples/page-content
:title "PUT / PATCH"
:description "sx-put replaces the entire resource. This example shows a profile card with an Edit All button that sends a PUT with all fields."
:demo-description "Click Edit All to replace the full profile via PUT."
:demo (~put-patch-demo :name "Ada Lovelace" :email "ada@example.com" :role "Engineer")
:demo (~examples/put-patch-demo :name "Ada Lovelace" :email "ada@example.com" :role "Engineer")
:sx-code ";; Replace entire resource\n(form :sx-put \"/sx/(geography.(hypermedia.(example.(api.putpatch))))\"\n :sx-target \"#pp-target\" :sx-swap \"innerHTML\"\n (input :name \"name\") (input :name \"email\")\n (button \"Save All (PUT)\"))"
:handler-code (str (handler-source "ex-pp-edit-all") "\n\n" (handler-source "ex-pp-put"))
:comp-placeholder-id "pp-comp"
:wire-placeholder-id "pp-wire"))
(defcomp ~example-json-encoding ()
(~example-page-content
(defcomp ~examples-content/example-json-encoding ()
(~examples/page-content
:title "JSON Encoding"
:description "Use sx-encoding=\"json\" to send form data as a JSON body instead of URL-encoded form data. The server echoes back what it received."
:demo-description "Submit the form and see the JSON body the server received."
:demo (~json-encoding-demo)
:demo (~examples/json-encoding-demo)
:sx-code "(form\n :sx-post \"/sx/(geography.(hypermedia.(example.(api.json-echo))))\"\n :sx-target \"#json-result\"\n :sx-swap \"innerHTML\"\n :sx-encoding \"json\"\n (input :name \"name\" :value \"Ada\")\n (input :type \"number\" :name \"age\" :value \"36\")\n (button \"Submit as JSON\"))"
:handler-code (handler-source "ex-json-echo")
:comp-placeholder-id "json-comp"
:wire-placeholder-id "json-wire"))
(defcomp ~example-vals-and-headers ()
(~example-page-content
(defcomp ~examples-content/example-vals-and-headers ()
(~examples/page-content
:title "Vals & Headers"
:description "sx-vals adds extra key/value pairs to the request parameters. sx-headers adds custom HTTP headers. The server echoes back what it received."
:demo-description "Click each button to see what the server receives."
:demo (~vals-headers-demo)
:demo (~examples/vals-headers-demo)
:sx-code ";; Send extra values with the request\n(button\n :sx-get \"/sx/(geography.(hypermedia.(example.(api.echo-vals))))\"\n :sx-vals \"{\\\"source\\\": \\\"button\\\"}\"\n \"Send with vals\")\n\n;; Send custom headers\n(button\n :sx-get \"/sx/(geography.(hypermedia.(example.(api.echo-headers))))\"\n :sx-headers {:X-Custom-Token \"abc123\"}\n \"Send with headers\")"
:handler-code (str (handler-source "ex-echo-vals") "\n\n" (handler-source "ex-echo-headers"))
:comp-placeholder-id "vals-comp"
:wire-placeholder-id "vals-wire"))
(defcomp ~example-loading-states ()
(~example-page-content
(defcomp ~examples-content/example-loading-states ()
(~examples/page-content
:title "Loading States"
:description "sx.js adds the .sx-request CSS class to any element that has an active request. Use pure CSS to show spinners, disable buttons, or change opacity during loading."
:demo-description "Click the button — it shows a spinner during the 2-second request."
:demo (~loading-states-demo)
:demo (~examples/loading-states-demo)
:sx-code ";; .sx-request class added during request\n(style \".sx-loading-btn.sx-request {\n opacity: 0.7; pointer-events: none; }\n.sx-loading-btn.sx-request .sx-spinner {\n display: inline-block; }\n.sx-loading-btn .sx-spinner {\n display: none; }\")\n\n(button :class \"sx-loading-btn\"\n :sx-get \"/sx/(geography.(hypermedia.(example.(api.slow))))\"\n :sx-target \"#loading-result\"\n (span :class \"sx-spinner animate-spin\" \"...\")\n \"Load slow endpoint\")"
:handler-code (handler-source "ex-slow")
:comp-placeholder-id "loading-comp"
:wire-placeholder-id "loading-wire"))
(defcomp ~example-sync-replace ()
(~example-page-content
(defcomp ~examples-content/example-sync-replace ()
(~examples/page-content
:title "Request Abort"
:description "sx-sync=\"replace\" aborts any in-flight request before sending a new one. This prevents stale responses from overwriting newer ones, even with random server delays."
:demo-description "Type quickly — only the latest result appears despite random 0.5-2s server delays."
:demo (~sync-replace-demo)
:demo (~examples/sync-replace-demo)
:sx-code "(input :type \"text\" :name \"q\"\n :sx-get \"/sx/(geography.(hypermedia.(example.(api.slow-search))))\"\n :sx-trigger \"keyup delay:200ms changed\"\n :sx-target \"#sync-result\"\n :sx-swap \"innerHTML\"\n :sx-sync \"replace\"\n \"Type to search...\")"
:handler-code (handler-source "ex-slow-search")
:comp-placeholder-id "sync-comp"
:wire-placeholder-id "sync-wire"))
(defcomp ~example-retry ()
(~example-page-content
(defcomp ~examples-content/example-retry ()
(~examples/page-content
:title "Retry"
:description "sx-retry=\"exponential:1000:8000\" retries failed requests with exponential backoff starting at 1s up to 8s. The endpoint fails the first 2 attempts and succeeds on the 3rd."
:demo-description "Click the button — watch it retry automatically after failures."
:demo (~retry-demo)
:demo (~examples/retry-demo)
:sx-code "(button\n :sx-get \"/sx/(geography.(hypermedia.(example.(api.flaky))))\"\n :sx-target \"#retry-result\"\n :sx-swap \"innerHTML\"\n :sx-retry \"exponential:1000:8000\"\n \"Call flaky endpoint\")"
:handler-code (handler-source "ex-flaky")
:comp-placeholder-id "retry-comp"

View File

@@ -1,34 +1,34 @@
;; Example page template and reference index
;; Template receives data values (code strings, titles), calls highlight internally.
(defcomp ~example-page-content (&key (title :as string) (description :as string) (demo-description :as string?) demo
(defcomp ~examples/page-content (&key (title :as string) (description :as string) (demo-description :as string?) demo
(sx-code :as string) (sx-lang :as string?) (handler-code :as string) (handler-lang :as string?)
(comp-placeholder-id :as string?) (wire-placeholder-id :as string?) (wire-note :as string?)
(comp-heading :as string?) (handler-heading :as string?))
(~doc-page :title title
(~docs/page :title title
(p :class "text-stone-600 mb-6" description)
(~example-card :title "Demo" :description demo-description
(~example-demo demo))
(~examples/card :title "Demo" :description demo-description
(~examples/demo demo))
(h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")
(~example-source :code (highlight sx-code (if sx-lang sx-lang "lisp")))
(~examples/source :code (highlight sx-code (if sx-lang sx-lang "lisp")))
(when comp-placeholder-id
(<>
(h3 :class "text-lg font-semibold text-stone-700 mt-6"
(if comp-heading comp-heading "Component"))
(~doc-placeholder :id comp-placeholder-id)))
(~docs/placeholder :id comp-placeholder-id)))
(h3 :class "text-lg font-semibold text-stone-700 mt-6"
(if handler-heading handler-heading "Server handler"))
(~example-source :code (highlight handler-code (if handler-lang handler-lang "python")))
(~examples/source :code (highlight handler-code (if handler-lang handler-lang "python")))
(div :class "flex items-center justify-between mt-6"
(h3 :class "text-lg font-semibold text-stone-700" "Wire response")
(~doc-clear-cache-btn))
(~docs/clear-cache-btn))
(when wire-note
(p :class "text-stone-500 text-sm mb-2" wire-note))
(when wire-placeholder-id
(~doc-placeholder :id wire-placeholder-id))))
(~docs/placeholder :id wire-placeholder-id))))
(defcomp ~reference-index-content ()
(~doc-page :title "Reference"
(defcomp ~examples/reference-index-content ()
(~docs/page :title "Reference"
(p :class "text-stone-600 mb-6"
"Complete reference for the sx client library.")
(div :class "grid gap-4 sm:grid-cols-2"

View File

@@ -61,11 +61,11 @@
(&key)
(let ((now (now "%Y-%m-%d %H:%M:%S")))
(<>
(~click-result :time now)
(~doc-oob-code :target-id "click-comp"
:text (component-source "~click-result"))
(~doc-oob-code :target-id "click-wire"
:text (str "(~click-result :time \"" now "\")")))))
(~examples/click-result :time now)
(~docs/oob-code :target-id "click-comp"
:text (component-source "~examples/click-result"))
(~docs/oob-code :target-id "click-wire"
:text (str "(~examples/click-result :time \"" now "\")")))))
;; --------------------------------------------------------------------------
@@ -80,11 +80,11 @@
(&key)
(let ((name (request-form "name" "")))
(<>
(~form-result :name name)
(~doc-oob-code :target-id "form-comp"
:text (component-source "~form-result"))
(~doc-oob-code :target-id "form-wire"
:text (str "(~form-result :name \"" name "\")")))))
(~examples/form-result :name name)
(~docs/oob-code :target-id "form-comp"
:text (component-source "~examples/form-result"))
(~docs/oob-code :target-id "form-wire"
:text (str "(~examples/form-result :name \"" name "\")")))))
;; --------------------------------------------------------------------------
@@ -101,11 +101,11 @@
(let ((now (now "%H:%M:%S"))
(count (if (< n 10) n 10)))
(<>
(~poll-result :time now :count count)
(~doc-oob-code :target-id "poll-comp"
:text (component-source "~poll-result"))
(~doc-oob-code :target-id "poll-wire"
:text (str "(~poll-result :time \"" now "\" :count " count ")"))))))
(~examples/poll-result :time now :count count)
(~docs/oob-code :target-id "poll-comp"
:text (component-source "~examples/poll-result"))
(~docs/oob-code :target-id "poll-wire"
:text (str "(~examples/poll-result :time \"" now "\" :count " count ")"))))))
;; --------------------------------------------------------------------------
@@ -119,9 +119,9 @@
:returns "element"
(&key item-id)
(<>
(~doc-oob-code :target-id "delete-comp"
:text (component-source "~delete-row"))
(~doc-oob-code :target-id "delete-wire"
(~docs/oob-code :target-id "delete-comp"
:text (component-source "~examples/delete-row"))
(~docs/oob-code :target-id "delete-wire"
:text "(empty — row removed by outerHTML swap)")))
@@ -136,11 +136,11 @@
(&key)
(let ((value (request-arg "value" "")))
(<>
(~inline-edit-form :value value)
(~doc-oob-code :target-id "edit-comp"
:text (component-source "~inline-edit-form"))
(~doc-oob-code :target-id "edit-wire"
:text (str "(~inline-edit-form :value \"" value "\")")))))
(~examples/inline-edit-form :value value)
(~docs/oob-code :target-id "edit-comp"
:text (component-source "~examples/inline-edit-form"))
(~docs/oob-code :target-id "edit-wire"
:text (str "(~examples/inline-edit-form :value \"" value "\")")))))
(defhandler ex-edit-save
:path "/sx/(geography.(hypermedia.(example.(api.edit))))"
@@ -150,11 +150,11 @@
(&key)
(let ((value (request-form "value" "")))
(<>
(~inline-view :value value)
(~doc-oob-code :target-id "edit-comp"
:text (component-source "~inline-view"))
(~doc-oob-code :target-id "edit-wire"
:text (str "(~inline-view :value \"" value "\")")))))
(~examples/inline-view :value value)
(~docs/oob-code :target-id "edit-comp"
:text (component-source "~examples/inline-view"))
(~docs/oob-code :target-id "edit-wire"
:text (str "(~examples/inline-view :value \"" value "\")")))))
(defhandler ex-edit-cancel
:path "/sx/(geography.(hypermedia.(example.(api.edit-cancel))))"
@@ -163,11 +163,11 @@
(&key)
(let ((value (request-arg "value" "")))
(<>
(~inline-view :value value)
(~doc-oob-code :target-id "edit-comp"
:text (component-source "~inline-view"))
(~doc-oob-code :target-id "edit-wire"
:text (str "(~inline-view :value \"" value "\")")))))
(~examples/inline-view :value value)
(~docs/oob-code :target-id "edit-comp"
:text (component-source "~examples/inline-view"))
(~docs/oob-code :target-id "edit-wire"
:text (str "(~examples/inline-view :value \"" value "\")")))))
;; --------------------------------------------------------------------------
@@ -186,7 +186,7 @@
(div :id "oob-box-b" :sx-swap-oob "innerHTML"
(p :class "text-violet-600 font-medium" "Box B updated via OOB!")
(p :class "text-sm text-stone-500" (str "at " now)))
(~doc-oob-code :target-id "oob-wire"
(~docs/oob-code :target-id "oob-wire"
:text (str "(<> (p ... \"Box A updated!\") (div :id \"oob-box-b\" :sx-swap-oob \"innerHTML\" (p ... \"Box B updated!\")))")))))
@@ -201,11 +201,11 @@
(&key)
(let ((now (now "%H:%M:%S")))
(<>
(~lazy-result :time now)
(~doc-oob-code :target-id "lazy-comp"
:text (component-source "~lazy-result"))
(~doc-oob-code :target-id "lazy-wire"
:text (str "(~lazy-result :time \"" now "\")")))))
(~examples/lazy-result :time now)
(~docs/oob-code :target-id "lazy-comp"
:text (component-source "~examples/lazy-result"))
(~docs/oob-code :target-id "lazy-wire"
:text (str "(~examples/lazy-result :time \"" now "\")")))))
;; --------------------------------------------------------------------------
@@ -235,7 +235,7 @@
"Loading more...")
(div :class "p-3 text-center text-stone-500 text-sm font-medium"
"All items loaded."))
(~doc-oob-code :target-id "scroll-wire"
(~docs/oob-code :target-id "scroll-wire"
:text (str "(items for page " page " + sentinel)"))))))
@@ -254,11 +254,11 @@
(let ((job-id (str "job-" n)))
(state-set! (str "ex-job-" job-id) 0)
(<>
(~progress-status :percent 0 :job-id job-id)
(~doc-oob-code :target-id "progress-comp"
:text (component-source "~progress-status"))
(~doc-oob-code :target-id "progress-wire"
:text (str "(~progress-status :percent 0 :job-id \"" job-id "\")"))))))
(~examples/progress-status :percent 0 :job-id job-id)
(~docs/oob-code :target-id "progress-comp"
:text (component-source "~examples/progress-status"))
(~docs/oob-code :target-id "progress-wire"
:text (str "(~examples/progress-status :percent 0 :job-id \"" job-id "\")"))))))
(defhandler ex-progress-status
:path "/sx/(geography.(hypermedia.(example.(api.progress-status))))"
@@ -270,11 +270,11 @@
(let ((next (if (>= (+ current (random-int 15 30)) 100) 100 (+ current (random-int 15 30)))))
(state-set! (str "ex-job-" job-id) next)
(<>
(~progress-status :percent next :job-id job-id)
(~doc-oob-code :target-id "progress-comp"
:text (component-source "~progress-status"))
(~doc-oob-code :target-id "progress-wire"
:text (str "(~progress-status :percent " next " :job-id \"" job-id "\")")))))))
(~examples/progress-status :percent next :job-id job-id)
(~docs/oob-code :target-id "progress-comp"
:text (component-source "~examples/progress-status"))
(~docs/oob-code :target-id "progress-wire"
:text (str "(~examples/progress-status :percent " next " :job-id \"" job-id "\")")))))))
;; --------------------------------------------------------------------------
@@ -293,9 +293,9 @@
search-languages))))
(<>
(~search-results :items results :query q)
(~doc-oob-code :target-id "search-comp"
(~docs/oob-code :target-id "search-comp"
:text (component-source "~search-results"))
(~doc-oob-code :target-id "search-wire"
(~docs/oob-code :target-id "search-wire"
:text (str "(~search-results :items (list ...) :query \"" q "\")"))))))
@@ -312,22 +312,22 @@
(let ((result
(cond
(= email "")
(list "validation-error" "(~validation-error :message \"Email is required\")"
(~validation-error :message "Email is required"))
(list "validation-error" "(~examples/validation-error :message \"Email is required\")"
(~examples/validation-error :message "Email is required"))
(not (contains? email "@"))
(list "validation-error" "(~validation-error :message \"Invalid email format\")"
(~validation-error :message "Invalid email format"))
(list "validation-error" "(~examples/validation-error :message \"Invalid email format\")"
(~examples/validation-error :message "Invalid email format"))
(some (fn (e) (= (lower-case e) (lower-case email))) taken-emails)
(list "validation-error" (str "(~validation-error :message \"" email " is already taken\")")
(~validation-error :message (str email " is already taken")))
(list "validation-error" (str "(~examples/validation-error :message \"" email " is already taken\")")
(~examples/validation-error :message (str email " is already taken")))
:else
(list "validation-ok" (str "(~validation-ok :email \"" email "\")")
(~validation-ok :email email)))))
(list "validation-ok" (str "(~examples/validation-ok :email \"" email "\")")
(~examples/validation-ok :email email)))))
(<>
(nth result 2)
(~doc-oob-code :target-id "validate-comp"
(~docs/oob-code :target-id "validate-comp"
:text (component-source (first result)))
(~doc-oob-code :target-id "validate-wire"
(~docs/oob-code :target-id "validate-wire"
:text (nth result 1))))))
(defhandler ex-validate-submit
@@ -358,7 +358,7 @@
(map (fn (i) (option :value i i)) items))))
(<>
options
(~doc-oob-code :target-id "values-wire"
(~docs/oob-code :target-id "values-wire"
:text (str "(options for \"" cat "\")")))))))
@@ -375,11 +375,11 @@
(let ((msg (request-form "message" "(empty)"))
(now (now "%H:%M:%S")))
(<>
(~reset-message :message msg :time now)
(~doc-oob-code :target-id "reset-comp"
:text (component-source "~reset-message"))
(~doc-oob-code :target-id "reset-wire"
:text (str "(~reset-message :message \"" msg "\" :time \"" now "\")")))))
(~examples/reset-message :message msg :time now)
(~docs/oob-code :target-id "reset-comp"
:text (component-source "~examples/reset-message"))
(~docs/oob-code :target-id "reset-wire"
:text (str "(~examples/reset-message :message \"" msg "\" :time \"" now "\")")))))
;; --------------------------------------------------------------------------
@@ -394,12 +394,12 @@
(let ((default (get edit-row-defaults row-id {"id" row-id "name" "" "price" "0" "stock" "0"})))
(let ((row (state-get (str "ex-row-" row-id) default)))
(<>
(~edit-row-form :id (get row "id") :name (get row "name")
(~examples/edit-row-form :id (get row "id") :name (get row "name")
:price (get row "price") :stock (get row "stock"))
(~doc-oob-code :target-id "editrow-comp"
:text (component-source "~edit-row-form"))
(~doc-oob-code :target-id "editrow-wire"
:text (str "(~edit-row-form :id \"" (get row "id") "\" ...)"))))))
(~docs/oob-code :target-id "editrow-comp"
:text (component-source "~examples/edit-row-form"))
(~docs/oob-code :target-id "editrow-wire"
:text (str "(~examples/edit-row-form :id \"" (get row "id") "\" ...)"))))))
(defhandler ex-editrow-save
:path "/sx/(geography.(hypermedia.(example.(api.(editrow.<sx:row_id>)))))"
@@ -413,11 +413,11 @@
(state-set! (str "ex-row-" row-id)
{"id" row-id "name" name "price" price "stock" stock})
(<>
(~edit-row-view :id row-id :name name :price price :stock stock)
(~doc-oob-code :target-id "editrow-comp"
:text (component-source "~edit-row-view"))
(~doc-oob-code :target-id "editrow-wire"
:text (str "(~edit-row-view :id \"" row-id "\" ...)")))))
(~examples/edit-row-view :id row-id :name name :price price :stock stock)
(~docs/oob-code :target-id "editrow-comp"
:text (component-source "~examples/edit-row-view"))
(~docs/oob-code :target-id "editrow-wire"
:text (str "(~examples/edit-row-view :id \"" row-id "\" ...)")))))
(defhandler ex-editrow-cancel
:path "/sx/(geography.(hypermedia.(example.(api.(editrow-cancel.<sx:row_id>)))))"
@@ -427,12 +427,12 @@
(let ((default (get edit-row-defaults row-id {"id" row-id "name" "" "price" "0" "stock" "0"})))
(let ((row (state-get (str "ex-row-" row-id) default)))
(<>
(~edit-row-view :id (get row "id") :name (get row "name")
(~examples/edit-row-view :id (get row "id") :name (get row "name")
:price (get row "price") :stock (get row "stock"))
(~doc-oob-code :target-id "editrow-comp"
:text (component-source "~edit-row-view"))
(~doc-oob-code :target-id "editrow-wire"
:text (str "(~edit-row-view :id \"" (get row "id") "\" ...)"))))))
(~docs/oob-code :target-id "editrow-comp"
:text (component-source "~examples/edit-row-view"))
(~docs/oob-code :target-id "editrow-wire"
:text (str "(~examples/edit-row-view :id \"" (get row "id") "\" ...)"))))))
;; --------------------------------------------------------------------------
;; Bulk Update
@@ -460,14 +460,14 @@
(let ((default (get bulk-user-defaults uid
{"id" uid "name" "" "email" "" "status" "active"})))
(let ((u (state-get (str "ex-bulk-" uid) default)))
(~bulk-row :id (get u "id") :name (get u "name")
(~examples/bulk-row :id (get u "id") :name (get u "name")
:email (get u "email") :status (get u "status")))))
(list "1" "2" "3" "4" "5"))))
(<>
rows
(~doc-oob-code :target-id "bulk-comp"
:text (component-source "~bulk-row"))
(~doc-oob-code :target-id "bulk-wire"
(~docs/oob-code :target-id "bulk-comp"
:text (component-source "~examples/bulk-row"))
(~docs/oob-code :target-id "bulk-wire"
:text (str "(updated " (len ids) " users to " new-status ")")))))))
@@ -491,7 +491,7 @@
(span :id "swap-counter" :sx-swap-oob "innerHTML"
:class "self-center text-sm text-stone-500"
(str "Count: " n))
(~doc-oob-code :target-id "swap-wire"
(~docs/oob-code :target-id "swap-wire"
:text (str "(entry + oob counter: " n ")")))))
@@ -521,7 +521,7 @@
(p :class "text-xs text-amber-600" "Revenue")))
(div :id "dash-footer" :class "p-3 bg-stone-50 rounded"
(p :class "text-sm text-stone-500" (str "Last updated: " now)))
(~doc-oob-code :target-id "filter-wire"
(~docs/oob-code :target-id "filter-wire"
:text (str "(<> (div :id \"dash-header\" ...) (div :id \"dash-stats\" ...) (div :id \"dash-footer\" ...))")))))
@@ -539,10 +539,10 @@
content
(div :id "tab-buttons" :sx-swap-oob "innerHTML"
:class "flex border-b border-stone-200"
(~tab-btn :tab "tab1" :label "Overview" :active (if (= tab "tab1") "true" "false"))
(~tab-btn :tab "tab2" :label "Details" :active (if (= tab "tab2") "true" "false"))
(~tab-btn :tab "tab3" :label "History" :active (if (= tab "tab3") "true" "false")))
(~doc-oob-code :target-id "tabs-wire"
(~examples/tab-btn :tab "tab1" :label "Overview" :active (if (= tab "tab1") "true" "false"))
(~examples/tab-btn :tab "tab2" :label "Details" :active (if (= tab "tab2") "true" "false"))
(~examples/tab-btn :tab "tab3" :label "History" :active (if (= tab "tab3") "true" "false")))
(~docs/oob-code :target-id "tabs-wire"
:text (str "(content for " tab " + oob tab buttons")))))
@@ -560,9 +560,9 @@
(let ((color (nth anim-colors idx)))
(<>
(~anim-result :color color :time now)
(~doc-oob-code :target-id "anim-comp"
(~docs/oob-code :target-id "anim-comp"
:text (component-source "~anim-result"))
(~doc-oob-code :target-id "anim-wire"
(~docs/oob-code :target-id "anim-wire"
:text (str "(~anim-result :color \"" color "\" :time \"" now "\")"))))))
@@ -576,12 +576,12 @@
:returns "element"
(&key)
(<>
(~dialog-modal :title "Confirm Action"
(~examples/dialog-modal :title "Confirm Action"
:message "Are you sure you want to proceed? This is a demo dialog rendered entirely with sx components.")
(~doc-oob-code :target-id "dialog-comp"
:text (component-source "~dialog-modal"))
(~doc-oob-code :target-id "dialog-wire"
:text "(~dialog-modal :title \"Confirm Action\" :message \"...\")")))
(~docs/oob-code :target-id "dialog-comp"
:text (component-source "~examples/dialog-modal"))
(~docs/oob-code :target-id "dialog-wire"
:text "(~examples/dialog-modal :title \"Confirm Action\" :message \"...\")")))
(defhandler ex-dialog-close
:path "/sx/(geography.(hypermedia.(example.(api.dialog-close))))"
@@ -589,7 +589,7 @@
:returns "element"
(&key)
(<>
(~doc-oob-code :target-id "dialog-wire"
(~docs/oob-code :target-id "dialog-wire"
:text "(empty — dialog closed)")))
@@ -605,11 +605,11 @@
(let ((key (request-arg "key" "")))
(let ((action (get kbd-actions key (str "Unknown key: " key))))
(<>
(~kbd-result :key key :action action)
(~doc-oob-code :target-id "kbd-comp"
:text (component-source "~kbd-result"))
(~doc-oob-code :target-id "kbd-wire"
:text (str "(~kbd-result :key \"" key "\" :action \"" action "\")"))))))
(~examples/kbd-result :key key :action action)
(~docs/oob-code :target-id "kbd-comp"
:text (component-source "~examples/kbd-result"))
(~docs/oob-code :target-id "kbd-wire"
:text (str "(~examples/kbd-result :key \"" key "\" :action \"" action "\")"))))))
;; --------------------------------------------------------------------------
@@ -624,11 +624,11 @@
(let ((p (state-get "ex-profile"
{"name" "Ada Lovelace" "email" "ada@example.com" "role" "Engineer"})))
(<>
(~pp-form-full :name (get p "name") :email (get p "email") :role (get p "role"))
(~doc-oob-code :target-id "pp-comp"
:text (component-source "~pp-form-full"))
(~doc-oob-code :target-id "pp-wire"
:text (str "(~pp-form-full :name \"" (get p "name") "\" ...)")))))
(~examples/pp-form-full :name (get p "name") :email (get p "email") :role (get p "role"))
(~docs/oob-code :target-id "pp-comp"
:text (component-source "~examples/pp-form-full"))
(~docs/oob-code :target-id "pp-wire"
:text (str "(~examples/pp-form-full :name \"" (get p "name") "\" ...)")))))
(defhandler ex-pp-put
:path "/sx/(geography.(hypermedia.(example.(api.putpatch))))"
@@ -641,11 +641,11 @@
(role (request-form "role" "")))
(state-set! "ex-profile" {"name" name "email" email "role" role})
(<>
(~pp-view :name name :email email :role role)
(~doc-oob-code :target-id "pp-comp"
:text (component-source "~pp-view"))
(~doc-oob-code :target-id "pp-wire"
:text (str "(~pp-view :name \"" name "\" ...)")))))
(~examples/pp-view :name name :email email :role role)
(~docs/oob-code :target-id "pp-comp"
:text (component-source "~examples/pp-view"))
(~docs/oob-code :target-id "pp-wire"
:text (str "(~examples/pp-view :name \"" name "\" ...)")))))
(defhandler ex-pp-cancel
:path "/sx/(geography.(hypermedia.(example.(api.putpatch-cancel))))"
@@ -655,11 +655,11 @@
(let ((p (state-get "ex-profile"
{"name" "Ada Lovelace" "email" "ada@example.com" "role" "Engineer"})))
(<>
(~pp-view :name (get p "name") :email (get p "email") :role (get p "role"))
(~doc-oob-code :target-id "pp-comp"
:text (component-source "~pp-view"))
(~doc-oob-code :target-id "pp-wire"
:text (str "(~pp-view :name \"" (get p "name") "\" ...)")))))
(~examples/pp-view :name (get p "name") :email (get p "email") :role (get p "role"))
(~docs/oob-code :target-id "pp-comp"
:text (component-source "~examples/pp-view"))
(~docs/oob-code :target-id "pp-wire"
:text (str "(~examples/pp-view :name \"" (get p "name") "\" ...)")))))
;; --------------------------------------------------------------------------
@@ -676,11 +676,11 @@
(ct (request-content-type)))
(let ((body (json-encode data)))
(<>
(~json-result :body body :content-type ct)
(~doc-oob-code :target-id "json-comp"
:text (component-source "~json-result"))
(~doc-oob-code :target-id "json-wire"
:text (str "(~json-result :body \"" body "\" :content-type \"" ct "\")"))))))
(~examples/json-result :body body :content-type ct)
(~docs/oob-code :target-id "json-comp"
:text (component-source "~examples/json-result"))
(~docs/oob-code :target-id "json-wire"
:text (str "(~examples/json-result :body \"" body "\" :content-type \"" ct "\")"))))))
;; --------------------------------------------------------------------------
@@ -698,11 +698,11 @@
vals)))
(let ((items (map (fn (pair) (str (first pair) ": " (nth pair 1))) filtered)))
(<>
(~echo-result :label "values" :items items)
(~doc-oob-code :target-id "vals-comp"
:text (component-source "~echo-result"))
(~doc-oob-code :target-id "vals-wire"
:text (str "(~echo-result :label \"values\" :items (list ...))")))))))
(~examples/echo-result :label "values" :items items)
(~docs/oob-code :target-id "vals-comp"
:text (component-source "~examples/echo-result"))
(~docs/oob-code :target-id "vals-wire"
:text (str "(~examples/echo-result :label \"values\" :items (list ...))")))))))
(defhandler ex-echo-headers
:path "/sx/(geography.(hypermedia.(example.(api.echo-headers))))"
@@ -713,11 +713,11 @@
(let ((custom (filter (fn (pair) (starts-with? (first pair) "x-")) all-headers)))
(let ((items (map (fn (pair) (str (first pair) ": " (nth pair 1))) custom)))
(<>
(~echo-result :label "headers" :items items)
(~doc-oob-code :target-id "vals-comp"
:text (component-source "~echo-result"))
(~doc-oob-code :target-id "vals-wire"
:text (str "(~echo-result :label \"headers\" :items (list ...))")))))))
(~examples/echo-result :label "headers" :items items)
(~docs/oob-code :target-id "vals-comp"
:text (component-source "~examples/echo-result"))
(~docs/oob-code :target-id "vals-wire"
:text (str "(~examples/echo-result :label \"headers\" :items (list ...))")))))))
;; --------------------------------------------------------------------------
@@ -732,11 +732,11 @@
(sleep 2000)
(let ((now (now "%H:%M:%S")))
(<>
(~loading-result :time now)
(~doc-oob-code :target-id "loading-comp"
:text (component-source "~loading-result"))
(~doc-oob-code :target-id "loading-wire"
:text (str "(~loading-result :time \"" now "\")")))))
(~examples/loading-result :time now)
(~docs/oob-code :target-id "loading-comp"
:text (component-source "~examples/loading-result"))
(~docs/oob-code :target-id "loading-wire"
:text (str "(~examples/loading-result :time \"" now "\")")))))
;; --------------------------------------------------------------------------
@@ -752,11 +752,11 @@
(sleep delay-ms)
(let ((q (request-arg "q" "")))
(<>
(~sync-result :query q :delay (str delay-ms))
(~doc-oob-code :target-id "sync-comp"
:text (component-source "~sync-result"))
(~doc-oob-code :target-id "sync-wire"
:text (str "(~sync-result :query \"" q "\" :delay \"" delay-ms "\")"))))))
(~examples/sync-result :query q :delay (str delay-ms))
(~docs/oob-code :target-id "sync-comp"
:text (component-source "~examples/sync-result"))
(~docs/oob-code :target-id "sync-wire"
:text (str "(~examples/sync-result :query \"" q "\" :delay \"" delay-ms "\")"))))))
;; --------------------------------------------------------------------------
@@ -775,8 +775,8 @@
(set-response-status 503)
"")
(<>
(~retry-result :attempt (str n) :message "Success! The endpoint finally responded.")
(~doc-oob-code :target-id "retry-comp"
:text (component-source "~retry-result"))
(~doc-oob-code :target-id "retry-wire"
:text (str "(~retry-result :attempt \"" n "\" ...)"))))))
(~examples/retry-result :attempt (str n) :message "Success! The endpoint finally responded.")
(~docs/oob-code :target-id "retry-comp"
:text (component-source "~examples/retry-result"))
(~docs/oob-code :target-id "retry-wire"
:text (str "(~examples/retry-result :attempt \"" n "\" ...)"))))))

View File

@@ -13,7 +13,7 @@
(let ((now (now "%H:%M:%S")))
(<>
(span :class "text-stone-800 text-sm" "Server time: " (strong now))
(~doc-oob-code :target-id "ref-wire-sx-get"
(~docs/oob-code :target-id "ref-wire-sx-get"
:text (str "(span :class \"text-stone-800 text-sm\" \"Server time: \" (strong \"" now "\"))")))))
;; --- sx-post demo: greet ---
@@ -27,7 +27,7 @@
(let ((name (request-form "name" "stranger")))
(<>
(span :class "text-stone-800 text-sm" "Hello, " (strong name) "!")
(~doc-oob-code :target-id "ref-wire-sx-post"
(~docs/oob-code :target-id "ref-wire-sx-post"
:text (str "(span :class \"text-stone-800 text-sm\" \"Hello, \" (strong \"" name "\") \"!\")")))))
;; --- sx-put demo: status update ---
@@ -41,7 +41,7 @@
(let ((status (request-form "status" "unknown")))
(<>
(span :class "text-stone-700 text-sm" "Status: " (strong status) " — updated via PUT")
(~doc-oob-code :target-id "ref-wire-sx-put"
(~docs/oob-code :target-id "ref-wire-sx-put"
:text (str "(span :class \"text-stone-700 text-sm\" \"Status: \" (strong \"" status "\") \" — updated via PUT\")")))))
;; --- sx-patch demo: theme ---
@@ -55,7 +55,7 @@
(let ((theme (request-form "theme" "unknown")))
(<>
theme
(~doc-oob-code :target-id "ref-wire-sx-patch"
(~docs/oob-code :target-id "ref-wire-sx-patch"
:text (str "\"" theme "\"")))))
;; --- sx-delete demo ---
@@ -67,7 +67,7 @@
:returns "element"
(&key)
(<>
(~doc-oob-code :target-id "ref-wire-sx-delete" :text "\"\"")))
(~docs/oob-code :target-id "ref-wire-sx-delete" :text "\"\"")))
;; --- sx-trigger demo: search ---
@@ -84,7 +84,7 @@
(if (= q "")
(span :class "text-stone-400 text-sm" "Start typing to trigger a search.")
(span :class "text-stone-800 text-sm" "Results for: " (strong q)))
(~doc-oob-code :target-id "ref-wire-sx-trigger" :text sx-text)))))
(~docs/oob-code :target-id "ref-wire-sx-trigger" :text sx-text)))))
;; --- sx-swap demo ---
@@ -96,7 +96,7 @@
(let ((now (now "%H:%M:%S")))
(<>
(div :class "text-sm text-violet-700" (str "New item (" now ")"))
(~doc-oob-code :target-id "ref-wire-sx-swap"
(~docs/oob-code :target-id "ref-wire-sx-swap"
:text (str "(div :class \"text-sm text-violet-700\" \"New item (" now ")\")")))))
;; --- sx-swap-oob demo ---
@@ -111,7 +111,7 @@
(span :class "text-emerald-700 text-sm" "Main updated at " now)
(div :id "ref-oob-side" :sx-swap-oob "innerHTML"
(span :class "text-violet-700 text-sm" "OOB updated at " now))
(~doc-oob-code :target-id "ref-wire-sx-swap-oob"
(~docs/oob-code :target-id "ref-wire-sx-swap-oob"
:text (str "(<> (span ... \"" now "\") (div :id \"ref-oob-side\" :sx-swap-oob \"innerHTML\" ...))")))))
;; --- sx-select demo ---
@@ -128,7 +128,7 @@
(span :class "text-emerald-700 text-sm"
"This fragment was selected from a larger response. Time: " now))
(div :id "the-footer" (p "Page footer — not selected"))
(~doc-oob-code :target-id "ref-wire-sx-select"
(~docs/oob-code :target-id "ref-wire-sx-select"
:text (str "(<> (div :id \"the-header\" ...) (div :id \"the-content\" ... \"" now "\") (div :id \"the-footer\" ...))")))))
;; --- sx-sync demo: slow echo ---
@@ -142,7 +142,7 @@
(sleep 800)
(<>
(span :class "text-stone-800 text-sm" "Echo: " (strong q))
(~doc-oob-code :target-id "ref-wire-sx-sync"
(~docs/oob-code :target-id "ref-wire-sx-sync"
:text (str "(span :class \"text-stone-800 text-sm\" \"Echo: \" (strong \"" q "\"))")))))
;; --- sx-prompt demo ---
@@ -155,7 +155,7 @@
(let ((name (request-header "SX-Prompt" "anonymous")))
(<>
(span :class "text-stone-800 text-sm" "Hello, " (strong name) "!")
(~doc-oob-code :target-id "ref-wire-sx-prompt"
(~docs/oob-code :target-id "ref-wire-sx-prompt"
:text (str "(span :class \"text-stone-800 text-sm\" \"Hello, \" (strong \"" name "\") \"!\")")))))
;; --- Error demo ---
@@ -185,7 +185,7 @@
(let ((sx-text (str "(span :class \"text-stone-800 text-sm\" \"Received: \" (strong \"" display "\"))")))
(<>
(span :class "text-stone-800 text-sm" "Received: " (strong display))
(~doc-oob-code :target-id "ref-wire-sx-encoding" :text sx-text))))))
(~docs/oob-code :target-id "ref-wire-sx-encoding" :text sx-text))))))
;; --- sx-headers demo: echo custom headers ---
@@ -209,7 +209,7 @@
(span :class "text-stone-400 text-sm" "No custom headers received.")
(ul :class "text-sm text-stone-700 space-y-1"
(map (fn (pair) (li (strong (first pair)) ": " (nth pair 1))) custom)))
(~doc-oob-code :target-id "ref-wire-sx-headers" :text sx-text))))))
(~docs/oob-code :target-id "ref-wire-sx-headers" :text sx-text))))))
;; --- sx-include demo: echo GET query params ---
@@ -230,7 +230,7 @@
(span :class "text-stone-400 text-sm" "No values received.")
(ul :class "text-sm text-stone-700 space-y-1"
(map (fn (pair) (li (strong (first pair)) ": " (nth pair 1))) vals)))
(~doc-oob-code :target-id "ref-wire-sx-include" :text sx-text)))))
(~docs/oob-code :target-id "ref-wire-sx-include" :text sx-text)))))
;; --- sx-vals demo: echo POST form values ---
@@ -252,7 +252,7 @@
(span :class "text-stone-400 text-sm" "No values received.")
(ul :class "text-sm text-stone-700 space-y-1"
(map (fn (pair) (li (strong (first pair)) ": " (nth pair 1))) vals)))
(~doc-oob-code :target-id "ref-wire-sx-vals" :text sx-text)))))
(~docs/oob-code :target-id "ref-wire-sx-vals" :text sx-text)))))
;; --- sx-retry demo: flaky endpoint (fails 2/3 times) ---
@@ -270,7 +270,7 @@
(let ((sx-text (str "(span :class \"text-emerald-700 text-sm\" \"Success on attempt \" \"" n "\" \"!\")")))
(<>
(span :class "text-emerald-700 text-sm" "Success on attempt " (str n) "!")
(~doc-oob-code :target-id "ref-wire-sx-retry" :text sx-text))))))
(~docs/oob-code :target-id "ref-wire-sx-retry" :text sx-text))))))
;; --- sx-trigger-event demo: response header triggers ---

View File

@@ -1,5 +1,5 @@
;; SX docs layout defcomps + in-page navigation.
;; Layout = root header only. Nav is in-page via ~sx-doc wrapper.
;; Layout = root header only. Nav is in-page via ~layouts/doc wrapper.
;; ---------------------------------------------------------------------------
;; Nav components — logo header, sibling arrows, children links
@@ -15,7 +15,7 @@
;; The server can update these during navigation morphs without disturbing
;; the reactive colour-cycling state. This is Level 2-3: the water (server
;; content) flows through the island, around the rocks (reactive signals).
(defisland ~sx-header (&key path)
(defisland ~layouts/header (&key path)
(let ((families (list "violet" "rose" "blue" "emerald" "amber" "cyan" "red" "teal" "pink" "indigo"))
(idx (signal 0))
(shade (signal 500))
@@ -60,7 +60,7 @@
;; Current section with prev/next siblings.
;; 3-column grid: prev is right-aligned, current centered, next left-aligned.
;; Current page is larger in the leaf (bottom) row.
(defcomp ~nav-sibling-row (&key node siblings is-leaf level depth)
(defcomp ~layouts/nav-sibling-row (&key node siblings is-leaf level depth)
(let* ((sibs (or siblings (list)))
(count (len sibs))
;; opacity = (n/x * 3/4) + 1/4
@@ -100,7 +100,7 @@
(str (get next-node "label") " →")))))))
;; Children links — shown as clearly clickable buttons.
(defcomp ~nav-children (&key items)
(defcomp ~layouts/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)
@@ -115,11 +115,11 @@
items))))
;; ---------------------------------------------------------------------------
;; ~sx-doc — in-page content wrapper with nav
;; ~layouts/doc — in-page content wrapper with nav
;; Used by every defpage :content to embed nav inside the page content area.
;; ---------------------------------------------------------------------------
(defcomp ~sx-doc (&key path &rest children) :affinity :server
(defcomp ~layouts/doc (&key path &rest children) :affinity :server
(let* ((nav-state (resolve-nav-path sx-nav-tree (or path "/")))
(trail (or (get nav-state "trail") (list)))
(trail-len (len trail))
@@ -132,12 +132,12 @@
(div :id "logo-opacity"
:style (str "opacity:" (+ (* (/ 1 depth) 0.75) 0.25) ";"
"transition:opacity 0.3s;")
(~sx-header :path (or path "/")))
(~layouts/header :path (or path "/")))
;; Sibling arrows for EVERY level in the trail
;; Trail row i is level (i+2) of depth — opacity = (i+2)/depth
;; Last row (leaf) gets is-leaf for larger current page title
(map-indexed (fn (i crumb)
(~nav-sibling-row
(~layouts/nav-sibling-row
:node (get crumb "node")
:siblings (get crumb "siblings")
:is-leaf (= i (- trail-len 1))
@@ -146,34 +146,34 @@
trail)
;; Children as button links
(when (get nav-state "children")
(~nav-children :items (get nav-state "children"))))
(~layouts/nav-children :items (get nav-state "children"))))
;; Page content follows
children)))
;; ---------------------------------------------------------------------------
;; SX docs layouts — root header only (nav is in page content via ~sx-doc)
;; SX docs layouts — root header only (nav is in page content via ~layouts/doc)
;; ---------------------------------------------------------------------------
(defcomp ~sx-docs-layout-full ()
(defcomp ~layouts/docs-layout-full ()
nil)
(defcomp ~sx-docs-layout-oob ()
(defcomp ~layouts/docs-layout-oob ()
nil)
(defcomp ~sx-docs-layout-mobile ()
(defcomp ~layouts/docs-layout-mobile ()
nil)
;; ---------------------------------------------------------------------------
;; Standalone layouts (no root header — for sx-web.org)
;; ---------------------------------------------------------------------------
(defcomp ~sx-standalone-docs-layout-full ()
(defcomp ~layouts/standalone-docs-layout-full ()
nil)
;; Standalone OOB: nothing needed — nav is in content.
(defcomp ~sx-standalone-docs-layout-oob ()
(defcomp ~layouts/standalone-docs-layout-oob ()
nil)
;; Standalone mobile: nothing — nav is in content.
(defcomp ~sx-standalone-docs-layout-mobile ()
(defcomp ~layouts/standalone-docs-layout-mobile ()
nil)

View File

@@ -275,7 +275,7 @@
:prose "The HTML adapter renders evaluated SX expressions to HTML strings. It is used server-side to produce complete HTML pages and fragments. It handles void elements (self-closing tags like <br>, <img>), boolean attributes, style serialization, class merging, and proper escaping. The output is standard HTML5 that any browser can parse.")
(dict :slug "adapter-sx" :filename "adapter-sx.sx" :title "SX Wire Adapter"
:desc "Serializes SX for client-side rendering. Component calls stay unexpanded."
:prose "The SX wire adapter serializes expressions as SX source text for transmission to the browser, where sx.js renders them client-side. Unlike the HTML adapter, component calls (~name ...) are NOT expanded — they are sent to the client as-is, allowing the browser to render them with its local component registry. HTML tags ARE serialized as s-expression source. This is the format used for SX-over-HTTP responses and the page boot payload.")
:prose "The SX wire adapter serializes expressions as SX source text for transmission to the browser, where sx.js renders them client-side. Unlike the HTML adapter, component calls (~plans/content-addressed-components/name ...) are NOT expanded — they are sent to the client as-is, allowing the browser to render them with its local component registry. HTML tags ARE serialized as s-expression source. This is the format used for SX-over-HTTP responses and the page boot payload.")
(dict :slug "adapter-async" :filename "adapter-async.sx" :title "Async Adapter"
:desc "Async versions of HTML and SX wire adapters for server-side rendering with I/O."
:prose "The async adapter provides async-aware versions of the HTML and SX wire rendering functions. It intercepts I/O operations (database queries, service calls, fragment fetches) during evaluation, awaiting them before continuing. Entry points: async-render (HTML output with awaited I/O), async-aser (SX wire format with awaited I/O). The bootstrapper emits async def and automatic await insertion for all define-async functions. This adapter is what makes server-side SX pages work with real data.")))
@@ -344,10 +344,10 @@
;; 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)
;; KEPT for backward compat with other apps — SX docs uses ~plans/nav-redesign/nav-list instead.
(defcomp ~nav-data/section-nav (&key items current)
(<> (map (fn (item)
(~nav-link
(~shared:layout/nav-link
:href (get item "href")
:label (get item "label")
:is-selected (when (= (get item "label") current) "true")

View File

@@ -1,6 +1,6 @@
;; 404 Not Found page content
(defcomp ~not-found-content (&key (path :as string?))
(defcomp ~not-found/content (&key (path :as string?))
(div :class "max-w-3xl mx-auto px-4 py-12 text-center"
(h1 :style (cssx (:text (colour "stone" 800) (size "3xl") (weight "bold")))
"404")

View File

@@ -8,7 +8,7 @@
;; "sx:offline syncing" — reconnected, replaying queued mutations
;; "sx:offline synced" — individual mutation confirmed by server
(defcomp ~offline-demo-content (&key notes server-time)
(defcomp ~offline-demo/content (&key notes server-time)
(div :class "space-y-8"
(div :class "border-b border-stone-200 pb-6"
(h1 :class "text-2xl font-bold text-stone-900" "Offline Data Layer")

View File

@@ -7,7 +7,7 @@
;; "sx:optimistic confirmed" — server accepted the mutation
;; "sx:optimistic reverted" — server rejected, data rolled back
(defcomp ~optimistic-demo-content (&key items server-time)
(defcomp ~optimistic-demo/content (&key items server-time)
(div :class "space-y-8"
(div :class "border-b border-stone-200 pb-6"
(h1 :class "text-2xl font-bold text-stone-900" "Optimistic Updates")

View File

@@ -12,8 +12,8 @@
;;
;; URL eval: /(language.(doc.introduction))
;; → (language (doc "introduction"))
;; → async_eval returns [Symbol("~docs-introduction-content")]
;; → _eval_slot wraps in (~sx-doc :path "..." <ast>) and renders via aser
;; → async_eval returns [Symbol("~docs-content/docs-introduction-content")]
;; → _eval_slot wraps in (~layouts/doc :path "..." <ast>) and renders via aser
;;
;; NOTE: Lambda &rest is not supported by call-lambda in the current spec.
;; All functions take explicit positional params; missing args default to nil.
@@ -24,7 +24,7 @@
(define home
(fn (content)
(if (nil? content) '(~sx-home-content) content)))
(if (nil? content) '(~docs-content/home-content) content)))
(define language
(fn (content)
@@ -51,27 +51,27 @@
(define reactive
(fn (slug)
(if (nil? slug)
'(~reactive-islands-index-content)
'(~reactive-islands/index/reactive-islands-index-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)))))
"demo" '(~reactive-islands/demo/reactive-islands-demo-content)
"event-bridge" '(~reactive-islands/event-bridge/reactive-islands-event-bridge-content)
"named-stores" '(~reactive-islands/named-stores/reactive-islands-named-stores-content)
"plan" '(~reactive-islands/plan/reactive-islands-plan-content)
"phase2" '(~reactive-islands/phase2/reactive-islands-phase2-content)
:else '(~reactive-islands/index/reactive-islands-index-content)))))
(define marshes
(fn (content)
(if (nil? content) '(~reactive-islands-marshes-content) content)))
(if (nil? content) '(~reactive-islands/marshes/reactive-islands-marshes-content) content)))
(define isomorphism
(fn (slug)
(if (nil? slug)
'(~plan-isomorphic-content)
'(~plans/isomorphic/plan-isomorphic-content)
(case slug
"bundle-analyzer"
(let ((data (bundle-analyzer-data)))
`(~bundle-analyzer-content
`(~analyzer/bundle-analyzer-content
:pages ,(get data "pages")
:total-components ,(get data "total-components")
:total-macros ,(get data "total-macros")
@@ -79,7 +79,7 @@
:io-count ,(get data "io-count")))
"routing-analyzer"
(let ((data (routing-analyzer-data)))
`(~routing-analyzer-content
`(~routing-analyzer/content
:pages ,(get data "pages")
:total-pages ,(get data "total-pages")
:client-count ,(get data "client-count")
@@ -87,29 +87,29 @@
:registry-sample ,(get data "registry-sample")))
"data-test"
(let ((data (data-test-data)))
`(~data-test-content
`(~data-test/content
:server-time ,(get data "server-time")
:items ,(get data "items")
:phase ,(get data "phase")
:transport ,(get data "transport")))
"async-io" '(~async-io-demo-content)
"async-io" '(~async-io-demo/content)
"affinity"
(let ((data (affinity-demo-data)))
`(~affinity-demo-content
`(~affinity-demo/content
:components ,(get data "components")
:page-plans ,(get data "page-plans")))
"optimistic"
(let ((data (optimistic-demo-data)))
`(~optimistic-demo-content
`(~optimistic-demo/content
:items ,(get data "items")
:server-time ,(get data "server-time")))
"offline"
(let ((data (offline-demo-data)))
`(~offline-demo-content
`(~offline-demo/content
:notes ,(get data "notes")
:server-time ,(get data "server-time")))
;; "streaming" → handled as special case by Python router
:else '(~plan-isomorphic-content)))))
:else '(~plans/isomorphic/plan-isomorphic-content)))))
;; ---------------------------------------------------------------------------
;; Page functions — leaf dispatch to content components
@@ -119,63 +119,63 @@
(define doc
(fn (slug)
(if (nil? slug)
'(~docs-introduction-content)
'(~docs-content/docs-introduction-content)
(case slug
"introduction" '(~docs-introduction-content)
"getting-started" '(~docs-getting-started-content)
"components" '(~docs-components-content)
"evaluator" '(~docs-evaluator-content)
"introduction" '(~docs-content/docs-introduction-content)
"getting-started" '(~docs-content/docs-getting-started-content)
"components" '(~docs-content/docs-components-content)
"evaluator" '(~docs-content/docs-evaluator-content)
"primitives"
(let ((data (primitives-data)))
`(~docs-primitives-content
:prims (~doc-primitives-tables :primitives ,data)))
`(~docs-content/docs-primitives-content
:prims (~docs/primitives-tables :primitives ,data)))
"special-forms"
(let ((data (special-forms-data)))
`(~docs-special-forms-content
:forms (~doc-special-forms-tables :forms ,data)))
"server-rendering" '(~docs-server-rendering-content)
:else '(~docs-introduction-content)))))
`(~docs-content/docs-special-forms-content
:forms (~docs/special-forms-tables :forms ,data)))
"server-rendering" '(~docs-content/docs-server-rendering-content)
:else '(~docs-content/docs-introduction-content)))))
;; Specs (under language)
(define spec
(fn (slug)
(if (nil? slug)
'(~spec-architecture-content)
'(~specs/architecture-content)
(case slug
"core"
(let ((files (make-spec-files core-spec-items)))
`(~spec-overview-content :spec-title "Core Language" :spec-files ,files))
`(~specs/overview-content :spec-title "Core Language" :spec-files ,files))
"adapters"
(let ((files (make-spec-files adapter-spec-items)))
`(~spec-overview-content :spec-title "Adapters" :spec-files ,files))
`(~specs/overview-content :spec-title "Adapters" :spec-files ,files))
"browser"
(let ((files (make-spec-files browser-spec-items)))
`(~spec-overview-content :spec-title "Browser Runtime" :spec-files ,files))
`(~specs/overview-content :spec-title "Browser Runtime" :spec-files ,files))
"reactive"
(let ((files (make-spec-files reactive-spec-items)))
`(~spec-overview-content :spec-title "Reactive System" :spec-files ,files))
`(~specs/overview-content :spec-title "Reactive System" :spec-files ,files))
"host"
(let ((files (make-spec-files host-spec-items)))
`(~spec-overview-content :spec-title "Host Interface" :spec-files ,files))
`(~specs/overview-content :spec-title "Host Interface" :spec-files ,files))
"extensions"
(let ((files (make-spec-files extension-spec-items)))
`(~spec-overview-content :spec-title "Extensions" :spec-files ,files))
`(~specs/overview-content :spec-title "Extensions" :spec-files ,files))
:else (let ((found-spec (find-spec slug)))
(if found-spec
(let ((src (read-spec-file (get found-spec "filename"))))
`(~spec-detail-content
`(~specs/detail-content
:spec-title ,(get found-spec "title")
:spec-desc ,(get found-spec "desc")
:spec-filename ,(get found-spec "filename")
:spec-source ,src
:spec-prose ,(get found-spec "prose")))
`(~spec-not-found :slug ,slug)))))))
`(~specs/not-found :slug ,slug)))))))
;; Spec explorer (under language → spec)
(define explore
(fn (slug)
(if (nil? slug)
'(~spec-architecture-content)
'(~specs/architecture-content)
(let ((found-spec (find-spec slug)))
(if found-spec
(let ((data (spec-explorer-data
@@ -183,9 +183,9 @@
(get found-spec "title")
(get found-spec "desc"))))
(if data
`(~spec-explorer-content :data ,data)
`(~spec-not-found :slug ,slug)))
`(~spec-not-found :slug ,slug))))))
`(~specs-explorer/spec-explorer-content :data ,data)
`(~specs/not-found :slug ,slug)))
`(~specs/not-found :slug ,slug))))))
;; Helper used by spec — make-spec-files
(define make-spec-files
@@ -201,13 +201,13 @@
(define bootstrapper
(fn (slug)
(if (nil? slug)
'(~bootstrappers-index-content)
'(~specs/bootstrappers-index-content)
(let ((data (bootstrapper-data slug)))
(if (get data "bootstrapper-not-found")
`(~spec-not-found :slug ,slug)
`(~specs/not-found :slug ,slug)
(case slug
"self-hosting"
`(~bootstrapper-self-hosting-content
`(~specs/bootstrapper-self-hosting-content
:py-sx-source ,(get data "py-sx-source")
:g0-output ,(get data "g0-output")
:g1-output ,(get data "g1-output")
@@ -217,19 +217,19 @@
:g0-bytes ,(get data "g0-bytes")
:verification-status ,(get data "verification-status"))
"self-hosting-js"
`(~bootstrapper-self-hosting-js-content
`(~specs/bootstrapper-self-hosting-js-content
:js-sx-source ,(get data "js-sx-source")
:defines-matched ,(get data "defines-matched")
:defines-total ,(get data "defines-total")
:js-sx-lines ,(get data "js-sx-lines")
:verification-status ,(get data "verification-status"))
"python"
`(~bootstrapper-py-content
`(~specs/bootstrapper-py-content
:bootstrapper-source ,(get data "bootstrapper-source")
:bootstrapped-output ,(get data "bootstrapped-output"))
"page-helpers"
(let ((ph-data (page-helpers-demo-data)))
`(~page-helpers-demo-content
`(~page-helpers-demo/content
:sf-categories ,(get ph-data "sf-categories")
:sf-total ,(get ph-data "sf-total")
:sf-ms ,(get ph-data "sf-ms")
@@ -247,7 +247,7 @@
:req-attrs ,(get ph-data "req-attrs")
:attr-keys ,(get ph-data "attr-keys")))
:else
`(~bootstrapper-js-content
`(~specs/bootstrapper-js-content
:bootstrapper-source ,(get data "bootstrapper-source")
:bootstrapped-output ,(get data "bootstrapped-output"))))))))
@@ -256,7 +256,7 @@
(fn (slug)
(if (nil? slug)
(let ((data (run-modular-tests "all")))
`(~testing-overview-content
`(~testing/overview-content
:server-results ,(get data "server-results")
:framework-source ,(get data "framework-source")
:eval-source ,(get data "eval-source")
@@ -266,81 +266,81 @@
:deps-source ,(get data "deps-source")
:engine-source ,(get data "engine-source")))
(case slug
"runners" '(~testing-runners-content)
"runners" '(~testing/runners-content)
:else
(let ((data (run-modular-tests slug)))
(case slug
"eval" `(~testing-spec-content
"eval" `(~testing/spec-content
:spec-name "eval" :spec-title "Evaluator Tests"
:spec-desc "81 tests covering the core evaluator and all primitives."
:spec-source ,(get data "spec-source")
:framework-source ,(get data "framework-source")
:server-results ,(get data "server-results"))
"parser" `(~testing-spec-content
"parser" `(~testing/spec-content
:spec-name "parser" :spec-title "Parser Tests"
:spec-desc "39 tests covering tokenization and parsing."
:spec-source ,(get data "spec-source")
:framework-source ,(get data "framework-source")
:server-results ,(get data "server-results"))
"router" `(~testing-spec-content
"router" `(~testing/spec-content
:spec-name "router" :spec-title "Router Tests"
:spec-desc "18 tests covering client-side route matching."
:spec-source ,(get data "spec-source")
:framework-source ,(get data "framework-source")
:server-results ,(get data "server-results"))
"render" `(~testing-spec-content
"render" `(~testing/spec-content
:spec-name "render" :spec-title "Renderer Tests"
:spec-desc "23 tests covering HTML rendering."
:spec-source ,(get data "spec-source")
:framework-source ,(get data "framework-source")
:server-results ,(get data "server-results"))
"deps" `(~testing-spec-content
"deps" `(~testing/spec-content
:spec-name "deps" :spec-title "Dependency Analysis Tests"
:spec-desc "33 tests covering component dependency analysis."
:spec-source ,(get data "spec-source")
:framework-source ,(get data "framework-source")
:server-results ,(get data "server-results"))
"engine" `(~testing-spec-content
"engine" `(~testing/spec-content
:spec-name "engine" :spec-title "Engine Tests"
:spec-desc "37 tests covering engine pure functions."
:spec-source ,(get data "spec-source")
:framework-source ,(get data "framework-source")
:server-results ,(get data "server-results"))
"orchestration" `(~testing-spec-content
"orchestration" `(~testing/spec-content
:spec-name "orchestration" :spec-title "Orchestration Tests"
:spec-desc "17 tests covering orchestration."
:spec-source ,(get data "spec-source")
:framework-source ,(get data "framework-source")
:server-results ,(get data "server-results"))
:else `(~testing-overview-content
:else `(~testing/overview-content
:server-results ,(get data "server-results"))))))))
;; Reference (under geography → hypermedia)
(define reference
(fn (slug)
(if (nil? slug)
'(~reference-index-content)
'(~examples/reference-index-content)
(let ((data (reference-data slug)))
(case slug
"attributes" `(~reference-attrs-content
:req-table (~doc-attr-table-from-data :title "Request Attributes" :attrs ,(get data "req-attrs"))
:beh-table (~doc-attr-table-from-data :title "Behavior Attributes" :attrs ,(get data "beh-attrs"))
:uniq-table (~doc-attr-table-from-data :title "Unique to sx" :attrs ,(get data "uniq-attrs")))
"headers" `(~reference-headers-content
:req-table (~doc-headers-table-from-data :title "Request Headers" :headers ,(get data "req-headers"))
:resp-table (~doc-headers-table-from-data :title "Response Headers" :headers ,(get data "resp-headers")))
"events" `(~reference-events-content
:table (~doc-two-col-table-from-data
"attributes" `(~reference/attrs-content
:req-table (~docs/attr-table-from-data :title "Request Attributes" :attrs ,(get data "req-attrs"))
:beh-table (~docs/attr-table-from-data :title "Behavior Attributes" :attrs ,(get data "beh-attrs"))
:uniq-table (~docs/attr-table-from-data :title "Unique to sx" :attrs ,(get data "uniq-attrs")))
"headers" `(~reference/headers-content
:req-table (~docs/headers-table-from-data :title "Request Headers" :headers ,(get data "req-headers"))
:resp-table (~docs/headers-table-from-data :title "Response Headers" :headers ,(get data "resp-headers")))
"events" `(~reference/events-content
:table (~docs/two-col-table-from-data
:intro "sx fires custom DOM events at various points in the request lifecycle."
:col1 "Event" :col2 "Description" :items ,(get data "events-list")))
"js-api" `(~reference-js-api-content
:table (~doc-two-col-table-from-data
"js-api" `(~reference/js-api-content
:table (~docs/two-col-table-from-data
:intro "The client-side sx.js library exposes a public API for programmatic use."
:col1 "Method" :col2 "Description" :items ,(get data "js-api-list")))
:else `(~reference-attrs-content
:req-table (~doc-attr-table-from-data :title "Request Attributes" :attrs ,(get data "req-attrs"))
:beh-table (~doc-attr-table-from-data :title "Behavior Attributes" :attrs ,(get data "beh-attrs"))
:uniq-table (~doc-attr-table-from-data :title "Unique to sx" :attrs ,(get data "uniq-attrs"))))))))
:else `(~reference/attrs-content
:req-table (~docs/attr-table-from-data :title "Request Attributes" :attrs ,(get data "req-attrs"))
:beh-table (~docs/attr-table-from-data :title "Behavior Attributes" :attrs ,(get data "beh-attrs"))
:uniq-table (~docs/attr-table-from-data :title "Unique to sx" :attrs ,(get data "uniq-attrs"))))))))
;; Reference detail pages (under geography → hypermedia → reference)
;; Takes two positional args: kind and slug
@@ -351,8 +351,8 @@
"attributes"
(let ((data (attr-detail-data slug)))
(if (get data "attr-not-found")
`(~reference-attr-not-found :slug ,slug)
`(~reference-attr-detail-content
`(~reference/attr-not-found :slug ,slug)
`(~reference/attr-detail-content
:title ,(get data "attr-title")
:description ,(get data "attr-description")
:demo ,(get data "attr-demo")
@@ -362,8 +362,8 @@
"headers"
(let ((data (header-detail-data slug)))
(if (get data "header-not-found")
`(~reference-attr-not-found :slug ,slug)
`(~reference-header-detail-content
`(~reference/attr-not-found :slug ,slug)
`(~reference/header-detail-content
:title ,(get data "header-title")
:direction ,(get data "header-direction")
:description ,(get data "header-description")
@@ -372,8 +372,8 @@
"events"
(let ((data (event-detail-data slug)))
(if (get data "event-not-found")
`(~reference-attr-not-found :slug ,slug)
`(~reference-event-detail-content
`(~reference/attr-not-found :slug ,slug)
`(~reference/event-detail-content
:title ,(get data "event-title")
:description ,(get data "event-description")
:example-code ,(get data "event-example")
@@ -386,142 +386,142 @@
(if (nil? slug)
nil
(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)))))
"click-to-load" '(~examples-content/example-click-to-load)
"form-submission" '(~examples-content/example-form-submission)
"polling" '(~examples-content/example-polling)
"delete-row" '(~examples-content/example-delete-row)
"inline-edit" '(~examples-content/example-inline-edit)
"oob-swaps" '(~examples-content/example-oob-swaps)
"lazy-loading" '(~examples-content/example-lazy-loading)
"infinite-scroll" '(~examples-content/example-infinite-scroll)
"progress-bar" '(~examples-content/example-progress-bar)
"active-search" '(~examples-content/example-active-search)
"inline-validation" '(~examples-content/example-inline-validation)
"value-select" '(~examples-content/example-value-select)
"reset-on-submit" '(~examples-content/example-reset-on-submit)
"edit-row" '(~examples-content/example-edit-row)
"bulk-update" '(~examples-content/example-bulk-update)
"swap-positions" '(~examples-content/example-swap-positions)
"select-filter" '(~examples-content/example-select-filter)
"tabs" '(~examples-content/example-tabs)
"animations" '(~examples-content/example-animations)
"dialogs" '(~examples-content/example-dialogs)
"keyboard-shortcuts" '(~examples-content/example-keyboard-shortcuts)
"put-patch" '(~examples-content/example-put-patch)
"json-encoding" '(~examples-content/example-json-encoding)
"vals-and-headers" '(~examples-content/example-vals-and-headers)
"loading-states" '(~examples-content/example-loading-states)
"sync-replace" '(~examples-content/example-sync-replace)
"retry" '(~examples-content/example-retry)
:else '(~examples-content/example-click-to-load)))))
;; SX URLs (under applications)
(define sx-urls
(fn (slug)
'(~sx-urls-content)))
'(~sx-urls/urls-content)))
;; CSSX (under applications)
(define cssx
(fn (slug)
(if (nil? slug)
'(~cssx-overview-content)
'(~cssx/overview-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)))))
"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)))))
;; Protocols (under applications)
(define protocol
(fn (slug)
(if (nil? slug)
'(~protocol-wire-format-content)
'(~protocols/wire-format-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)))))
"wire-format" '(~protocols/wire-format-content)
"fragments" '(~protocols/fragments-content)
"resolver-io" '(~protocols/resolver-io-content)
"internal-services" '(~protocols/internal-services-content)
"activitypub" '(~protocols/activitypub-content)
"future" '(~protocols/future-content)
:else '(~protocols/wire-format-content)))))
;; Essays (under etc)
(define essay
(fn (slug)
(if (nil? slug)
'(~essays-index-content)
'(~essays/index/essays-index-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)
"hegelian-synthesis" '(~essay-hegelian-synthesis)
"the-art-chain" '(~essay-the-art-chain)
"self-defining-medium" '(~essay-self-defining-medium)
:else '(~essays-index-content)))))
"sx-sucks" '(~essays/sx-sucks/essay-sx-sucks)
"why-sexps" '(~essays/why-sexps/essay-why-sexps)
"htmx-react-hybrid" '(~essays/htmx-react-hybrid/essay-htmx-react-hybrid)
"on-demand-css" '(~essays/on-demand-css/essay-on-demand-css)
"client-reactivity" '(~essays/client-reactivity/essay-client-reactivity)
"sx-native" '(~essays/sx-native/essay-sx-native)
"tail-call-optimization" '(~essays/tail-call-optimization/essay-tail-call-optimization)
"continuations" '(~essays/continuations/essay-continuations)
"reflexive-web" '(~essays/reflexive-web/essay-reflexive-web)
"server-architecture" '(~essays/server-architecture/essay-server-architecture)
"separation-of-concerns" '(~essays/separation-of-concerns/essay-separation-of-concerns)
"sx-and-ai" '(~essays/sx-and-ai/essay-sx-and-ai)
"no-alternative" '(~essays/no-alternative/essay-no-alternative)
"zero-tooling" '(~essays/zero-tooling/essay-zero-tooling)
"react-is-hypermedia" '(~essays/react-is-hypermedia/essay-react-is-hypermedia)
"hegelian-synthesis" '(~essays/hegelian-synthesis/essay-hegelian-synthesis)
"the-art-chain" '(~essays/the-art-chain/essay-the-art-chain)
"self-defining-medium" '(~essays/self-defining-medium/essay-self-defining-medium)
:else '(~essays/index/essays-index-content)))))
;; Philosophy (under etc)
(define philosophy
(fn (slug)
(if (nil? slug)
'(~philosophy-index-content)
'(~essays/philosophy-index/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)))))
"godel-escher-bach" '(~essays/godel-escher-bach/essay-godel-escher-bach)
"wittgenstein" '(~essays/sx-and-wittgenstein/essay-sx-and-wittgenstein)
"dennett" '(~essays/sx-and-dennett/essay-sx-and-dennett)
"existentialism" '(~essays/s-existentialism/essay-s-existentialism)
:else '(~essays/philosophy-index/content)))))
;; Plans (under etc)
(define plan
(fn (slug)
(if (nil? slug)
'(~plans-index-content)
'(~plans/index/plans-index-content)
(case slug
"status" '(~plan-status-content)
"reader-macros" '(~plan-reader-macros-content)
"reader-macro-demo" '(~plan-reader-macro-demo-content)
"status" '(~plans/status/plan-status-content)
"reader-macros" '(~plans/reader-macros/plan-reader-macros-content)
"reader-macro-demo" '(~plans/reader-macro-demo/plan-reader-macro-demo-content)
"theorem-prover"
(let ((data (prove-data)))
'(~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)
"sx-forge" '(~plan-sx-forge-content)
"sx-swarm" '(~plan-sx-swarm-content)
"sx-proxy" '(~plan-sx-proxy-content)
"async-eval-convergence" '(~plan-async-eval-convergence-content)
"wasm-bytecode-vm" '(~plan-wasm-bytecode-vm-content)
"generative-sx" '(~plan-generative-sx-content)
"art-dag-sx" '(~plan-art-dag-sx-content)
"spec-explorer" '(~plan-spec-explorer-content)
"sx-urls" '(~plan-sx-urls-content)
"sx-protocol" '(~plan-sx-protocol-content)
:else '(~plans-index-content)))))
'(~plans/theorem-prover/plan-theorem-prover-content))
"self-hosting-bootstrapper" '(~plans/self-hosting-bootstrapper/plan-self-hosting-bootstrapper-content)
"js-bootstrapper" '(~plans/js-bootstrapper/plan-js-bootstrapper-content)
"sx-activity" '(~plans/sx-activity/plan-sx-activity-content)
"predictive-prefetch" '(~plans/predictive-prefetch/plan-predictive-prefetch-content)
"content-addressed-components" '(~plans/content-addressed-components/plan-content-addressed-components-content)
"environment-images" '(~plans/environment-images/plan-environment-images-content)
"runtime-slicing" '(~plans/runtime-slicing/plan-runtime-slicing-content)
"typed-sx" '(~plans/typed-sx/plan-typed-sx-content)
"nav-redesign" '(~plans/nav-redesign/plan-nav-redesign-content)
"fragment-protocol" '(~plans/fragment-protocol/plan-fragment-protocol-content)
"glue-decoupling" '(~plans/glue-decoupling/plan-glue-decoupling-content)
"social-sharing" '(~plans/social-sharing/plan-social-sharing-content)
"sx-ci" '(~plans/sx-ci/plan-sx-ci-content)
"live-streaming" '(~plans/live-streaming/plan-live-streaming-content)
"sx-web-platform" '(~plans/sx-web-platform/plan-sx-web-platform-content)
"sx-forge" '(~plans/sx-forge/plan-sx-forge-content)
"sx-swarm" '(~plans/sx-swarm/plan-sx-swarm-content)
"sx-proxy" '(~plans/sx-proxy/plan-sx-proxy-content)
"async-eval-convergence" '(~plans/async-eval-convergence/plan-async-eval-convergence-content)
"wasm-bytecode-vm" '(~plans/wasm-bytecode-vm/plan-wasm-bytecode-vm-content)
"generative-sx" '(~plans/generative-sx/plan-generative-sx-content)
"art-dag-sx" '(~plans/art-dag-sx/plan-art-dag-sx-content)
"spec-explorer" '(~plans/spec-explorer/plan-spec-explorer-content)
"sx-urls" '(~plans/sx-urls/plan-sx-urls-content)
"sx-protocol" '(~plans/sx-protocol/plan-sx-protocol-content)
:else '(~plans/index/plans-index-content)))))

View File

@@ -9,7 +9,7 @@
;; Shared card component — used by both server and client results
;; ---------------------------------------------------------------------------
(defcomp ~demo-result-card (&key (title :as string) (ms :as number) (desc :as string) (theme :as string) &rest children)
(defcomp ~page-helpers-demo/demo-result-card (&key (title :as string) (ms :as number) (desc :as string) (theme :as string) &rest children)
(let ((border (if (= theme "blue") "border-blue-200 bg-blue-50/30" "border-stone-200"))
(title-c (if (= theme "blue") "text-blue-700" "text-stone-700"))
(badge-c (if (= theme "blue") "text-blue-400" "text-stone-400"))
@@ -28,7 +28,7 @@
;; Client-side island — runs spec functions in the browser on button click
;; ---------------------------------------------------------------------------
(defisland ~demo-client-runner (&key sf-source attr-detail req-attrs attr-keys)
(defisland ~page-helpers-demo/demo-client-runner (&key sf-source attr-detail req-attrs attr-keys)
(let ((results (signal nil))
(running (signal false))
(run-demo (fn (e)
@@ -105,7 +105,7 @@
(let ((r (deref results)))
(div :class "grid grid-cols-1 md:grid-cols-2 gap-4"
(~demo-result-card
(~page-helpers-demo/demo-result-card
:title "categorize-special-forms"
:ms (get r "sf-ms") :theme "blue"
:desc "Parses special-forms.sx and classifies each form by category (control flow, binding, quoting, etc)."
@@ -116,7 +116,7 @@
(div (str k ": " (get (get r "sf-cats") k))))
(keys (get r "sf-cats"))))
(~demo-result-card
(~page-helpers-demo/demo-result-card
:title "build-reference-data"
:ms (get r "ref-ms") :theme "blue"
:desc "Takes raw attribute tuples and generates reference table rows with documentation hrefs."
@@ -127,7 +127,7 @@
(or (get item "href") "no detail page"))))
(get r "ref-sample")))
(~demo-result-card
(~page-helpers-demo/demo-result-card
:title "build-attr-detail"
:ms (get r "attr-ms") :theme "blue"
:desc "Builds a detail page data structure for a single attribute (sx-get): title, wire ID, handler status."
@@ -135,7 +135,7 @@
(div (str "wire-id: " (or (get (get r "attr-result") "attr-wire-id") "none")))
(div (str "has handler: " (if (get (get r "attr-result") "attr-handler") "yes" "no"))))
(~demo-result-card
(~page-helpers-demo/demo-result-card
:title "build-component-source"
:ms (get r "comp-ms") :theme "blue"
:desc "Reconstructs a defcomp source definition from a component metadata dict (name, params, body)."
@@ -165,7 +165,7 @@
;; Main page component — server-rendered content + client island
;; ---------------------------------------------------------------------------
(defcomp ~page-helpers-demo-content (&key
(defcomp ~page-helpers-demo/content (&key
sf-categories sf-total sf-ms
ref-sample ref-ms
attr-result attr-ms
@@ -196,7 +196,7 @@
(div :class "grid grid-cols-1 md:grid-cols-2 gap-4"
(~demo-result-card
(~page-helpers-demo/demo-result-card
:title "categorize-special-forms"
:ms sf-ms :theme "stone"
:desc "Parses special-forms.sx and classifies each form by category (control flow, binding, quoting, etc)."
@@ -207,7 +207,7 @@
(div (str k ": " (get sf-categories k))))
(keys sf-categories)))
(~demo-result-card
(~page-helpers-demo/demo-result-card
:title "build-reference-data"
:ms ref-ms :theme "stone"
:desc "Takes raw attribute tuples and generates reference table rows with documentation hrefs."
@@ -218,7 +218,7 @@
(or (get item "href") "no detail page"))))
ref-sample))
(~demo-result-card
(~page-helpers-demo/demo-result-card
:title "build-attr-detail"
:ms attr-ms :theme "stone"
:desc "Builds a detail page data structure for a single attribute (sx-get): title, wire ID, handler status."
@@ -226,7 +226,7 @@
(div (str "wire-id: " (or (get attr-result "attr-wire-id") "none")))
(div (str "has handler: " (if (get attr-result "attr-handler") "yes" "no"))))
(~demo-result-card
(~page-helpers-demo/demo-result-card
:title "build-component-source"
:ms comp-ms :theme "stone"
:desc "Reconstructs a defcomp source definition from a component metadata dict (name, params, body)."
@@ -257,7 +257,7 @@
"Client Results "
(span :class "text-sm font-normal text-stone-500" "(JavaScript, sx-browser.js)"))
(~demo-client-runner
(~page-helpers-demo/demo-client-runner
:sf-source sf-source
:attr-detail attr-detail
:req-attrs req-attrs

View File

@@ -2,8 +2,8 @@
;; Art DAG on SX — SX endpoints as portals into media processing environments
;; ---------------------------------------------------------------------------
(defcomp ~plan-art-dag-sx-content ()
(~doc-page :title "Art DAG on SX"
(defcomp ~plans/art-dag-sx/plan-art-dag-sx-content ()
(~docs/page :title "Art DAG on SX"
(p :class "text-stone-500 text-sm italic mb-8"
"An SX endpoint is a portal into an execution environment. The URL is the entry point; the boundary declaration determines what world you enter.")
@@ -12,7 +12,7 @@
;; I. The endpoint as portal
;; =====================================================================
(~doc-section :title "The endpoint as portal" :id "endpoint-as-portal"
(~docs/section :title "The endpoint as portal" :id "endpoint-as-portal"
(p "An SX endpoint isn't a static route handler. It's a portal into an execution environment. A blog endpoint has " (code "query-posts") ", " (code "render-markdown") ". An art-dag endpoint has " (code "fetch-recipe") ", " (code "resolve-cid") ", " (code "gpu-exec") ", " (code "encode-stream") ", " (code "open-feed") ". Same evaluator. Different primitives. Different capabilities.")
(p "The URL is the entry point. The boundary declaration determines what world you enter. When you hit " (code "/art/render/Qm...") ", the evaluator boots with media-processing primitives. When you hit " (code "/blog/post/hello") ", the evaluator boots with content primitives. The SX code looks the same either way " (em "- ") "function calls, let bindings, conditionals. But the set of primitives available changes everything about what the program can do.")
(p "This is the same principle as the browser/server split. The browser has " (code "render-to-dom") " and " (code "signal") ". The server has " (code "query-db") " and " (code "render-to-html") ". Neither is a restricted version of the other " (em "- ") "they are different environments with different capabilities. The art-dag environment is a third world: " (code "gpu-exec") ", " (code "resolve-cid") ", " (code "encode-stream") ". Same language. Different universe."))
@@ -21,9 +21,9 @@
;; II. Recipes as SX
;; =====================================================================
(~doc-section :title "Recipes as SX" :id "recipes-as-sx"
(~docs/section :title "Recipes as SX" :id "recipes-as-sx"
(p "Art DAG recipes are already s-expression effect chains. Currently executed by L1 Celery workers via Python. The SX version: recipes " (em "are") " SX programs. They evaluate in an environment that has media processing primitives. A recipe doesn't \"call\" a GPU function " (em "- ") "it evaluates in an environment where " (code "gpu-exec") " is a primitive.")
(~doc-code :code (highlight ";; A recipe is an SX program in a media-processing environment\n(define composite-layers\n (fn (base-cid overlay-cid blend)\n (let ((base (resolve-cid base-cid))\n (overlay (resolve-cid overlay-cid)))\n (gpu-exec :op \"composite\"\n :layers (list base overlay)\n :blend blend\n :output :stream))))" "lisp"))
(~docs/code :code (highlight ";; A recipe is an SX program in a media-processing environment\n(define composite-layers\n (fn (base-cid overlay-cid blend)\n (let ((base (resolve-cid base-cid))\n (overlay (resolve-cid overlay-cid)))\n (gpu-exec :op \"composite\"\n :layers (list base overlay)\n :blend blend\n :output :stream))))" "lisp"))
(p "This isn't a DSL embedded in Python. It's SX, the same language that renders pages and defines components. The recipe author uses " (code "let") ", " (code "fn") ", " (code "map") ", " (code "if") " " (em "- ") "the full language. The only difference is what primitives are available. " (code "resolve-cid") " fetches content-addressed data. " (code "gpu-exec") " dispatches GPU operations. These are primitives, not library calls. They exist in the environment the same way " (code "+") " and " (code "str") " exist.")
(p "The recipe is data. It's an s-expression. You can parse it, analyze it, transform it, serialize it, hash it, store it, transmit it. You can inspect a recipe's dependency graph the same way " (code "deps.sx") " inspects component dependencies. You can type-check a recipe the same way " (code "typed-sx") " type-checks components. The tools already exist. They just need a new set of primitives to reason about."))
@@ -31,9 +31,9 @@
;; III. Split execution
;; =====================================================================
(~doc-section :title "Split execution" :id "split-execution"
(~docs/section :title "Split execution" :id "split-execution"
(p "Some recipe steps run against cached data (fast, local). Others need GPU. Others need live input. The evaluator doesn't dispatch " (em "- ") "the boundary declarations do. " (code "(with-boundary (gpu-compute) ...)") " migrates to a GPU-capable host. " (code "(with-boundary (live-ingest) ...)") " opens WebRTC feeds. The recipe author doesn't manage infrastructure " (em "- ") "they declare capabilities, and execution flows to where they exist.")
(~doc-code :code (highlight "(define live-composite\n (fn (recipe-cid camera-count)\n (let ((recipe (resolve-cid recipe-cid)))\n ;; Phase 1: cached data (local, fast)\n (let ((base-layers (map resolve-cid (get recipe \"layers\"))))\n ;; Phase 2: GPU processing\n (with-boundary (gpu-compute)\n (let ((composed (gpu-exec :op \"composite\"\n :layers base-layers\n :blend (get recipe \"blend\"))))\n ;; Phase 3: live feeds\n (with-boundary (live-ingest)\n (let ((feeds (map (fn (i)\n (open-feed :protocol \"webrtc\"\n :label (str \"camera-\" i)))\n (range 0 camera-count))))\n ;; Phase 4: encode and stream\n (with-boundary (encoding)\n (encode-stream\n :sources (concat (list composed) feeds)\n :codec \"h264\"\n :output :stream))))))))))" "lisp"))
(~docs/code :code (highlight "(define live-composite\n (fn (recipe-cid camera-count)\n (let ((recipe (resolve-cid recipe-cid)))\n ;; Phase 1: cached data (local, fast)\n (let ((base-layers (map resolve-cid (get recipe \"layers\"))))\n ;; Phase 2: GPU processing\n (with-boundary (gpu-compute)\n (let ((composed (gpu-exec :op \"composite\"\n :layers base-layers\n :blend (get recipe \"blend\"))))\n ;; Phase 3: live feeds\n (with-boundary (live-ingest)\n (let ((feeds (map (fn (i)\n (open-feed :protocol \"webrtc\"\n :label (str \"camera-\" i)))\n (range 0 camera-count))))\n ;; Phase 4: encode and stream\n (with-boundary (encoding)\n (encode-stream\n :sources (concat (list composed) feeds)\n :codec \"h264\"\n :output :stream))))))))))" "lisp"))
(p "Four phases. Four capability requirements. The program reads linearly " (em "- ") "resolve cached layers, composite on GPU, open live feeds, encode output. But execution migrates across hosts as needed. The " (code "with-boundary") " blocks are the seams. Everything inside a boundary block runs on a host that provides those capabilities. Everything outside runs wherever the program started.")
(p "This is the same mechanism described in the generative SX plan's environment migration section. " (code "with-boundary") " serializes the environment (" (code "env-snapshot") "), ships the pending expression to a capable host, and execution continues there. The recipe author writes a linear program. The runtime makes it distributed."))
@@ -41,7 +41,7 @@
;; IV. Content-addressed everything
;; =====================================================================
(~doc-section :title "Content-addressed everything" :id "content-addressed"
(~docs/section :title "Content-addressed everything" :id "content-addressed"
(p "Recipes are CIDs. Effects are CIDs. Intermediate frames are content-addressed. The execution DAG is a content-addressed graph " (em "- ") "every step can be verified, cached, or replayed.")
(p "A composite of three layers with a specific blend mode always produces the same CID. Caching is automatic: if the CID exists locally, skip the computation. This is the Art DAG's existing model " (em "- ") "SHA3-256 hashes identify everything. SX makes it explicit: the recipe source itself is content-addressed. Two users who write the same recipe get the same CID. They're running the same program.")
(div :class "rounded border border-stone-200 bg-stone-50 p-4 my-4"
@@ -53,19 +53,19 @@
;; V. Feed generation
;; =====================================================================
(~doc-section :title "Feed generation" :id "feed-generation"
(~docs/section :title "Feed generation" :id "feed-generation"
(p "The executing program creates new endpoints as a side effect. " (code "(open-feed ...)") " doesn't return data " (em "- ") "it returns a connectable endpoint. WebRTC peers, SSE streams, WebSocket channels. These are generative acts: the endpoint didn't exist before the recipe ran. The program grew its own input surface.")
(p "When the recipe completes or the island disposes, feeds are cleaned up via the disposal mechanism. An " (code "effect") " in island scope that opens a feed returns a cleanup function. When the island unmounts, the cleanup runs, the feed closes, the endpoint disappears. The lifecycle is automatic.")
(~doc-code :code (highlight ";; A feed is a connectable endpoint, not raw data\n(define create-camera-mosaic\n (fn (camera-ids)\n ;; Each open-feed returns a connectable URL, not bytes\n (let ((feeds (map (fn (id)\n (open-feed :protocol \"webrtc\"\n :label (str \"cam-\" id)\n :quality \"720p\"))\n camera-ids)))\n ;; The mosaic recipe composes feeds as inputs\n (gpu-exec :op \"mosaic\"\n :inputs feeds\n :layout \"grid\"\n :output (open-feed :protocol \"sse\"\n :label \"mosaic-output\"\n :format \"mjpeg\")))))" "lisp"))
(~docs/code :code (highlight ";; A feed is a connectable endpoint, not raw data\n(define create-camera-mosaic\n (fn (camera-ids)\n ;; Each open-feed returns a connectable URL, not bytes\n (let ((feeds (map (fn (id)\n (open-feed :protocol \"webrtc\"\n :label (str \"cam-\" id)\n :quality \"720p\"))\n camera-ids)))\n ;; The mosaic recipe composes feeds as inputs\n (gpu-exec :op \"mosaic\"\n :inputs feeds\n :layout \"grid\"\n :output (open-feed :protocol \"sse\"\n :label \"mosaic-output\"\n :format \"mjpeg\")))))" "lisp"))
(p "The output is itself a feed. A client connects to the mosaic output URL and receives composed frames. The feeds are the program's I/O surface " (em "- ") "they exist because the program created them, and they die when the program stops. No static route configuration. No service mesh. The program declares what it needs and creates what it produces."))
;; =====================================================================
;; VI. The client boundary
;; =====================================================================
(~doc-section :title "The client boundary" :id "client-boundary"
(~docs/section :title "The client boundary" :id "client-boundary"
(p "The browser is just another execution environment with its own primitive set: " (code "render-to-dom") ", " (code "signal") ", " (code "deref") ", " (code "open-feed") " (as WebRTC consumer). A streaming art-dag response arrives as SX wire format. The client evaluates it in island scope " (em "- ") "signals bind to stream frames, " (code "reactive-list") " renders feed thumbnails, " (code "computed") " derives overlay parameters. The server pushes frames; the client renders them reactively. No special video player " (em "- ") "just signals and DOM.")
(~doc-code :code (highlight "(defisland ~live-canvas ()\n (let ((frames (signal nil))\n (feed-url (signal nil)))\n ;; Connect to stream when URL arrives\n (effect (fn ()\n (when (deref feed-url)\n (connect-stream (deref feed-url)\n :on-frame (fn (f) (reset! frames f))))))\n (div :class \"relative aspect-video bg-black rounded\"\n (when (deref frames)\n (canvas :width 1920 :height 1080\n :draw (fn (ctx)\n (draw-frame ctx (deref frames)))))\n (when (not (deref frames))\n (p :class \"absolute inset-0 flex items-center justify-center text-white/50\"\n \"Waiting for stream...\")))))" "lisp"))
(~docs/code :code (highlight "(defisland ~plans/art-dag-sx/live-canvas ()\n (let ((frames (signal nil))\n (feed-url (signal nil)))\n ;; Connect to stream when URL arrives\n (effect (fn ()\n (when (deref feed-url)\n (connect-stream (deref feed-url)\n :on-frame (fn (f) (reset! frames f))))))\n (div :class \"relative aspect-video bg-black rounded\"\n (when (deref frames)\n (canvas :width 1920 :height 1080\n :draw (fn (ctx)\n (draw-frame ctx (deref frames)))))\n (when (not (deref frames))\n (p :class \"absolute inset-0 flex items-center justify-center text-white/50\"\n \"Waiting for stream...\")))))" "lisp"))
(p "The island is reactive. When " (code "frames") " updates, only the canvas redraws. When " (code "feed-url") " updates, the effect reconnects. No polling loop. No WebSocket message handler parsing JSON. The stream is a signal source. The DOM is a signal consumer. The reactive graph connects them.")
(p "This is the same island architecture from the reactive islands plan " (em "- ") "signals, effects, computed, disposal. The only difference is the data source. Instead of an HTMX response mutating a signal, a WebRTC stream mutates a signal. The rendering pipeline doesn't know or care where the data comes from. It reacts."))
@@ -73,17 +73,17 @@
;; VII. L1/L2 integration
;; =====================================================================
(~doc-section :title "L1/L2 integration" :id "l1-l2"
(~docs/section :title "L1/L2 integration" :id "l1-l2"
(p "L1 is the compute layer (Celery workers, GPU nodes). L2 is the registry (ActivityPub, recipe discovery). In SX terms: L1 hosts provide " (code "gpu-exec") ", " (code "encode-stream") ", " (code "resolve-cid-local") ". L2 hosts provide " (code "discover-recipe") ", " (code "publish-recipe") ", " (code "federate-activity") ".")
(p "An SX program that needs both crosses boundaries as needed " (em "- ") "fetch recipe metadata from L2, execute it on L1, publish results back to L2.")
(~doc-code :code (highlight ";; A full pipeline crossing L1 and L2 boundaries\n(define render-and-publish\n (fn (recipe-name output-label)\n ;; L2: discover the recipe\n (with-boundary (registry)\n (let ((recipe-cid (discover-recipe :name recipe-name\n :version \"latest\")))\n ;; L1: execute the recipe\n (with-boundary (gpu-compute)\n (let ((result-cid (gpu-exec :recipe (resolve-cid recipe-cid)\n :output :cid)))\n ;; L2: publish the result\n (with-boundary (registry)\n (publish-recipe\n :name output-label\n :input-cid recipe-cid\n :output-cid result-cid\n :activity \"Create\"))))))))" "lisp"))
(~docs/code :code (highlight ";; A full pipeline crossing L1 and L2 boundaries\n(define render-and-publish\n (fn (recipe-name output-label)\n ;; L2: discover the recipe\n (with-boundary (registry)\n (let ((recipe-cid (discover-recipe :name recipe-name\n :version \"latest\")))\n ;; L1: execute the recipe\n (with-boundary (gpu-compute)\n (let ((result-cid (gpu-exec :recipe (resolve-cid recipe-cid)\n :output :cid)))\n ;; L2: publish the result\n (with-boundary (registry)\n (publish-recipe\n :name output-label\n :input-cid recipe-cid\n :output-cid result-cid\n :activity \"Create\"))))))))" "lisp"))
(p "Three boundary crossings. L2 to find the recipe. L1 to execute it. L2 to publish the result. The program reads as a linear sequence of operations. The runtime handles the dispatch " (em "- ") "which host provides " (code "discover-recipe") ", which host provides " (code "gpu-exec") ", which host provides " (code "publish-recipe") ". The program author doesn't configure endpoints or manage connections. They declare capabilities."))
;; =====================================================================
;; VIII. The primitive sets
;; =====================================================================
(~doc-section :title "The primitive sets" :id "primitive-sets"
(~docs/section :title "The primitive sets" :id "primitive-sets"
(p "Each execution environment provides its own set of primitives. The language is the same everywhere. The capabilities differ.")
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
(table :class "w-full text-left text-sm"

View File

@@ -2,10 +2,10 @@
;; Async Evaluator Convergence — Bootstrap async_eval.py from Spec
;; ---------------------------------------------------------------------------
(defcomp ~plan-async-eval-convergence-content ()
(~doc-page :title "Async Evaluator Convergence"
(defcomp ~plans/async-eval-convergence/plan-async-eval-convergence-content ()
(~docs/page :title "Async Evaluator Convergence"
(~doc-section :title "The Problem" :id "problem"
(~docs/section :title "The Problem" :id "problem"
(p "There are currently " (strong "three") " lambda call implementations that must be kept in sync:")
(ol :class "list-decimal list-inside space-y-2 mt-2"
(li (code "shared/sx/ref/eval.sx") " — the canonical spec, bootstrapped to " (code "sx-ref.js") " and " (code "sx_ref.py"))
@@ -14,7 +14,7 @@
(p "Every semantic change to the evaluator — lenient lambda arity, new special forms, calling convention tweaks — must be applied to all three. The spec is authoritative but " (code "async_eval.py") " is what actually serves pages. This is a maintenance hazard and a source of subtle divergence bugs.")
(p "The lenient arity change (lambda params pad missing args with nil instead of erroring) exposed this: the spec and sync evaluator were updated, but " (code "async_eval.py") " still had strict arity checking, causing production crashes."))
(~doc-section :title "Why async_eval.py Exists" :id "why"
(~docs/section :title "Why async_eval.py Exists" :id "why"
(p "The async evaluator exists because SX page rendering needs to:")
(ul :class "list-disc list-inside space-y-2 mt-2"
(li (strong "Await IO primitives") " — page helpers like " (code "highlight") ", " (code "reference-data") ", " (code "component-source") " call Python async functions (DB queries, HTTP fetches). The spec evaluator is synchronous.")
@@ -26,7 +26,7 @@
;; Architecture
;; -----------------------------------------------------------------------
(~doc-section :title "Target Architecture" :id "architecture"
(~docs/section :title "Target Architecture" :id "architecture"
(p "The goal is to " (strong "eliminate hand-written evaluator code entirely") ". All evaluation semantics come from the spec via bootstrapping. The host provides only:")
(ul :class "list-disc list-inside space-y-2 mt-2"
(li (strong "Platform primitives") " — type constructors, env operations, DOM/HTML primitives")
@@ -38,17 +38,17 @@
;; Approach: Async Adapter Layer
;; -----------------------------------------------------------------------
(~doc-section :title "Approach: Async Adapter Layer" :id "approach"
(~docs/section :title "Approach: Async Adapter Layer" :id "approach"
(p "Rather than making the spec itself async (which would pollute it with Python-specific concerns), introduce a thin adapter layer between the bootstrapped evaluator and the IO boundary:")
(h4 :class "font-semibold mt-4 mb-2" "Phase 1 — Async call hook")
(p "The bootstrapped evaluator calls primitives via " (code "apply(fn, args)") ". In the Python host, " (code "apply") " is a platform primitive. Replace it with an async-aware version:")
(~doc-code :code (highlight "(define apply-fn\n (fn (f args)\n ;; Platform provides: if f returns a coroutine, await it\n (apply-maybe-async f args)))" "lisp"))
(~docs/code :code (highlight "(define apply-fn\n (fn (f args)\n ;; Platform provides: if f returns a coroutine, await it\n (apply-maybe-async f args)))" "lisp"))
(p "The bootstrapper emits " (code "apply_maybe_async") " as a Python " (code "async def") " that checks if the result is a coroutine and awaits it if so. Pure functions return immediately. IO primitives return coroutines that get awaited. " (strong "Zero overhead for pure calls") " — just an " (code "isinstance") " check.")
(h4 :class "font-semibold mt-4 mb-2" "Phase 2 — Async trampoline")
(p "The spec's trampoline loop resolves thunks synchronously. The Python bootstrapper emits an " (code "async def trampoline") " variant that can await thunks whose bodies contain IO calls. The trampoline structure is identical — only the " (code "await") " keyword is added.")
(~doc-code :code (highlight "# Bootstrapper emits this for Python async target\nasync def trampoline(val):\n while isinstance(val, Thunk):\n val = await eval_expr(val.expr, val.env)\n return val" "python"))
(~docs/code :code (highlight "# Bootstrapper emits this for Python async target\nasync def trampoline(val):\n while isinstance(val, Thunk):\n val = await eval_expr(val.expr, val.env)\n return val" "python"))
(h4 :class "font-semibold mt-4 mb-2" "Phase 3 — Aser as spec module")
(p "The " (code "_aser") " rendering mode (evaluate control flow, serialize HTML/components as SX source) should be specced as a module in " (code "render.sx") " alongside " (code "render-to-html") " and " (code "render-to-dom") ". It's currently hand-written Python because it predates the spec, but its logic is pure SX: walk the AST, eval certain forms, serialize others.")
@@ -61,7 +61,7 @@
;; What changes in the bootstrapper
;; -----------------------------------------------------------------------
(~doc-section :title "Bootstrapper Changes" :id "bootstrapper"
(~docs/section :title "Bootstrapper Changes" :id "bootstrapper"
(p "The Python bootstrapper (" (code "bootstrap_py.py") ") gains a new emit mode: " (code "--async") ". This emits:")
(ul :class "list-disc list-inside space-y-2 mt-2"
(li (code "async def eval_expr") " instead of " (code "def eval_expr"))
@@ -74,7 +74,7 @@
;; Migration path
;; -----------------------------------------------------------------------
(~doc-section :title "Migration Path" :id "migration"
(~docs/section :title "Migration Path" :id "migration"
(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"
@@ -107,14 +107,14 @@
;; Principles
;; -----------------------------------------------------------------------
(~doc-section :title "Principles" :id "principles"
(~docs/section :title "Principles" :id "principles"
(ul :class "list-disc list-inside space-y-2"
(li (strong "The spec is the single source of truth.") " All SX evaluation semantics live in .sx files. Host code implements platform primitives, not evaluation rules.")
(li (strong "Async is a host concern, not a language concern.") " The spec is synchronous. The Python bootstrapper emits async wrappers. The JS bootstrapper emits sync code. The spec doesn't know or care.")
(li (strong "Shadow-compare before switching.") " Every migration step runs both paths in parallel and asserts identical output. No big-bang cutover.")
(li (strong "Aser is just another render mode.") " It belongs in render.sx alongside render-to-html and render-to-dom. It's not special — it's the 'evaluate some, serialize the rest' mode.")))
(~doc-section :title "Outcome" :id "outcome"
(~docs/section :title "Outcome" :id "outcome"
(p "After convergence:")
(ul :class "list-disc list-inside space-y-2 mt-2"
(li "One evaluator implementation (the spec), bootstrapped to every host")

View File

@@ -2,10 +2,10 @@
;; Content-Addressed Components
;; ---------------------------------------------------------------------------
(defcomp ~plan-content-addressed-components-content ()
(~doc-page :title "Content-Addressed Components"
(defcomp ~plans/content-addressed-components/plan-content-addressed-components-content ()
(~docs/page :title "Content-Addressed Components"
(~doc-section :title "The Premise" :id "premise"
(~docs/section :title "The Premise" :id "premise"
(p "SX components are pure functions. Boundary enforcement guarantees it — a component cannot call IO primitives, make network requests, access cookies, or touch the filesystem. " (code "Component.is_pure") " is a structural property, verified at registration time by scanning the transitive closure of IO references via " (code "deps.sx") ".")
(p "Pure functions have a remarkable property: " (strong "their identity is their content.") " Two components that produce the same serialized form are the same component, regardless of who wrote them or where they're hosted. This means we can content-address them — compute a cryptographic hash of the canonical serialized form, and that hash " (em "is") " the component's identity.")
(p "Content addressing turns components into shared infrastructure. Define " (code "~card") " once, pin it to IPFS, and every SX application on the planet can use it by CID. No package registry, no npm install, no version conflicts. The CID " (em "is") " the version. The hash " (em "is") " the trust. Boundary enforcement " (em "is") " the sandbox.")
@@ -15,7 +15,7 @@
;; Current State
;; -----------------------------------------------------------------------
(~doc-section :title "Current State" :id "current-state"
(~docs/section :title "Current State" :id "current-state"
(p "What already exists and what's missing.")
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
@@ -62,28 +62,28 @@
;; Canonical Serialization
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 1: Canonical Serialization" :id "canonical-serialization"
(~docs/section :title "Phase 1: Canonical Serialization" :id "canonical-serialization"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "The foundation")
(p :class "text-violet-800" "Same component must always produce the same bytes, regardless of original formatting, whitespace, or comment placement. Without this, content addressing is meaningless."))
(~doc-subsection :title "The Problem"
(~docs/subsection :title "The Problem"
(p "Currently " (code "serialize(body, pretty=True)") " produces readable SX source from the parsed AST. But serialization isn't fully canonical — it depends on the internal representation order, and there's no normalization pass. Two semantically identical components formatted differently would produce different hashes.")
(p "We need a " (strong "canonical form") " that strips all variance:"))
(~doc-subsection :title "Canonical Form Rules"
(~docs/subsection :title "Canonical Form Rules"
(ol :class "list-decimal pl-5 text-stone-700 space-y-2"
(li (strong "Strip comments.") " Comments are parsing artifacts, not part of the AST. The serializer already ignores them (it works from the parsed tree), but any future comment-preserving parser must not affect canonical output.")
(li (strong "Normalize whitespace.") " Single space between tokens, newline before each top-level form in a body. No trailing whitespace. No blank lines.")
(li (strong "Sort keyword arguments alphabetically.") " In component calls: " (code "(~card :class \"x\" :title \"y\")") " not " (code "(~card :title \"y\" :class \"x\")") ". In dict literals: " (code "{:a 1 :b 2}") " not " (code "{:b 2 :a 1}") ".")
(li (strong "Normalize string escapes.") " Use " (code "\\n") " not literal newlines in strings. Escape only what must be escaped.")
(li (strong "Normalize numbers.") " " (code "1.0") " not " (code "1.00") " or " (code "1.") ". " (code "42") " not " (code "042") ".")
(li (strong "Include the full definition form.") " Hash the complete " (code "(defcomp ~name (params) body)") ", not just the body. The name and parameter signature are part of the component's identity.")))
(li (strong "Include the full definition form.") " Hash the complete " (code "(defcomp ~plans/content-addressed-components/name (params) body)") ", not just the body. The name and parameter signature are part of the component's identity.")))
(~doc-subsection :title "Implementation"
(~docs/subsection :title "Implementation"
(p "New spec function in a " (code "canonical.sx") " module:")
(~doc-code :code (highlight "(define canonical-serialize\n (fn (node)\n ;; Produce a canonical s-expression string from an AST node.\n ;; Deterministic: same AST always produces same output.\n ;; Used for CID computation — NOT for human-readable output.\n (case (type-of node)\n \"list\"\n (str \"(\" (join \" \" (map canonical-serialize node)) \")\")\n \"dict\"\n (let ((sorted-keys (sort (keys node))))\n (str \"{\" (join \" \"\n (map (fn (k)\n (str \":\" k \" \" (canonical-serialize (get node k))))\n sorted-keys)) \"}\"))\n \"string\"\n (str '\"' (escape-canonical node) '\"')\n \"number\"\n (canonical-number node)\n \"symbol\"\n (symbol-name node)\n \"keyword\"\n (str \":\" (keyword-name node))\n \"boolean\"\n (if node \"true\" \"false\")\n \"nil\"\n \"nil\")))" "lisp"))
(~docs/code :code (highlight "(define canonical-serialize\n (fn (node)\n ;; Produce a canonical s-expression string from an AST node.\n ;; Deterministic: same AST always produces same output.\n ;; Used for CID computation — NOT for human-readable output.\n (case (type-of node)\n \"list\"\n (str \"(\" (join \" \" (map canonical-serialize node)) \")\")\n \"dict\"\n (let ((sorted-keys (sort (keys node))))\n (str \"{\" (join \" \"\n (map (fn (k)\n (str \":\" k \" \" (canonical-serialize (get node k))))\n sorted-keys)) \"}\"))\n \"string\"\n (str '\"' (escape-canonical node) '\"')\n \"number\"\n (canonical-number node)\n \"symbol\"\n (symbol-name node)\n \"keyword\"\n (str \":\" (keyword-name node))\n \"boolean\"\n (if node \"true\" \"false\")\n \"nil\"\n \"nil\")))" "lisp"))
(p "This function must be bootstrapped to both Python and JS — the server computes CIDs at registration time, the client verifies them on fetch.")
(p "The canonical serializer is distinct from " (code "serialize()") " for display. " (code "serialize(pretty=True)") " remains for human-readable output. " (code "canonical-serialize") " is for hashing only.")))
@@ -91,26 +91,26 @@
;; CID Computation
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 2: CID Computation" :id "cid-computation"
(~docs/section :title "Phase 2: CID Computation" :id "cid-computation"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What it enables")
(p :class "text-violet-800" "Every component gets a stable, unique content identifier. Same source → same CID, always. Different source → different CID, always."))
(~doc-subsection :title "CID Format"
(~docs/subsection :title "CID Format"
(p "Use " (a :href "https://github.com/multiformats/cid" :class "text-violet-700 underline" "CIDv1") " with:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "Hash function:") " SHA3-256 (already used by artdag for content addressing)")
(li (strong "Codec:") " raw (the content is the canonical SX source bytes, not a DAG-PB wrapper)")
(li (strong "Base encoding:") " base32lower for URL-safe representation (" (code "bafy...") " prefix)"))
(~doc-code :code (highlight ";; CID computation pipeline\n(define component-cid\n (fn (component)\n ;; 1. Reconstruct full defcomp form\n ;; 2. Canonical serialize\n ;; 3. SHA3-256 hash\n ;; 4. Wrap as CIDv1\n (let ((source (canonical-serialize\n (list 'defcomp\n (symbol (str \"~\" (component-name component)))\n (component-params-list component)\n (component-body component)))))\n (cid-v1 :sha3-256 :raw (encode-utf8 source)))))" "lisp")))
(~docs/code :code (highlight ";; CID computation pipeline\n(define component-cid\n (fn (component)\n ;; 1. Reconstruct full defcomp form\n ;; 2. Canonical serialize\n ;; 3. SHA3-256 hash\n ;; 4. Wrap as CIDv1\n (let ((source (canonical-serialize\n (list 'defcomp\n (symbol (str \"~\" (component-name component)))\n (component-params-list component)\n (component-body component)))))\n (cid-v1 :sha3-256 :raw (encode-utf8 source)))))" "lisp")))
(~doc-subsection :title "Where CIDs Live"
(~docs/subsection :title "Where CIDs Live"
(p "Each " (code "Component") " object gains a " (code "cid") " field, computed at registration time:")
(~doc-code :code (highlight ";; types.py extension\n@dataclass\nclass Component:\n name: str\n params: list[str]\n has_children: bool\n body: Any\n closure: dict[str, Any]\n css_classes: set[str]\n deps: set[str] # by name\n io_refs: set[str]\n cid: str | None = None # computed after registration\n dep_cids: dict[str, str] | None = None # name → CID" "python"))
(~docs/code :code (highlight ";; types.py extension\n@dataclass\nclass Component:\n name: str\n params: list[str]\n has_children: bool\n body: Any\n closure: dict[str, Any]\n css_classes: set[str]\n deps: set[str] # by name\n io_refs: set[str]\n cid: str | None = None # computed after registration\n dep_cids: dict[str, str] | None = None # name → CID" "python"))
(p "After " (code "compute_all_deps()") " runs, a new " (code "compute_all_cids()") " pass fills in CIDs for every component. Dependency CIDs are also recorded — when a component references " (code "~card") ", we store both the name and card's CID."))
(~doc-subsection :title "CID Stability"
(~docs/subsection :title "CID Stability"
(p "A component's CID changes when and only when its " (strong "semantics") " change:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Reformatting the " (code ".sx") " source file → same AST → same canonical form → " (strong "same CID"))
@@ -123,14 +123,14 @@
;; Component Manifest
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 3: Component Manifest" :id "manifest"
(~docs/section :title "Phase 3: Component Manifest" :id "manifest"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What it enables")
(p :class "text-violet-800" "Metadata that travels with a CID — what a component needs, what it provides, whether it's safe to run. Enough information to resolve, validate, and render without fetching the source first."))
(~doc-subsection :title "Manifest Structure"
(~doc-code :code (highlight ";; Component manifest — published alongside the source\n(SxComponent\n :name \"~product-card\"\n :cid \"bafy...productcard\"\n :source-bytes 847\n :params (:title :price :image-url)\n :has-children true\n :pure true\n :deps (\n {:name \"~card\" :cid \"bafy...card\"}\n {:name \"~price-tag\" :cid \"bafy...pricetag\"}\n {:name \"~lazy-image\" :cid \"bafy...lazyimg\"})\n :css-atoms (:border :rounded :p-4 :text-sm :font-bold\n :text-green-700 :line-through :text-stone-400)\n :author \"https://rose-ash.com/apps/market\"\n :published \"2026-03-06T14:30:00Z\")" "lisp"))
(~docs/subsection :title "Manifest Structure"
(~docs/code :code (highlight ";; Component manifest — published alongside the source\n(SxComponent\n :name \"~product-card\"\n :cid \"bafy...productcard\"\n :source-bytes 847\n :params (:title :price :image-url)\n :has-children true\n :pure true\n :deps (\n {:name \"~card\" :cid \"bafy...card\"}\n {:name \"~price-tag\" :cid \"bafy...pricetag\"}\n {:name \"~lazy-image\" :cid \"bafy...lazyimg\"})\n :css-atoms (:border :rounded :p-4 :text-sm :font-bold\n :text-green-700 :line-through :text-stone-400)\n :author \"https://rose-ash.com/apps/market\"\n :published \"2026-03-06T14:30:00Z\")" "lisp"))
(p "Key fields:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code ":cid") " — content address of the canonical serialized source")
@@ -140,7 +140,7 @@
(li (code ":params") " — parameter signature for tooling, documentation, IDE support")
(li (code ":author") " — who published this. AP actor URL, verifiable via HTTP Signatures")))
(~doc-subsection :title "Manifest CID"
(~docs/subsection :title "Manifest CID"
(p "The manifest itself is content-addressed. But the manifest CID is " (em "not") " the component CID — they're separate objects. The component CID is derived from the source alone (pure content). The manifest CID includes metadata that could change (author, publication date) without changing the component.")
(p "Resolution order: manifest CID → manifest → component CID → component source. Or shortcut: component CID → source directly, if you already know what you need.")))
@@ -148,13 +148,13 @@
;; IPFS Storage & Resolution
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 4: IPFS Storage & Resolution" :id "ipfs"
(~docs/section :title "Phase 4: IPFS Storage & Resolution" :id "ipfs"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What it enables")
(p :class "text-violet-800" "Components live on IPFS. Any browser can fetch them by CID. No origin server needed. No CDN. No DNS. The content network IS the distribution network."))
(~doc-subsection :title "Server-Side: Publication"
(~docs/subsection :title "Server-Side: Publication"
(p "On component registration (startup or hot-reload), the server:")
(ol :class "list-decimal pl-5 text-stone-700 space-y-1"
(li "Computes canonical form and CID")
@@ -163,14 +163,14 @@
(li "Creates/updates " (code "IPFSPin") " record with " (code "pin_type=\"component\""))
(li "Publishes manifest to IPFS (separate CID)")
(li "Optionally announces via AP outbox for federated discovery"))
(~doc-code :code (highlight ";; IPFSPin usage for components\nIPFSPin(\n content_hash=\"sha3-256:abcdef...\",\n ipfs_cid=\"bafy...productcard\",\n pin_type=\"component\",\n source_type=\"market\", # which service defined it\n metadata={\n \"name\": \"~product-card\",\n \"manifest_cid\": \"bafy...manifest\",\n \"deps\": [\"bafy...card\", \"bafy...pricetag\"],\n \"pure\": True\n }\n)" "python")))
(~docs/code :code (highlight ";; IPFSPin usage for components\nIPFSPin(\n content_hash=\"sha3-256:abcdef...\",\n ipfs_cid=\"bafy...productcard\",\n pin_type=\"component\",\n source_type=\"market\", # which service defined it\n metadata={\n \"name\": \"~product-card\",\n \"manifest_cid\": \"bafy...manifest\",\n \"deps\": [\"bafy...card\", \"bafy...pricetag\"],\n \"pure\": True\n }\n)" "python")))
(~doc-subsection :title "Client-Side: Resolution"
(~docs/subsection :title "Client-Side: Resolution"
(p "New spec module " (code "resolve.sx") " — the client-side component resolution pipeline:")
(~doc-code :code (highlight "(define resolve-component-by-cid\n (fn (cid callback)\n ;; Resolution cascade:\n ;; 1. Check component env (already loaded?)\n ;; 2. Check localStorage (keyed by CID = cache-forever)\n ;; 3. Check origin server (/sx/components?cid=bafy...)\n ;; 4. Fetch from IPFS gateway\n ;; 5. Verify hash matches CID\n ;; 6. Parse, validate purity, register, callback\n (let ((cached (local-storage-get (str \"sx-cid:\" cid))))\n (if cached\n (do\n (register-component-source cached)\n (callback true))\n (fetch-component-by-cid cid\n (fn (source)\n (if (verify-cid cid source)\n (do\n (local-storage-set (str \"sx-cid:\" cid) source)\n (register-component-source source)\n (callback true))\n (do\n (log-warn (str \"sx:cid verification failed \" cid))\n (callback false)))))))))" "lisp"))
(~docs/code :code (highlight "(define resolve-component-by-cid\n (fn (cid callback)\n ;; Resolution cascade:\n ;; 1. Check component env (already loaded?)\n ;; 2. Check localStorage (keyed by CID = cache-forever)\n ;; 3. Check origin server (/sx/components?cid=bafy...)\n ;; 4. Fetch from IPFS gateway\n ;; 5. Verify hash matches CID\n ;; 6. Parse, validate purity, register, callback\n (let ((cached (local-storage-get (str \"sx-cid:\" cid))))\n (if cached\n (do\n (register-component-source cached)\n (callback true))\n (fetch-component-by-cid cid\n (fn (source)\n (if (verify-cid cid source)\n (do\n (local-storage-set (str \"sx-cid:\" cid) source)\n (register-component-source source)\n (callback true))\n (do\n (log-warn (str \"sx:cid verification failed \" cid))\n (callback false)))))))))" "lisp"))
(p "The cache-forever semantics are the key insight: because CIDs are content-addressed, a cached component " (strong "can never be stale") ". If the source changes, it gets a new CID. Old CIDs remain valid forever. There is no cache invalidation problem."))
(~doc-subsection :title "Resolution Cascade"
(~docs/subsection :title "Resolution Cascade"
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-100"
@@ -210,22 +210,22 @@
;; Security Model
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 5: Security Model" :id "security"
(~docs/section :title "Phase 5: Security Model" :id "security"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "The hard part")
(p :class "text-violet-800" "Loading code from the network is the web's original sin. Content-addressed components are safe because of three structural guarantees — not policies, not trust, not sandboxes that can be escaped."))
(~doc-subsection :title "Guarantee 1: Purity is Structural"
(~docs/subsection :title "Guarantee 1: Purity is Structural"
(p "SX boundary enforcement isn't a runtime sandbox — it's a registration-time structural check. When a component is loaded from IPFS and parsed, " (code "compute_all_io_refs()") " walks its entire AST and transitive dependencies. If " (em "any") " node references an IO primitive, the component is classified as IO-dependent and " (strong "rejected for untrusted registration."))
(p "This means the evaluator literally doesn't have IO primitives in scope when running an IPFS-loaded component. It's not that we catch IO calls — the names don't resolve. There's nothing to catch.")
(~doc-code :code (highlight "(define register-untrusted-component\n (fn (source origin)\n ;; Parse the defcomp from source\n ;; Run compute-all-io-refs on the parsed component\n ;; If io_refs is non-empty → REJECT\n ;; If pure → register in env with :origin metadata\n (let ((comp (parse-component source)))\n (if (not (component-pure? comp))\n (do\n (log-warn (str \"sx:reject IO component from \" origin))\n nil)\n (do\n (register-component comp)\n (log-info (str \"sx:registered \" (component-name comp)\n \" from \" origin))\n comp)))))" "lisp")))
(~docs/code :code (highlight "(define register-untrusted-component\n (fn (source origin)\n ;; Parse the defcomp from source\n ;; Run compute-all-io-refs on the parsed component\n ;; If io_refs is non-empty → REJECT\n ;; If pure → register in env with :origin metadata\n (let ((comp (parse-component source)))\n (if (not (component-pure? comp))\n (do\n (log-warn (str \"sx:reject IO component from \" origin))\n nil)\n (do\n (register-component comp)\n (log-info (str \"sx:registered \" (component-name comp)\n \" from \" origin))\n comp)))))" "lisp")))
(~doc-subsection :title "Guarantee 2: Content Verification"
(~docs/subsection :title "Guarantee 2: Content Verification"
(p "The CID IS the hash. When you fetch " (code "bafy...abc") " from any source — IPFS gateway, origin server, peer — you hash the response and compare. If it doesn't match, you reject it. No MITM attack can alter the content without changing the CID.")
(p "This is stronger than HTTPS. HTTPS trusts the certificate authority, the DNS resolver, and the server operator. Content addressing trusts " (em "mathematics") ". The hash either matches or it doesn't."))
(~doc-subsection :title "Guarantee 3: Evaluation Limits"
(~docs/subsection :title "Guarantee 3: Evaluation Limits"
(p "Pure doesn't mean terminating. A component could contain an infinite loop or exponential recursion. SX evaluators enforce step limits:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "Max eval steps:") " configurable per context. Untrusted components get a lower limit than local ones.")
@@ -233,7 +233,7 @@
(li (strong "Max output size:") " prevents a component from producing gigabytes of DOM nodes."))
(p "Exceeding any limit halts evaluation and returns an error node. The worst case is wasted CPU — never data exfiltration, never unauthorized IO."))
(~doc-subsection :title "Trust Tiers"
(~docs/subsection :title "Trust Tiers"
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-100"
@@ -258,7 +258,7 @@
(td :class "px-3 py-2 text-stone-700" "Pure only (IO rejected)")
(td :class "px-3 py-2 text-stone-600" "Strict limits"))))))
(~doc-subsection :title "What Can Go Wrong"
(~docs/subsection :title "What Can Go Wrong"
(p "Honest accounting of the attack surface:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "Visual spoofing:") " A malicious component could render UI that looks like a login form. Mitigation: untrusted components render inside a visually distinct container with origin attribution.")
@@ -271,49 +271,49 @@
;; Wire Format & Prefetch Integration
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 6: Wire Format & Prefetch Integration" :id "wire-format"
(~docs/section :title "Phase 6: Wire Format & Prefetch Integration" :id "wire-format"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What it enables")
(p :class "text-violet-800" "Pages and SX responses reference components by CID. The prefetch system resolves them from the most efficient source. Components become location-independent."))
(~doc-subsection :title "CID References in Page Registry"
(~docs/subsection :title "CID References in Page Registry"
(p "The page registry (shipped to the client as " (code "<script type=\"text/sx-pages\">") ") currently lists deps by name. Extend to include CIDs:")
(~doc-code :code (highlight "{:name \"docs-page\" :path \"/language/docs/<slug>\"\n :auth \"public\" :has-data false\n :deps ({:name \"~essay-foo\" :cid \"bafy...essay\"}\n {:name \"~doc-code\" :cid \"bafy...doccode\"})\n :content \"(case slug ...)\" :closure {}}" "lisp"))
(~docs/code :code (highlight "{:name \"docs-page\" :path \"/language/docs/<slug>\"\n :auth \"public\" :has-data false\n :deps ({:name \"~essay-foo\" :cid \"bafy...essay\"}\n {:name \"~doc-code\" :cid \"bafy...doccode\"})\n :content \"(case slug ...)\" :closure {}}" "lisp"))
(p "The " (a :href "/sx/(etc.(plan.predictive-prefetch))" :class "text-violet-700 underline" "predictive prefetch system") " uses these CIDs to fetch components from the resolution cascade rather than only from the origin server's " (code "/sx/components") " endpoint."))
(~doc-subsection :title "SX Response Component Headers"
(~docs/subsection :title "SX Response Component Headers"
(p "Currently, " (code "SX-Components") " header lists loaded component names. Extend to support CIDs:")
(~doc-code :code (highlight "Request:\nSX-Components: ~card:bafy...card,~nav:bafy...nav\n\nResponse:\nSX-Component-CIDs: ~essay-foo:bafy...essay,~doc-code:bafy...doccode\n\n;; Response body only includes defs the client doesn't have\n(defcomp ~essay-foo ...)" "http"))
(~docs/code :code (highlight "Request:\nSX-Components: ~card:bafy...card,~plans/environment-images/nav:bafy...nav\n\nResponse:\nSX-Component-CIDs: ~plans/content-addressed-components/essay-foo:bafy...essay,~docs/code:bafy...doccode\n\n;; Response body only includes defs the client doesn't have\n(defcomp ~plans/content-addressed-components/essay-foo ...)" "http"))
(p "The client can then verify received components match their declared CIDs. If the origin server is compromised, CID verification catches the tampered response."))
(~doc-subsection :title "Federated Content"
(~docs/subsection :title "Federated Content"
(p "When an ActivityPub activity arrives with SX content, it declares component requirements by CID:")
(~doc-code :code (highlight "(Create\n :actor \"https://other-instance.com/users/bob\"\n :object (Note\n :content (~product-card :title \"Bob's Widget\" :price 29.99)\n :requires (list\n {:name \"~product-card\" :cid \"bafy...prodcard\"}\n {:name \"~price-tag\" :cid \"bafy...pricetag\"})))" "lisp"))
(~docs/code :code (highlight "(Create\n :actor \"https://other-instance.com/users/bob\"\n :object (Note\n :content (~product-card :title \"Bob's Widget\" :price 29.99)\n :requires (list\n {:name \"~product-card\" :cid \"bafy...prodcard\"}\n {:name \"~price-tag\" :cid \"bafy...pricetag\"})))" "lisp"))
(p "The receiving browser resolves required components through the cascade. If Bob's instance is down, the components are still fetchable from IPFS. The content is self-describing and self-resolving.")))
;; -----------------------------------------------------------------------
;; Component Sharing & Discovery
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 7: Sharing & Discovery" :id "sharing"
(~docs/section :title "Phase 7: Sharing & Discovery" :id "sharing"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What it enables")
(p :class "text-violet-800" "Servers publish component collections via AP. Other servers follow them. Like npm, but federated, content-addressed, and structurally safe."))
(~doc-subsection :title "Component Registry as AP Actor"
(~docs/subsection :title "Component Registry as AP Actor"
(p "Each server exposes a component registry actor:")
(~doc-code :code (highlight "(Service\n :id \"https://rose-ash.com/sx-registry\"\n :type \"SxComponentRegistry\"\n :name \"Rose Ash Components\"\n :outbox \"https://rose-ash.com/sx-registry/outbox\"\n :followers \"https://rose-ash.com/sx-registry/followers\")" "lisp"))
(~docs/code :code (highlight "(Service\n :id \"https://rose-ash.com/sx-registry\"\n :type \"SxComponentRegistry\"\n :name \"Rose Ash Components\"\n :outbox \"https://rose-ash.com/sx-registry/outbox\"\n :followers \"https://rose-ash.com/sx-registry/followers\")" "lisp"))
(p "Follow the registry to receive component updates. The outbox is a chronological feed of Create/Update/Delete activities for components. 'Update' means a new CID for the same name — consumers decide whether to adopt it."))
(~doc-subsection :title "Discovery Protocol"
(~docs/subsection :title "Discovery Protocol"
(p "Webfinger-style lookup for components by name:")
(~doc-code :code (highlight "GET /.well-known/sx-component?name=~product-card\n\n{\n \"name\": \"~product-card\",\n \"cid\": \"bafy...prodcard\",\n \"manifest_cid\": \"bafy...manifest\",\n \"gateway\": \"https://rose-ash.com/ipfs/\",\n \"author\": \"https://rose-ash.com/apps/market\"\n}" "http"))
(~docs/code :code (highlight "GET /.well-known/sx-component?name=~product-card\n\n{\n \"name\": \"~product-card\",\n \"cid\": \"bafy...prodcard\",\n \"manifest_cid\": \"bafy...manifest\",\n \"gateway\": \"https://rose-ash.com/ipfs/\",\n \"author\": \"https://rose-ash.com/apps/market\"\n}" "http"))
(p "This is an optional convenience — any consumer that knows the CID can skip discovery and fetch directly from IPFS. Discovery answers the question: " (em "\"what's the current version of ~product-card on rose-ash.com?\""))
)
(~doc-subsection :title "Name Resolution"
(~docs/subsection :title "Name Resolution"
(p "Names are human-friendly aliases for CIDs. The same name on different servers can refer to different components (different CIDs). Conflict resolution is simple:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "Local wins:") " If the server defines " (code "~card") ", that definition takes precedence over any federated " (code "~card") ".")
@@ -324,7 +324,7 @@
;; Spec modules
;; -----------------------------------------------------------------------
(~doc-section :title "Spec Modules" :id "spec-modules"
(~docs/section :title "Spec Modules" :id "spec-modules"
(p "Per the SX host architecture principle, all content-addressing logic is specced in " (code ".sx") " files and bootstrapped:")
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
@@ -351,7 +351,7 @@
;; Critical files
;; -----------------------------------------------------------------------
(~doc-section :title "Critical Files" :id "critical-files"
(~docs/section :title "Critical Files" :id "critical-files"
(div :class "overflow-x-auto rounded border border-stone-200"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-100"
@@ -404,7 +404,7 @@
;; Relationship
;; -----------------------------------------------------------------------
(~doc-section :title "Relationships" :id "relationships"
(~docs/section :title "Relationships" :id "relationships"
(p "This plan is the foundation for several other plans and roadmaps:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (a :href "/sx/(etc.(plan.sx-activity))" :class "text-violet-700 underline" "SX-Activity") " Phase 2 (content-addressed components on IPFS) is a summary of this plan. This plan supersedes that section with full detail.")

View File

@@ -2,10 +2,10 @@
;; Content-Addressed Environment Images
;; ---------------------------------------------------------------------------
(defcomp ~plan-environment-images-content ()
(~doc-page :title "Content-Addressed Environment Images"
(defcomp ~plans/environment-images/plan-environment-images-content ()
(~docs/page :title "Content-Addressed Environment Images"
(~doc-section :title "The Idea" :id "idea"
(~docs/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:")
@@ -22,7 +22,7 @@
;; What gets serialized
;; -----------------------------------------------------------------------
(~doc-section :title "What Gets Serialized" :id "what"
(~docs/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"
@@ -63,10 +63,10 @@
;; Image format
;; -----------------------------------------------------------------------
(~doc-section :title "Image Format" :id "format"
(~docs/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"))
(~docs/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 ~plans/environment-images/card (&key title subtitle &rest children)\n (div :class \"card\" (h2 title) (when subtitle (p subtitle)) children))\n (defcomp ~plans/environment-images/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"
@@ -79,11 +79,11 @@
;; Image CID
;; -----------------------------------------------------------------------
(~doc-section :title "Image CID" :id "image-cid"
(~docs/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"))
(~docs/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."))
@@ -92,10 +92,10 @@
;; Endpoint provenance
;; -----------------------------------------------------------------------
(~doc-section :title "Endpoint Provenance" :id "provenance"
(~docs/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"))
(~docs/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: ~plans/environment-images/card:bafy...card,~plans/environment-images/nav:bafy...nav" "http"))
(p "Three levels of verification:")
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
@@ -107,7 +107,7 @@
(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-700" "Fetch " (code "~plans/environment-images/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")
@@ -124,19 +124,19 @@
;; Cold start optimization
;; -----------------------------------------------------------------------
(~doc-section :title "Cold Start: Images as Cache" :id "cold-start"
(~docs/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"
(~docs/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")))
(~docs/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"
(~docs/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")
@@ -150,10 +150,10 @@
;; Standalone HTML
;; -----------------------------------------------------------------------
(~doc-section :title "Standalone HTML Bundles" :id "standalone"
(~docs/section :title "Standalone HTML Bundles" :id "standalone"
(p "An image can be inlined into a single HTML document, producing a fully self-contained application with no server dependency:")
(~doc-code :code (highlight "<!doctype html>\n<html>\n<head>\n <script type=\"text/sx-image\">\n (sx-image\n :version 1\n :spec-cids {...}\n :components (...)\n :macros (...)\n :bindings (...))\n </script>\n <script type=\"text/sx-pages\">\n (defpage home :path \"/\" :content (~home-page))\n </script>\n <script src=\"sx-ref.js\"></script>\n</head>\n<body>\n <div id=\"app\"></div>\n <script>\n // Deserialize image, register components, render\n Sx.bootFromImage(document.querySelector('[type=\"text/sx-image\"]'))\n </script>\n</body>\n</html>" "html"))
(~docs/code :code (highlight "<!doctype html>\n<html>\n<head>\n <script type=\"text/sx-image\">\n (sx-image\n :version 1\n :spec-cids {...}\n :components (...)\n :macros (...)\n :bindings (...))\n </script>\n <script type=\"text/sx-pages\">\n (defpage home :path \"/\" :content (~home-page))\n </script>\n <script src=\"sx-ref.js\"></script>\n</head>\n<body>\n <div id=\"app\"></div>\n <script>\n // Deserialize image, register components, render\n Sx.bootFromImage(document.querySelector('[type=\"text/sx-image\"]'))\n </script>\n</body>\n</html>" "html"))
(p "This document is its own CID. Pin it to IPFS and it's a permanent, executable, verifiable application. No origin server, no CDN, no DNS. The content network is the deployment target.")
@@ -165,12 +165,12 @@
;; Namespace scoping
;; -----------------------------------------------------------------------
(~doc-section :title "Namespaced Environments" :id "namespaces"
(~docs/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"))
(~docs/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 ~plans/environment-images/product-card ...)\n (defcomp ~plans/environment-images/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.")
(p "Resolution: " (code "market/~plans/environment-images/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"
@@ -181,19 +181,19 @@
(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-700" "~plans/environment-images/card, ~plans/environment-images/nav, ~nav-data/section-nav, ~docs/page, ~docs/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-700" "~shared:cards/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-700" "~plans/environment-images/product-card, ~plans/environment-images/price-tag, ~shared:fragments/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-700" "~docs/section, ~examples/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."))
@@ -202,10 +202,10 @@
;; Spec → Image → Page chain
;; -----------------------------------------------------------------------
(~doc-section :title "The Verification Chain" :id "chain"
(~docs/section :title "The Verification Chain" :id "chain"
(p "The full provenance chain from served page back to source:")
(~doc-code :code (highlight ";; 1. Page served with provenance headers\n;;\n;; SX-Spec: bafy...eval,bafy...render,...\n;; SX-Image: bafy...market-image\n;; SX-Page: (defpage product :path \"/products/<slug>\" ...)\n;;\n;; 2. Verify image → spec\n;;\n;; Fetch specs by CID → evaluate → build image → compare CID\n;; If match: the image was correctly produced from these specs\n;;\n;; 3. Verify page → image\n;;\n;; Deserialize image → evaluate page defn → render\n;; If output matches served HTML: the page was correctly rendered\n;;\n;; 4. Trust chain terminates at the spec\n;;\n;; The spec is self-hosting (eval.sx evaluates itself)\n;; The spec's CID is its identity\n;; No external trust anchor needed beyond the hash function" "lisp"))
(~docs/code :code (highlight ";; 1. Page served with provenance headers\n;;\n;; SX-Spec: bafy...eval,bafy...render,...\n;; SX-Image: bafy...market-image\n;; SX-Page: (defpage product :path \"/products/<slug>\" ...)\n;;\n;; 2. Verify image → spec\n;;\n;; Fetch specs by CID → evaluate → build image → compare CID\n;; If match: the image was correctly produced from these specs\n;;\n;; 3. Verify page → image\n;;\n;; Deserialize image → evaluate page defn → render\n;; If output matches served HTML: the page was correctly rendered\n;;\n;; 4. Trust chain terminates at the spec\n;;\n;; The spec is self-hosting (eval.sx evaluates itself)\n;; The spec's CID is its identity\n;; No external trust anchor needed beyond the hash function" "lisp"))
(p "This is stronger than code signing. Code signing says " (em "\"this entity vouches for this binary.\"") " Content addressing says " (em "\"this binary is the deterministic output of this source.\"") " No entity needed. No certificate authority. No revocation lists. Just math."))
@@ -213,9 +213,9 @@
;; Implementation phases
;; -----------------------------------------------------------------------
(~doc-section :title "Implementation" :id "implementation"
(~docs/section :title "Implementation" :id "implementation"
(~doc-subsection :title "Phase 1: Image Serialization"
(~docs/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")
@@ -223,14 +223,14 @@
(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"
(~docs/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"
(~docs/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")
@@ -238,7 +238,7 @@
(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"
(~docs/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")
@@ -246,7 +246,7 @@
(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"
(~docs/subsection :title "Phase 5: Standalone Export"
(p "Generate self-contained HTML with inlined image. Pin to IPFS.")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Inline " (code "(sx-image ...)") " as " (code "<script type=\"text/sx-image\">"))
@@ -254,7 +254,7 @@
(li "Include sx-ref.js (or link to its CID)")
(li "The resulting HTML is a complete application — pin its CID to IPFS")))
(~doc-subsection :title "Phase 6: Namespaced Images"
(~docs/subsection :title "Phase 6: Namespaced Images"
(p "Per-service images with " (code ":extends") " for layered composition.")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Shared image: components from " (code "shared/sx/templates/"))
@@ -266,7 +266,7 @@
;; Dependencies
;; -----------------------------------------------------------------------
(~doc-section :title "Dependencies" :id "dependencies"
(~docs/section :title "Dependencies" :id "dependencies"
(p "What must exist before this plan can execute:")
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
(table :class "w-full text-left text-sm"

View File

@@ -2,14 +2,14 @@
;; Fragment Protocol
;; ---------------------------------------------------------------------------
(defcomp ~plan-fragment-protocol-content ()
(~doc-page :title "Fragment Protocol"
(defcomp ~plans/fragment-protocol/plan-fragment-protocol-content ()
(~docs/page :title "Fragment Protocol"
(~doc-section :title "Context" :id "context"
(~docs/section :title "Context" :id "context"
(p "Fragment endpoints return raw sexp source (e.g., " (code "(~blog-nav-wrapper :items ...)") "). The consuming service embeds this in its page sexp, which the client evaluates. But service-specific components like " (code "~blog-nav-wrapper") " are only in that service's component env — not in the consumer's. So the consumer's " (code "client_components_tag()") " never sends them to the client, causing \"Unknown component\" errors.")
(p "The fix: transfer component definitions alongside fragments. Services tell the provider what they already have; the provider sends only what's missing."))
(~doc-section :title "What exists" :id "exists"
(~docs/section :title "What exists" :id "exists"
(div :class "rounded border border-green-200 bg-green-50 p-4"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Fragment GET infrastructure works (" (code "shared/infrastructure/fragments.py") ")")
@@ -17,7 +17,7 @@
(li "Content type negotiation for text/html and text/sx responses")
(li "Fragment caching and composition in page rendering"))))
(~doc-section :title "What remains" :id "remains"
(~docs/section :title "What remains" :id "remains"
(div :class "rounded border border-amber-200 bg-amber-50 p-4"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "POST sexp protocol: ") "Switch from GET to POST with structured sexp body containing " (code ":components") " list of what consumer already has")
@@ -26,7 +26,7 @@
(li (strong "Register received defs: ") "Consumer parses " (code ":defs") " from response and registers into its " (code "_COMPONENT_ENV"))
(li (strong "Shared blueprint factory: ") (code "create_fragment_blueprint(handlers)") " to deduplicate the identical fragment endpoint pattern across 8 services"))))
(~doc-section :title "Files to modify" :id "files"
(~docs/section :title "Files to modify" :id "files"
(div :class "overflow-x-auto rounded border border-stone-200"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-100"

View File

@@ -2,8 +2,8 @@
;; Generative SX — programs that write themselves as they run
;; ---------------------------------------------------------------------------
(defcomp ~plan-generative-sx-content ()
(~doc-page :title "Generative SX"
(defcomp ~plans/generative-sx/plan-generative-sx-content ()
(~docs/page :title "Generative SX"
(p :class "text-stone-500 text-sm italic mb-8"
"In the browser, SX is a program that modifies itself in response to external stimuli. Outside the browser, it becomes a program that writes itself as it runs.")
@@ -12,7 +12,7 @@
;; I. The observation
;; =====================================================================
(~doc-section :title "The observation" :id "observation"
(~docs/section :title "The observation" :id "observation"
(p "The marshes work made something visible. A server response arrives carrying " (code "(reset! (use-store \"price\") 14.99)") " inside a " (code "data-init") " script. The SX evaluator parses this string, evaluates it in its own environment, and mutates its own signal graph. The program accepted new source at runtime and changed itself.")
(p "This isn't metaprogramming. Macros expand at compile time — they transform source before evaluation. This is different: the program is " (em "already running") " when it receives new code, evaluates it, and continues with an extended state. The DOM is just the boundary. The signal graph is just the state. The mechanism is general:")
(ol :class "list-decimal list-inside space-y-2 text-stone-600"
@@ -27,34 +27,34 @@
;; II. What already exists
;; =====================================================================
(~doc-section :title "What already exists" :id "exists"
(~docs/section :title "What already exists" :id "exists"
(p "The pieces are already built. They just haven't been connected into the generative pattern.")
(~doc-subsection :title "Homoiconicity"
(~docs/subsection :title "Homoiconicity"
(p "SX code is SX data. " (code "parse") " takes a string and returns a list. " (code "aser") " takes a list and returns a string. These round-trip perfectly. The program can read its own source as naturally as it reads a config file, because they're the same format.")
(~doc-code :code (highlight ";; Code is data\n(define source \"(+ 1 2)\")\n(define ast (parse source)) ;; → (list '+ 1 2)\n(define result (eval-expr ast env)) ;; → 3\n\n;; Data is code\n(define spec '(define greet (fn (name) (str \"Hello, \" name \"!\"))))\n(eval-expr spec env)\n(greet \"world\") ;; → \"Hello, world!\"" "lisp")))
(~docs/code :code (highlight ";; Code is data\n(define source \"(+ 1 2)\")\n(define ast (parse source)) ;; → (list '+ 1 2)\n(define result (eval-expr ast env)) ;; → 3\n\n;; Data is code\n(define spec '(define greet (fn (name) (str \"Hello, \" name \"!\"))))\n(eval-expr spec env)\n(greet \"world\") ;; → \"Hello, world!\"" "lisp")))
(~doc-subsection :title "Runtime eval"
(~docs/subsection :title "Runtime eval"
(p (code "eval-expr") " is available at runtime, not just boot. " (code "data-init") " scripts already use it. Any SX string can become running code at any point in the program's execution. This is not " (code "eval()") " bolted onto a language that doesn't want it — it's the " (em "primary mechanism") " of the language."))
(~doc-subsection :title "The environment model"
(~docs/subsection :title "The environment model"
(p (code "env-extend") " creates a child scope. " (code "env-set!") " adds to the current scope. " (code "define") " creates new bindings. New definitions don't replace old ones — they extend the environment. The program grows monotonically. Every previous state is still reachable through scope chains.")
(p "This is the critical property. A generative program doesn't destroy what it was — it " (em "becomes more") ". Each generation includes everything before it plus what it just wrote."))
(~doc-subsection :title "The bootstrapper"
(~docs/subsection :title "The bootstrapper"
(p "The bootstrapper reads " (code "eval.sx") " — the evaluator's definition of itself — and emits JavaScript or Python that " (em "is") " that evaluator. The spec writes itself into a host language. This is already a generative program, frozen at build time: read source → transform → emit target. Generative SX unfreezes this. The transformation happens " (em "while the program runs") ", not before."))
(~doc-subsection :title "Content-addressed identity"
(~docs/subsection :title "Content-addressed identity"
(p "From the Art DAG: all data identified by SHA3-256 hashes. If a program fragment is identified by its hash, then \"writing yourself\" means producing new hashes. The history is immutable. You can always go back. A generative program isn't a mutating blob — it's a DAG of versioned states.")))
;; =====================================================================
;; III. The generative pattern
;; =====================================================================
(~doc-section :title "The generative pattern" :id "pattern"
(~docs/section :title "The generative pattern" :id "pattern"
(p "A generative SX program starts with a seed and grows by evaluating its own output.")
(~doc-code :code (highlight ";; The core loop\n(define run\n (fn (env source)\n (let ((ast (parse source))\n (result (eval-expr ast env)))\n ;; result might be:\n ;; a value → done, return it\n ;; a string → new SX source, evaluate it (grow)\n ;; a list of defs → new definitions to add to env\n ;; a dict → {source: \"...\", env-patch: {...}} (grow + configure)\n (cond\n (string? result)\n (run env result) ;; evaluate the output\n (and (dict? result) (has-key? result \"source\"))\n (let ((patched (env-merge env (get result \"env-patch\"))))\n (run patched (get result \"source\")))\n :else\n result))))" "lisp"))
(~docs/code :code (highlight ";; The core loop\n(define run\n (fn (env source)\n (let ((ast (parse source))\n (result (eval-expr ast env)))\n ;; result might be:\n ;; a value → done, return it\n ;; a string → new SX source, evaluate it (grow)\n ;; a list of defs → new definitions to add to env\n ;; a dict → {source: \"...\", env-patch: {...}} (grow + configure)\n (cond\n (string? result)\n (run env result) ;; evaluate the output\n (and (dict? result) (has-key? result \"source\"))\n (let ((patched (env-merge env (get result \"env-patch\"))))\n (run patched (get result \"source\")))\n :else\n result))))" "lisp"))
(p "The program evaluates source. If the result is more source, it evaluates that too. Each iteration can extend the environment — add new functions, new macros, new primitives. The environment grows. The program becomes capable of things it couldn't do at the start.")
@@ -62,7 +62,7 @@
(p :class "text-violet-900 font-medium" "This is not eval-in-a-loop")
(p :class "text-violet-800 text-sm" "A REPL evaluates user input in a persistent environment. That's interactive, not generative. The generative pattern is different: the program itself decides what to evaluate next. No user in the loop. The output of one evaluation becomes the input to the next. The program writes itself."))
(~doc-subsection :title "Three modes"
(~docs/subsection :title "Three modes"
(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"
@@ -91,44 +91,44 @@
;; IV. Concrete manifestations
;; =====================================================================
(~doc-section :title "Concrete manifestations" :id "manifestations"
(~docs/section :title "Concrete manifestations" :id "manifestations"
(~doc-subsection :title "1. The spec that compiles itself"
(~docs/subsection :title "1. The spec that compiles itself"
(p "Currently: " (code "bootstrap_js.py") " (Python) reads " (code "eval.sx") " (SX) and emits " (code "sx-browser.js") " (JavaScript). Three languages. Two of them are hosts, one is the spec.")
(p "Generative version: " (code "eval.sx") " evaluates itself with a code-generation adapter. The evaluator walks its own AST and emits the target language. No Python bootstrapper. No JavaScript template. The spec " (em "is") " the compiler.")
(~doc-code :code (highlight ";; bootstrap.sx — the spec compiles itself\n;;\n;; Load the codegen adapter for the target\n(define emit (load-adapter target)) ;; target = \"js\" | \"py\" | \"go\" | ...\n;;\n;; Read the spec files\n(define spec-source (read-file \"eval.sx\"))\n(define spec-ast (parse spec-source))\n;;\n;; Walk the AST and emit target code\n;; The walker IS the evaluator — eval.sx evaluating eval.sx\n;; with emit instead of execute\n(define target-code\n (eval-expr spec-ast\n (env-extend codegen-env\n ;; Override define, fn, if, etc. to emit instead of execute\n (codegen-special-forms emit))))\n;;\n(write-file (str \"sx-ref.\" (target-extension target)) target-code)" "lisp"))
(~docs/code :code (highlight ";; bootstrap.sx — the spec compiles itself\n;;\n;; Load the codegen adapter for the target\n(define emit (load-adapter target)) ;; target = \"js\" | \"py\" | \"go\" | ...\n;;\n;; Read the spec files\n(define spec-source (read-file \"eval.sx\"))\n(define spec-ast (parse spec-source))\n;;\n;; Walk the AST and emit target code\n;; The walker IS the evaluator — eval.sx evaluating eval.sx\n;; with emit instead of execute\n(define target-code\n (eval-expr spec-ast\n (env-extend codegen-env\n ;; Override define, fn, if, etc. to emit instead of execute\n (codegen-special-forms emit))))\n;;\n(write-file (str \"sx-ref.\" (target-extension target)) target-code)" "lisp"))
(p "This is the bootstrapper rewritten as a generative program. The spec reads itself, walks itself, and writes the output language. Adding a new target means writing a new " (code "load-adapter") " — a set of emitters for the SX special forms. The walker doesn't change. The spec doesn't change. Only the output format changes.")
(p "The current bootstrappers (" (code "bootstrap_js.py") ", " (code "bootstrap_py.py") ") would become the first two adapters. Future targets (Go, Rust, WASM) are additional adapters, written in SX and bootstrapped like everything else."))
(~doc-subsection :title "2. The program that discovers its dependencies"
(p (code "deps.sx") " analyzes component dependency graphs. It walks component ASTs, finds " (code "~name") " references, computes transitive closures. This is the analytic mode — SX analyzing SX.")
(~docs/subsection :title "2. The program that discovers its dependencies"
(p (code "deps.sx") " analyzes component dependency graphs. It walks component ASTs, finds " (code "~plans/content-addressed-components/name") " references, computes transitive closures. This is the analytic mode — SX analyzing SX.")
(p "The generative version: a program that discovers it needs a component, searches for its definition (local files, IPFS, a registry), loads it, evaluates it, and continues. The program grows its own capability set at runtime.")
(~doc-code :code (highlight ";; A program that discovers and loads what it needs\n(define render-page\n (fn (page-name)\n (let ((page-def (lookup-component page-name)))\n (when (nil? page-def)\n ;; Component not found locally — fetch from registry\n (let ((source (fetch-component-source page-name)))\n ;; Evaluate the definition — it joins the environment\n (eval-expr (parse source) env)\n ;; Now it exists\n (set! page-def (lookup-component page-name))))\n ;; Render with all dependencies resolved\n (render-to-html (list page-def)))))" "lisp"))
(~docs/code :code (highlight ";; A program that discovers and loads what it needs\n(define render-page\n (fn (page-name)\n (let ((page-def (lookup-component page-name)))\n (when (nil? page-def)\n ;; Component not found locally — fetch from registry\n (let ((source (fetch-component-source page-name)))\n ;; Evaluate the definition — it joins the environment\n (eval-expr (parse source) env)\n ;; Now it exists\n (set! page-def (lookup-component page-name))))\n ;; Render with all dependencies resolved\n (render-to-html (list page-def)))))" "lisp"))
(p "This already happens in the browser. " (code "sx_response") " prepends missing component definitions as a " (code "data-components") " script block. The client evaluates them and they join the environment. The generative version makes this explicit: the program tells you what it needs, you give it source, it evaluates it, it grows."))
(~doc-subsection :title "3. The test suite that writes tests"
(~docs/subsection :title "3. The test suite that writes tests"
(p "Given a function's signature and a set of properties (" (code "prove.sx") " already has the property language), generate test cases that verify the properties. The program reads its own function definitions, generates SX expressions that test them, and evaluates those expressions.")
(~doc-code :code (highlight ";; Given: a function and properties about it\n(define-property string-reverse-involutory\n :forall ((s string?))\n :holds (= (reverse (reverse s)) s))\n\n;; Generate: test cases from the property\n;; The program reads the property, generates test source, evals it\n(define tests (generate-tests string-reverse-involutory))\n;; tests = list of (assert (= (reverse (reverse \"hello\")) \"hello\"))\n;; (assert (= (reverse (reverse \"\")) \"\"))\n;; (assert (= (reverse (reverse \"a\")) \"a\"))\n;; ... (random strings, edge cases)\n(for-each (fn (t) (eval-expr t env)) tests)" "lisp"))
(~docs/code :code (highlight ";; Given: a function and properties about it\n(define-property string-reverse-involutory\n :forall ((s string?))\n :holds (= (reverse (reverse s)) s))\n\n;; Generate: test cases from the property\n;; The program reads the property, generates test source, evals it\n(define tests (generate-tests string-reverse-involutory))\n;; tests = list of (assert (= (reverse (reverse \"hello\")) \"hello\"))\n;; (assert (= (reverse (reverse \"\")) \"\"))\n;; (assert (= (reverse (reverse \"a\")) \"a\"))\n;; ... (random strings, edge cases)\n(for-each (fn (t) (eval-expr t env)) tests)" "lisp"))
(p "The program analyzed itself (read the property), generated new SX (the test cases), and evaluated it (ran the tests). Three modes — analytic, synthetic, generative — in sequence."))
(~doc-subsection :title "4. The server that extends its own API"
(~docs/subsection :title "4. The server that extends its own API"
(p "An SX server receives a request it doesn't know how to handle. Instead of returning 404, it examines the request, generates a handler, evaluates it, and handles the request.")
(~doc-code :code (highlight ";; A route handler that generates new route handlers\n(define handle-unknown-route\n (fn (path params)\n ;; Analyze what was requested\n (let ((segments (split path \"/\"))\n (resource (nth segments 1))\n (action (nth segments 2)))\n ;; Check if a schema exists for this resource\n (let ((schema (lookup-schema resource)))\n (when schema\n ;; Generate a CRUD handler from the schema\n (let ((handler-source (generate-crud-handler resource action schema)))\n ;; Evaluate it — the handler now exists\n (eval-expr (parse handler-source) env)\n ;; Route future requests to the generated handler\n (register-route path (env-get env (str resource \"-\" action)))\n ;; Handle this request with the new handler\n ((env-get env (str resource \"-\" action)) params)))))))" "lisp"))
(~docs/code :code (highlight ";; A route handler that generates new route handlers\n(define handle-unknown-route\n (fn (path params)\n ;; Analyze what was requested\n (let ((segments (split path \"/\"))\n (resource (nth segments 1))\n (action (nth segments 2)))\n ;; Check if a schema exists for this resource\n (let ((schema (lookup-schema resource)))\n (when schema\n ;; Generate a CRUD handler from the schema\n (let ((handler-source (generate-crud-handler resource action schema)))\n ;; Evaluate it — the handler now exists\n (eval-expr (parse handler-source) env)\n ;; Route future requests to the generated handler\n (register-route path (env-get env (str resource \"-\" action)))\n ;; Handle this request with the new handler\n ((env-get env (str resource \"-\" action)) params)))))))" "lisp"))
(p "This is not code generation in the Rails scaffolding sense — those generate files you then edit. This generates running code at runtime. The handler didn't exist. Now it does. The server grew."))
(~doc-subsection :title "5. The macro system that learns idioms"
(~docs/subsection :title "5. The macro system that learns idioms"
(p "A generative macro system that detects repeated patterns in code and synthesizes macros to capture them. The program watches itself being written and abstracts its own patterns.")
(~doc-code :code (highlight ";; The program notices this pattern appearing repeatedly:\n;; (div :class \"card\" (h2 title) (p body) children...)\n;;\n;; It generates:\n(defmacro ~card (title body &rest children)\n (div :class \"card\"\n (h2 ,title)\n (p ,body)\n ,@children))\n;;\n;; And rewrites its own source to use the new macro.\n;; This is an SX program that:\n;; 1. Analyzed its own AST (found repeated subtrees)\n;; 2. Synthesized a macro (extracted the pattern)\n;; 3. Evaluated the macro definition (extended env)\n;; 4. Rewrote its own source (used the macro)\n;; Four generative steps." "lisp"))
(~docs/code :code (highlight ";; The program notices this pattern appearing repeatedly:\n;; (div :class \"card\" (h2 title) (p body) children...)\n;;\n;; It generates:\n(defmacro ~card (title body &rest children)\n (div :class \"card\"\n (h2 ,title)\n (p ,body)\n ,@children))\n;;\n;; And rewrites its own source to use the new macro.\n;; This is an SX program that:\n;; 1. Analyzed its own AST (found repeated subtrees)\n;; 2. Synthesized a macro (extracted the pattern)\n;; 3. Evaluated the macro definition (extended env)\n;; 4. Rewrote its own source (used the macro)\n;; Four generative steps." "lisp"))
(p "The connection to the Art DAG: each version of the source is content-addressed. The original (before macros) and the refactored (after macros) are both immutable nodes. The generative step is an edge in the DAG. You can always inspect what the program was before it rewrote itself.")))
;; =====================================================================
;; V. The seed
;; =====================================================================
(~doc-section :title "The seed" :id "seed"
(~docs/section :title "The seed" :id "seed"
(p "A generative SX program starts with a seed. The seed must contain enough to bootstrap the generative loop: a parser, an evaluator, and the " (code "run") " function. Everything else is grown.")
(~doc-code :code (highlight ";; seed.sx — the minimal generative program\n;;\n;; This file contains:\n;; - The SX parser (parse)\n;; - The SX evaluator (eval-expr)\n;; - The generative loop (run)\n;; - A source acquisition function (next-source)\n;;\n;; Everything else — primitives, rendering, networking, persistence —\n;; is loaded by the program as it discovers it needs them.\n\n(define run\n (fn (env)\n (let ((source (next-source env)))\n (when source\n (let ((result (eval-expr (parse source) env)))\n (run env))))))\n\n;; Start with a bare environment\n(run (env-extend (dict\n \"parse\" parse\n \"eval-expr\" eval-expr\n \"next-source\" initial-source-fn)))" "lisp"))
(~docs/code :code (highlight ";; seed.sx — the minimal generative program\n;;\n;; This file contains:\n;; - The SX parser (parse)\n;; - The SX evaluator (eval-expr)\n;; - The generative loop (run)\n;; - A source acquisition function (next-source)\n;;\n;; Everything else — primitives, rendering, networking, persistence —\n;; is loaded by the program as it discovers it needs them.\n\n(define run\n (fn (env)\n (let ((source (next-source env)))\n (when source\n (let ((result (eval-expr (parse source) env)))\n (run env))))))\n\n;; Start with a bare environment\n(run (env-extend (dict\n \"parse\" parse\n \"eval-expr\" eval-expr\n \"next-source\" initial-source-fn)))" "lisp"))
(p "The seed is a quine that doesn't just reproduce itself — it " (em "extends") " itself. Each call to " (code "next-source") " returns new SX that the seed evaluates in its own environment. The environment grows. The seed's capabilities grow. But the seed itself never changes — it's the fixed point of the generative process.")
@@ -140,19 +140,19 @@
;; VI. Growth constraints
;; =====================================================================
(~doc-section :title "Growth constraints" :id "constraints"
(~docs/section :title "Growth constraints" :id "constraints"
(p "Unconstrained self-modification is dangerous. A program that can rewrite any part of itself can rewrite its own safety checks. Generative SX needs growth constraints — rules about what the program can and cannot do to itself.")
(~doc-subsection :title "The boundary"
(~docs/subsection :title "The boundary"
(p "The boundary system (" (code "boundary.sx") ") already enforces this. Pure primitives can't do IO. IO primitives can't escape their declared capabilities. Components are classified as pure or IO-dependent. The boundary is checked at registration time — " (code "SX_BOUNDARY_STRICT=1") " means violations crash at startup.")
(p "For generative programs, the boundary extends: generated code is subject to the same constraints as hand-written code. A generative program can't synthesize an IO primitive — it can only compose existing ones. It can't bypass the boundary by generating code that accesses raw platform APIs. The sandbox applies to generated code exactly as it applies to original code.")
(p "This is the key safety property: " (strong "generative SX is sandboxed generative SX") ". The generated code runs in the same evaluator with the same restrictions. No escape hatches."))
(~doc-subsection :title "Content addressing as audit trail"
(~docs/subsection :title "Content addressing as audit trail"
(p "Every piece of generated code is content-addressed. The SHA3-256 hash of the generated source is its identity. You can trace any piece of running code back to the generation step that produced it, the input that triggered that step, and the state of the environment at that point.")
(p "This makes generative programs auditable. \"Where did this function come from?\" has a definite answer: it was generated by " (em "this") " code, from " (em "this") " input, at " (em "this") " point in the generative sequence. The DAG of content hashes is the program's autobiography."))
(~doc-subsection :title "Monotonic growth"
(~docs/subsection :title "Monotonic growth"
(p "The environment model is append-only. " (code "define") " creates new bindings; it doesn't destroy old ones (inner scopes shadow, but the outer binding persists). " (code "env-extend") " creates a child — the parent is immutable.")
(p "A generative program can extend its environment but cannot shrink it. It can add new functions but cannot delete existing ones. It can shadow a function with a new definition but cannot destroy the original. This means the program's history is preserved in its scope chain — you can always inspect what it was before any given generation step.")
(p "Destructive operations (" (code "set!") ") are confined to mutable cells explicitly created for that purpose. The generative loop itself operates on immutable environments extended with each step.")))
@@ -161,14 +161,14 @@
;; VII. Host properties
;; =====================================================================
(~doc-section :title "Host properties" :id "host-properties"
(~docs/section :title "Host properties" :id "host-properties"
(p "A generative SX program runs on a host — JavaScript, Python, Go, bare metal. The host must provide specific properties or the generative loop breaks. These aren't preferences. They're " (em "requirements") ". A host that violates any of them can't run generative SX correctly.")
(~doc-subsection :title "Lossless parse/serialize round-trip"
(~docs/subsection :title "Lossless parse/serialize round-trip"
(p "The host must implement " (code "parse") " and " (code "aser") " such that " (code "(aser (parse source))") " produces semantically equivalent source. Generated code passes through " (code "parse → transform → serialize → parse") " cycles. If the round-trip is lossy — if whitespace, keyword order, or nested structure is corrupted — the generative loop silently degrades. After enough iterations, the program isn't what it thinks it is.")
(p "This is homoiconicity at the implementation level, not just the language level. The host's data structures must faithfully represent the full AST, and the serializer must faithfully reproduce it."))
(~doc-subsection :title "Runtime eval with first-class environments"
(~docs/subsection :title "Runtime eval with first-class environments"
(p (code "eval-in") " requires evaluating arbitrary expressions in arbitrary environments at runtime. The host must support:")
(ul :class "list-disc pl-5 space-y-1 text-stone-600"
(li "Creating new environments (" (code "env-extend") ")")
@@ -177,37 +177,37 @@
(li "Passing environments as values — storing them in variables, returning them from functions"))
(p "Environments aren't implementation detail in a generative program. They're the " (em "state") ". The running environment at generation step N is the complete description of what the program knows. The host must treat environments as first-class values, not hidden interpreter internals."))
(~doc-subsection :title "Monotonic environment growth"
(~docs/subsection :title "Monotonic environment growth"
(p "A generative program that can " (code "undefine") " things becomes unpredictable. If generation step N+1 removes a function that step N defined, step N+2 might reference the missing function and fail — or worse, silently bind to a different function in an outer scope.")
(p "The host must enforce that environments grow monotonically. New bindings append. Existing bindings in a given scope are immutable once set (or explicitly versioned). " (code "env-extend") " creates children; it never mutates the parent. This makes the generative loop convergent — each step strictly increases the program's capabilities, never decreases them."))
(~doc-subsection :title "Content-addressed storage"
(~docs/subsection :title "Content-addressed storage"
(p "Every generated fragment gets a SHA3-256 identity. The host needs native or near-native hashing and a content-addressed store — an in-memory dict at minimum, IPFS at scale. This provides the audit trail: you can always answer \"where did this code come from?\" by walking the hash chain back to the generation step that produced it.")
(p "Without content addressing, generative programs are opaque. You can't diff two versions of a generated function. You can't roll back to a previous generation. You can't verify that two nodes in a seed network generated the same code from the same input. Content addressing makes the generative process " (em "inspectable") "."))
(~doc-subsection :title "Boundary enforcement on generated code"
(~docs/subsection :title "Boundary enforcement on generated code"
(p "Generated code must pass through the same boundary validation as hand-written code. If " (code "write-file") " is a Tier 2 IO primitive, a generated expression can't call it unless the evaluation context permits Tier 2.")
(p "The host must enforce this " (em "at eval time") ", not just at definition time — because generated code didn't exist at definition time. Every call to " (code "eval-in") " must check the boundary. Every primitive invoked by generated code must verify its tier. There is no \"trusted generated code\" — all code is untrusted until the boundary clears it."))
(~doc-subsection :title "Correct quotation and splicing"
(~docs/subsection :title "Correct quotation and splicing"
(p "Quasiquote (" (code "`") "), unquote (" (code ",") "), and unquote-splicing (" (code ",@") ") must work correctly for programmatic code construction. The host needs these as first-class operations, not string concatenation.")
(p "A generative program builds code by template:")
(~doc-code :code (highlight ";; The generative program builds new definitions by template\n(define gen-handler\n (fn (name params body)\n `(define ,name\n (fn ,params\n ,@body))))\n\n;; gen-handler produces an AST, not a string\n;; The AST can be inspected, transformed, hashed, then evaluated\n(eval-in (gen-handler 'greet '(name) '((str \"Hello, \" name))) env)" "lisp"))
(~docs/code :code (highlight ";; The generative program builds new definitions by template\n(define gen-handler\n (fn (name params body)\n `(define ,name\n (fn ,params\n ,@body))))\n\n;; gen-handler produces an AST, not a string\n;; The AST can be inspected, transformed, hashed, then evaluated\n(eval-in (gen-handler 'greet '(name) '((str \"Hello, \" name))) env)" "lisp"))
(p "String concatenation would work — " (code "(str \"(define \" name \" ...)\")") " — but it's fragile, unstructured, and can't be inspected before evaluation. Quasiquote produces an AST. The generative program works with " (em "structure") ", not text."))
(~doc-subsection :title "Tail-call optimization"
(~docs/subsection :title "Tail-call optimization"
(p "The generative loop is inherently recursive: eval produces source, which is eval'd, which may produce more source. Without TCO, the loop blows the stack after enough iterations. The trampoline/thunk mechanism in the spec handles this, but the host must implement it efficiently.")
(p "This is not optional. A generative program that can only recurse a few thousand times before crashing is not a generative program — it's a demo. The self-compiling spec (Phase 1) alone requires walking every node of " (code "eval.sx") ", which is thousands of recursive calls."))
(~doc-subsection :title "Deterministic evaluation order"
(~docs/subsection :title "Deterministic evaluation order"
(p "If two hosts evaluate the same generative program and get different results because of evaluation order, the content hashes diverge. The programs are no longer equivalent. They can't federate (Phase 5), can't verify each other's output, can't share generated code.")
(p "The host must guarantee: dict iteration order is deterministic (insertion order). Argument evaluation is left-to-right. Effect sequencing follows definition order. No observable nondeterminism in pure evaluation. This is what makes generative programs " (em "reproducible") " — same seed, same input, same output, regardless of host."))
(~doc-subsection :title "Serializable state"
(~docs/subsection :title "Serializable state"
(p "For Phase 4 (self-extending server) and Phase 5 (seed network), a generative program needs to pause, serialize its state, and resume elsewhere. The host needs the ability to serialize an environment + pending expression as data.")
(p "This doesn't require first-class continuations (though those work). It requires that everything in the environment is serializable: functions serialize as their source AST, signals as their current value, environments as nested dicts. The " (code "env-snapshot") " primitive provides this. The host must ensure nothing in the environment is opaque — no host-language closures that can't be serialized, no hidden mutable state that isn't captured by the snapshot."))
(~doc-subsection :title "IO isolation"
(~docs/subsection :title "IO isolation"
(p "The generative primitives (" (code "read-file") ", " (code "write-file") ", " (code "list-files") ") are the " (em "only") " way generated code touches the outside world. The host must be able to intercept, log, and deny all IO. There is no escape hatch through FFI or native calls.")
(p "This is what makes generative programs auditable. If the host allows generated code to call raw " (code "fs.writeFileSync") " or " (code "os.system") ", the boundary is meaningless. The host must virtualize all IO through the declared primitives. Generated code that tries to escape the sandbox hits the boundary, not the OS."))
@@ -220,29 +220,29 @@
;; VIII. Environment migration
;; =====================================================================
(~doc-section :title "Environment migration" :id "env-migration"
(~docs/section :title "Environment migration" :id "env-migration"
(p "SX endpoints tunnel into different execution environments with different primitive sets. A browser has " (code "render-to-dom") " but no " (code "gpu-exec") ". A render node has " (code "gpu-exec") " but no " (code "fetch-fragment") ". An ingest server has " (code "open-feed") " but neither. The boundary isn't just a restriction — it's a " (em "capability declaration") ". It tells you what an environment " (em "can do") ".")
(~doc-subsection :title "Boundary as capability declaration"
(~docs/subsection :title "Boundary as capability declaration"
(p "Every environment declares its boundary: the set of primitives it provides. SX source is portable across any environment that satisfies its primitive requirements. If a program only uses pure primitives (Tier 0), it runs anywhere. If it calls " (code "gpu-exec") ", it needs an environment that provides " (code "gpu-exec") ". The boundary is a type signature on the environment itself — not \"what can this code do\" but \"what must the host provide.\"")
(p "This inverts the usual framing. The boundary doesn't " (em "forbid") " — it " (em "requires") ". A generated program that calls " (code "encode-stream") " is declaring a hardware dependency. The boundary system doesn't block the call — it routes the program to a host that can satisfy it."))
(~doc-subsection :title "with-boundary as migration point"
(~docs/subsection :title "with-boundary as migration point"
(p "Execution migrates to where the primitives are. When the evaluator hits a " (code "with-boundary") " block requiring primitives the current host doesn't have, it serializes state (" (code "env-snapshot") "), ships the pending expression plus environment to a host that has them, and execution continues there. The block is the unit of migration, not individual primitive calls.")
(~doc-code :code (highlight "(with-boundary (media-processing encoding)\n (let ((frames (gpu-exec recipe cached-layers)))\n (encode-stream frames :codec \"h264\"\n :on-input-needed (fn (slot)\n (with-boundary (live-ingest)\n (open-feed :protocol \"webrtc\" :slot slot))))))" "lisp"))
(~docs/code :code (highlight "(with-boundary (media-processing encoding)\n (let ((frames (gpu-exec recipe cached-layers)))\n (encode-stream frames :codec \"h264\"\n :on-input-needed (fn (slot)\n (with-boundary (live-ingest)\n (open-feed :protocol \"webrtc\" :slot slot))))))" "lisp"))
(p "This program starts wherever it starts. When it hits " (code "(with-boundary (media-processing encoding) ...)") ", the evaluator checks: does the current host provide " (code "gpu-exec") " and " (code "encode-stream") "? If yes, evaluate in place. If no, snapshot the environment, serialize the pending expression, and dispatch to a host that does. Inside the encoding block, " (code ":on-input-needed") " triggers a nested migration — the " (code "(with-boundary (live-ingest) ...)") " block dispatches to an ingest server that provides " (code "open-feed") ".")
(p "The program doesn't know where it runs. It declares what it needs. The runtime figures out " (em "where") "."))
(~doc-subsection :title "Declaration, not discovery"
(~docs/subsection :title "Declaration, not discovery"
(p "Boundary requirements are declared at scope boundaries, not discovered at call time. This is the critical constraint. A generative program that synthesizes a " (code "with-boundary") " block is declaring — at generation time — what the block will need. The declaration is inspectable before execution. You can analyze a generated program's boundary requirements without running it.")
(p "This gives constraint checking on generated code. A generative loop that produces a " (code "with-boundary") " block must produce a valid boundary declaration. If the generated block calls " (code "gpu-exec") " but doesn't declare " (code "media-processing") ", the boundary checker rejects it — at generation time, not at runtime. The program must say what it needs before it needs it."))
(~doc-subsection :title "Nested migration"
(~docs/subsection :title "Nested migration"
(p "Nested " (code "with-boundary") " blocks are nested migrations. The program walks the capability graph, carrying its state, accumulating content-addressed history. Each migration is an edge in the DAG — the source environment, the target environment, the serialized state, the pending expression. All content-addressed. All auditable.")
(p "A three-level nesting — browser to render node to ingest server — is three migrations. The browser evaluates the outer expression, hits a " (code "with-boundary") " requiring GPU, migrates to the render node. The render node evaluates until it hits a " (code "with-boundary") " requiring live ingest, migrates to the ingest server. Each migration carries the accumulated environment. Each return ships results back up the chain.")
(p "The nesting depth is bounded by the capability graph. If there are four distinct environment types, the maximum nesting is four. In practice, most programs need one or two migrations. The deep nesting is there for generative programs that discover capabilities as they run."))
(~doc-subsection :title "Environment chaining"
(~docs/subsection :title "Environment chaining"
(p "Split execution — cached layers on one host, GPU rendering on another, encoding on a third — is just environment chaining. The evaluator runs in one environment until it hits a primitive requiring a different one. The primitive " (em "is") " the dispatch.")
(p "This collapses the distinction between \"local function call\" and \"remote service invocation.\" From the SX program's perspective, " (code "gpu-exec") " is a primitive. Whether it runs on the local GPU or a remote render farm is an environment configuration detail, not a language-level concern. The " (code "with-boundary") " block declares the requirement. The runtime satisfies it. The program doesn't care how.")
(p "Environment chaining also explains the Art DAG's three-phase execution pattern (analyze, plan, execute). Each phase runs in a different environment with different primitives. The analyze phase needs " (code "content-hash") " and " (code "list-files") ". The plan phase needs " (code "env-snapshot") " and scheduling primitives. The execute phase needs " (code "gpu-exec") " and storage primitives. Three " (code "with-boundary") " blocks. Three environments. One program.")))
@@ -251,9 +251,9 @@
;; IX. Implementation phases
;; =====================================================================
(~doc-section :title "Implementation phases" :id "phases"
(~docs/section :title "Implementation phases" :id "phases"
(~doc-subsection :title "Phase 0: Generative primitives"
(~docs/subsection :title "Phase 0: Generative primitives"
(p "Add the minimal set of primitives needed for a generative loop. These are IO primitives — they cross the boundary.")
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
(table :class "w-full text-left text-sm"
@@ -288,7 +288,7 @@
(td :class "px-3 py-2 text-stone-600" "SHA3-256 hash of source string")))))
(p "These are the building blocks. The generative loop composes them. The primitives themselves are minimal — no networking, no databases, no UI. Just: read, write, evaluate, inspect, hash."))
(~doc-subsection :title "Phase 1: Self-compiling spec"
(~docs/subsection :title "Phase 1: Self-compiling spec"
(p "Rewrite " (code "bootstrap_js.py") " as " (code "bootstrap.sx") ". The bootstrapper becomes an SX program that reads the spec files and emits target code.")
(ol :class "list-decimal list-inside space-y-2 text-stone-600"
(li "Write " (code "codegen-js.sx") " — JavaScript code generation adapter (emit JS from SX AST)")
@@ -298,19 +298,19 @@
(li "Retire the Python bootstrappers"))
(p "This is the first real generative program: SX reading SX and writing JavaScript. The same program, with a different adapter, writes Python. Or Go. Or WASM. The spec doesn't change. Only the adapter changes."))
(~doc-subsection :title "Phase 2: Generative deps"
(~docs/subsection :title "Phase 2: Generative deps"
(p "Rewrite " (code "deps.sx") " as a generative program. Instead of computing a static dependency graph, it runs continuously: watch for new component definitions, update the graph, re-emit optimized bundles.")
(p "This is the deps analyzer turned inside out. Instead of \"analyze all components, output a graph,\" it's \"when a new component appears, update the running graph.\" The dependency analysis is an ongoing computation, not a one-shot pass."))
(~doc-subsection :title "Phase 3: Generative testing"
(~docs/subsection :title "Phase 3: Generative testing"
(p "Connect " (code "prove.sx") " to the generative loop. When a new function is defined, automatically generate property tests, run them, report failures. When a function changes, regenerate and rerun only the affected tests.")
(p "The test suite is not a separate artifact — it's a side effect of the generative process. Every function that enters the environment is tested. The tests are generated from properties, not hand-written. The program verifies itself as it grows."))
(~doc-subsection :title "Phase 4: The self-extending server"
(~docs/subsection :title "Phase 4: The self-extending server"
(p "An SX server with a generative core. New routes, handlers, and middleware can be added at runtime by evaluating SX source. The server's API surface is a living environment that grows with use.")
(p "Not a scripting layer bolted onto a framework — the server " (em "is") " a generative SX program. Its routes are SX definitions. Its middleware is SX functions. Adding a new endpoint means evaluating a new " (code "defhandler") " in the running environment."))
(~doc-subsection :title "Phase 5: The seed network"
(~docs/subsection :title "Phase 5: The seed network"
(p "Multiple generative SX programs exchanging source. Each node runs a seed. When node A discovers a capability it lacks, it requests the source from node B. Node B's generated code is content-addressed — A can verify it, evaluate it, and grow.")
(p "This is SX-Activity applied to generative programs. The wire format is SX. The content is SX. The evaluation is SX. The programs share source, not data. They grow together.")))
@@ -318,7 +318,7 @@
;; X. The strange loop
;; =====================================================================
(~doc-section :title "The strange loop" :id "strange-loop"
(~docs/section :title "The strange loop" :id "strange-loop"
(p "Hofstadter's strange loop: a hierarchy of levels where the top level reaches back down and affects the bottom level. In a generative SX program:")
(ul :class "list-disc pl-5 space-y-2 text-stone-600"
(li "The bottom level is the evaluator — it evaluates expressions")

View File

@@ -2,14 +2,14 @@
;; Glue Decoupling
;; ---------------------------------------------------------------------------
(defcomp ~plan-glue-decoupling-content ()
(~doc-page :title "Cross-App Decoupling via Glue Services"
(defcomp ~plans/glue-decoupling/plan-glue-decoupling-content ()
(~docs/page :title "Cross-App Decoupling via Glue Services"
(~doc-section :title "Context" :id "context"
(~docs/section :title "Context" :id "context"
(p "All cross-domain FK constraints have been dropped (with pragmatic exceptions for OrderItem.product_id and CartItem). Cross-domain writes go through internal HTTP and activity bus. However, " (strong "25+ cross-app model imports remain") " — apps still import from each other's models/ directories. This means every app needs every other app's code on disk to start.")
(p "The goal: eliminate all cross-app model imports. Every app only imports from its own models/, from shared/, and from a new glue/ service layer."))
(~doc-section :title "Current state" :id "current"
(~docs/section :title "Current state" :id "current"
(p "Apps are partially decoupled via HTTP interfaces (fetch_data, call_action, send_internal_activity) and DTOs. The Cart microservice split (relations, likes, orders) is complete. But direct model imports persist in:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "Cart") " — 9 files importing from market, events, blog")
@@ -17,7 +17,7 @@
(li (strong "Events") " — 5 files importing from blog, market, cart")
(li (strong "Market") " — 1 file importing from blog")))
(~doc-section :title "What remains" :id "remains"
(~docs/section :title "What remains" :id "remains"
(div :class "space-y-3"
(div :class "rounded border border-stone-200 p-3"
(h4 :class "font-semibold text-stone-700" "1. glue/services/pages.py")
@@ -41,7 +41,7 @@
(h4 :class "font-semibold text-stone-700" "7. Model registration + cleanup")
(p :class "text-sm text-stone-600" "register_models() in glue/setup.py, update all app.py files, delete moved service files."))))
(~doc-section :title "Docker consideration" :id "docker"
(~docs/section :title "Docker consideration" :id "docker"
(p :class "text-stone-600" "For glue services to work in Docker (single app per container), model files from other apps must be importable. Recommended: try/except at import time — glue services that can't import a model raise ImportError at call time, which only happens if called from the wrong app."))))
;; ---------------------------------------------------------------------------

View File

@@ -4,8 +4,8 @@
;; Plans index page
;; ---------------------------------------------------------------------------
(defcomp ~plans-index-content ()
(~doc-page :title "Plans"
(defcomp ~plans/index/plans-index-content ()
(~docs/page :title "Plans"
(div :class "space-y-4"
(p :class "text-lg text-stone-600 mb-4"
"Architecture roadmaps and implementation plans for SX.")

View File

@@ -2,14 +2,14 @@
;; Isomorphic Architecture Roadmap
;; ---------------------------------------------------------------------------
(defcomp ~plan-isomorphic-content ()
(~doc-page :title "Isomorphic Architecture Roadmap"
(defcomp ~plans/isomorphic/plan-isomorphic-content ()
(~docs/page :title "Isomorphic Architecture Roadmap"
(~doc-section :title "Context" :id "context"
(~docs/section :title "Context" :id "context"
(p "SX has a working server-client pipeline: server evaluates pages with IO (DB, fragments), serializes as SX wire format, client parses and renders to DOM. The language and primitives are already isomorphic " (em "— same spec, same semantics, both sides.") " What's missing is the " (strong "plumbing") " that makes the boundary between server and client a sliding window rather than a fixed wall.")
(p "The key insight: " (strong "s-expressions can partially unfold on the server after IO, then finish unfolding on the client.") " The system knows which components have data fetches (via IO detection in " (a :href "/sx/(language.(spec.deps))" :class "text-violet-700 underline" "deps.sx") "), resolves those server-side, and sends the rest as pure SX for client rendering. The boundary slides automatically based on what each component actually needs."))
(~doc-section :title "Current State" :id "current-state"
(~docs/section :title "Current State" :id "current-state"
(ul :class "space-y-2 text-stone-700 list-disc pl-5"
(li (strong "Primitive parity: ") "100%. ~80 pure primitives, same names/semantics, JS and Python.")
(li (strong "eval/parse/render: ") "Complete both sides. sx-ref.js has eval, parse, render-to-html, render-to-dom, aser.")
@@ -21,13 +21,13 @@
(li (strong "IO detection: ") "deps.sx classifies every component as pure or IO-dependent. Server expands IO components, serializes pure ones for client.")
(li (strong "Client-side routing: ") "router.sx matches URL patterns. Pure pages render instantly without server roundtrips. Pages with :data fall through to server transparently.")
(li (strong "Client IO proxy: ") "IO primitives registered on the client call back to the server via fetch. Components with IO deps can render client-side.")
(li (strong "Streaming/suspense: ") "defpage :stream true enables chunked HTML. ~suspense placeholders show loading skeletons; __sxResolve() fills in content as IO completes.")))
(li (strong "Streaming/suspense: ") "defpage :stream true enables chunked HTML. ~shared:pages/suspense placeholders show loading skeletons; __sxResolve() fills in content as IO completes.")))
;; -----------------------------------------------------------------------
;; Phase 1
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 1: Component Distribution & Dependency Analysis" :id "phase-1"
(~docs/section :title "Phase 1: Component Distribution & Dependency Analysis" :id "phase-1"
(div :class "rounded border border-green-300 bg-green-50 p-4 mb-4"
(div :class "flex items-center gap-2 mb-2"
@@ -37,10 +37,10 @@
(p :class "text-green-900 font-medium" "What it enables")
(p :class "text-green-800" "Per-page component bundles instead of sending every definition to every page. Smaller payloads, faster boot, better cache hit rates."))
(~doc-subsection :title "The Problem"
(~docs/subsection :title "The Problem"
(p "The page boot payload serializes every component definition in the environment. A page that uses 5 components still receives all 50+. No mechanism determines which components a page actually needs — the boundary between \"loaded\" and \"used\" is invisible."))
(~doc-subsection :title "Implementation"
(~docs/subsection :title "Implementation"
(p "The dependency analysis algorithm is defined in "
(a :href "/sx/(language.(spec.deps))" :class "text-violet-700 underline" "deps.sx")
@@ -50,7 +50,7 @@
(div
(h4 :class "font-semibold text-stone-700" "1. Transitive closure (deps.sx)")
(p "9 functions that walk the component graph. The core:")
(~doc-code :code (highlight "(define (transitive-deps name env)\n (let ((key (if (starts-with? name \"~\") name\n (concat \"~\" name)))\n (seen (set-create)))\n (transitive-deps-walk key env seen)\n (set-remove seen key)))" "lisp"))
(~docs/code :code (highlight "(define (transitive-deps name env)\n (let ((key (if (starts-with? name \"~\") name\n (concat \"~\" name)))\n (seen (set-create)))\n (transitive-deps-walk key env seen)\n (set-remove seen key)))" "lisp"))
(p (code "scan-refs") " walks a component body AST collecting " (code "~") " symbols. "
(code "transitive-deps") " follows references recursively through the env. "
(code "compute-all-deps") " batch-computes and caches deps for every component. "
@@ -58,8 +58,8 @@
(div
(h4 :class "font-semibold text-stone-700" "2. Page scanning")
(~doc-code :code (highlight "(define (components-needed page-source env)\n (let ((direct (scan-components-from-source page-source))\n (all-needed (set-create)))\n (for-each (fn (name) ...\n (set-add! all-needed name)\n (set-union! all-needed (component-deps comp)))\n direct)\n all-needed))" "lisp"))
(p (code "scan-components-from-source") " finds " (code "(~name") " patterns in serialized SX via regex. " (code "components-needed") " combines scanning with the cached transitive closure to produce the minimal component set for a page."))
(~docs/code :code (highlight "(define (components-needed page-source env)\n (let ((direct (scan-components-from-source page-source))\n (all-needed (set-create)))\n (for-each (fn (name) ...\n (set-add! all-needed name)\n (set-union! all-needed (component-deps comp)))\n direct)\n all-needed))" "lisp"))
(p (code "scan-components-from-source") " finds " (code "(~plans/content-addressed-components/name") " patterns in serialized SX via regex. " (code "components-needed") " combines scanning with the cached transitive closure to produce the minimal component set for a page."))
(div
(h4 :class "font-semibold text-stone-700" "3. Per-page CSS scoping")
@@ -74,13 +74,13 @@
(li (code "env-components") " — enumerate all component entries in an environment")
(li (code "regex-find-all") " / " (code "scan-css-classes") " — host-native regex and CSS scanning")))))
(~doc-subsection :title "Spec module"
(~docs/subsection :title "Spec module"
(p "deps.sx is loaded as a " (strong "spec module") " — an optional extension to the core spec. The bootstrapper flag " (code "--spec-modules deps") " includes it in the generated output alongside the core evaluator, parser, and renderer. Phase 2 IO detection was added to the same module — same bootstrapping mechanism, no architecture changes needed.")
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "shared/sx/ref/deps.sx — canonical spec (14 functions, 8 platform declarations)")
(li "Bootstrapped to all host targets via --spec-modules deps")))
(~doc-subsection :title "Verification"
(~docs/subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "15 dedicated tests: scan, transitive closure, circular deps, compute-all, components-needed")
(li "Bootstrapped output verified on both host targets")
@@ -91,7 +91,7 @@
;; Phase 2
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 2: Smart Server/Client Boundary — IO Detection" :id "phase-2"
(~docs/section :title "Phase 2: Smart Server/Client Boundary — IO Detection" :id "phase-2"
(div :class "rounded border border-green-300 bg-green-50 p-4 mb-4"
(div :class "flex items-center gap-2 mb-2"
@@ -101,7 +101,7 @@
(p :class "text-green-900 font-medium" "What it enables")
(p :class "text-green-800" "Automatic IO detection and selective expansion. Server expands IO-dependent components, serializes pure ones for client. Per-component intelligence replaces global toggle."))
(~doc-subsection :title "IO Detection in the Spec"
(~docs/subsection :title "IO Detection in the Spec"
(p "Five new functions in "
(a :href "/sx/(language.(spec.deps))" :class "text-violet-700 underline" "deps.sx")
" extend the Phase 1 walker to detect IO primitive references:")
@@ -110,12 +110,12 @@
(div
(h4 :class "font-semibold text-stone-700" "1. IO scanning")
(p (code "scan-io-refs") " walks an AST node, collecting symbol names that match an IO name set. The IO set is provided by the host from boundary declarations (all three tiers: core IO, deployment IO, page helpers).")
(~doc-code :code (highlight "(define scan-io-refs\n (fn (node io-names)\n (let ((refs (list)))\n (scan-io-refs-walk node io-names refs)\n refs)))" "lisp")))
(~docs/code :code (highlight "(define scan-io-refs\n (fn (node io-names)\n (let ((refs (list)))\n (scan-io-refs-walk node io-names refs)\n refs)))" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "2. Transitive IO closure")
(p (code "transitive-io-refs") " follows component deps recursively, unioning IO refs from all reachable components and macros. Cycle-safe via seen-set.")
(~doc-code :code (highlight "(define transitive-io-refs\n (fn (name env io-names)\n ;; Walk deps, scan each body for IO refs,\n ;; union all refs transitively.\n ...))" "lisp")))
(~docs/code :code (highlight "(define transitive-io-refs\n (fn (name env io-names)\n ;; Walk deps, scan each body for IO refs,\n ;; union all refs transitively.\n ...))" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "3. Batch computation")
@@ -125,7 +125,7 @@
(h4 :class "font-semibold text-stone-700" "4. Component metadata")
(p "Each component now carries " (code "io_refs") " (transitive IO primitive names) alongside " (code "deps") " and " (code "css_classes") ". The derived " (code "is_pure") " property is true when " (code "io_refs") " is empty — the component can render anywhere without server data."))))
(~doc-subsection :title "Selective Expansion"
(~docs/subsection :title "Selective Expansion"
(p "The partial evaluator " (code "_aser") " now uses per-component IO metadata instead of a global toggle:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "IO-dependent") " → expand server-side (IO must resolve)")
@@ -133,13 +133,13 @@
(li (strong "Layout slot context") " → all components still expand (backwards compat via " (code "_expand_components") " context var)"))
(p "A component calling " (code "(highlight ...)") " or " (code "(query ...)") " is IO-dependent. A component with only HTML tags and string ops is pure."))
(~doc-subsection :title "Platform interface additions"
(~docs/subsection :title "Platform interface additions"
(p "Two new platform functions each host implements:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "(component-io-refs c) → cached IO ref list")
(li "(component-set-io-refs! c refs) → cache IO refs on component")))
(~doc-subsection :title "Verification"
(~docs/subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Components calling (query ...) or (highlight ...) classified IO-dependent")
(li "Pure components (HTML-only) classified pure with empty io_refs")
@@ -151,7 +151,7 @@
;; Phase 3
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 3: Client-Side Routing (SPA Mode)" :id "phase-3"
(~docs/section :title "Phase 3: Client-Side Routing (SPA Mode)" :id "phase-3"
(div :class "rounded border border-green-300 bg-green-50 p-4 mb-4"
(div :class "flex items-center gap-2 mb-2"
@@ -161,20 +161,20 @@
(p :class "text-green-900 font-medium" "What it enables")
(p :class "text-green-800" "After initial page load, pure pages render instantly without server roundtrips. Client matches routes locally, evaluates content expressions with cached components, and only falls back to server for pages with :data dependencies."))
(~doc-subsection :title "Architecture"
(~docs/subsection :title "Architecture"
(p "Three-layer approach: spec defines pure route matching, page registry bridges server metadata to client, orchestration intercepts navigation for try-first/fallback.")
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Route matching spec (router.sx)")
(p "New spec module with pure functions for Flask-style route pattern matching:")
(~doc-code :code (highlight "(define split-path-segments ;; \"/language/docs/hello\" → (\"docs\" \"hello\")\n(define parse-route-pattern ;; \"/language/docs/<slug>\" → segment descriptors\n(define match-route-segments ;; segments + pattern → params dict or nil\n(define find-matching-route ;; path + route table → first match" "lisp"))
(~docs/code :code (highlight "(define split-path-segments ;; \"/language/docs/hello\" → (\"docs\" \"hello\")\n(define parse-route-pattern ;; \"/language/docs/<slug>\" → segment descriptors\n(define match-route-segments ;; segments + pattern → params dict or nil\n(define find-matching-route ;; path + route table → first match" "lisp"))
(p "No platform interface needed — uses only pure string and list primitives. Bootstrapped to both hosts via " (code "--spec-modules deps,router") "."))
(div
(h4 :class "font-semibold text-stone-700" "2. Page registry")
(p "Server serializes defpage metadata as SX dict literals inside " (code "<script type=\"text/sx-pages\">") ". Each entry carries name, path pattern, auth level, has-data flag, serialized content expression, and closure values.")
(~doc-code :code (highlight "{:name \"docs-page\" :path \"/language/docs/<slug>\"\n :auth \"public\" :has-data false\n :content \"(case slug ...)\" :closure {}}" "lisp"))
(~docs/code :code (highlight "{:name \"docs-page\" :path \"/language/docs/<slug>\"\n :auth \"public\" :has-data false\n :content \"(case slug ...)\" :closure {}}" "lisp"))
(p "boot.sx processes these at startup using the SX parser — the same " (code "parse") " function from parser.sx — building route entries with parsed patterns into the " (code "_page-routes") " table. No JSON dependency."))
(div
@@ -188,7 +188,7 @@
(li "If anything fails (no match, has data, eval error): transparent fallback to server fetch"))
(p (code "handle-popstate") " also tries client routing before server fetch on back/forward."))))
(~doc-subsection :title "What becomes client-routable"
(~docs/subsection :title "What becomes client-routable"
(p "All pages with content expressions — most of this docs app. Pure pages render instantly; :data pages fetch data then render client-side (Phase 4):")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code "/") ", " (code "/language/docs/") ", " (code "/language/docs/<slug>") " (most slugs), " (code "/applications/protocols/") ", " (code "/applications/protocols/<slug>"))
@@ -202,11 +202,11 @@
(li (code "/geography/isomorphism/bundle-analyzer") " (has " (code ":data (bundle-analyzer-data)") ")")
(li (code "/geography/isomorphism/data-test") " (has " (code ":data (data-test-data)") " — " (a :href "/sx/(geography.(isomorphism.data-test))" :class "text-violet-700 underline" "Phase 4 demo") ")")))
(~doc-subsection :title "Try-first/fallback design"
(~docs/subsection :title "Try-first/fallback design"
(p "Client routing uses a try-first approach: attempt local evaluation in a try/catch, fall back to server fetch on any failure. This avoids needing perfect static analysis of content expressions — if a content expression calls a page helper the client doesn't have, the eval throws, and the server handles it transparently.")
(p "Console messages provide visibility: " (code "sx:route client /essays/why-sexps") " vs " (code "sx:route server /specs/eval") "."))
(~doc-subsection :title "Files"
(~docs/subsection :title "Files"
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "shared/sx/ref/router.sx — route pattern matching spec")
(li "shared/sx/ref/boot.sx — process page registry scripts")
@@ -215,7 +215,7 @@
(li "shared/sx/ref/bootstrap_py.py — router spec module (parity)")
(li "shared/sx/helpers.py — page registry SX serialization")))
(~doc-subsection :title "Verification"
(~docs/subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Pure page navigation: zero server requests, console shows \"sx:route client\"")
(li "IO/data page fallback: falls through to server fetch transparently")
@@ -227,7 +227,7 @@
;; Phase 4
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 4: Client Async & IO Bridge" :id "phase-4"
(~docs/section :title "Phase 4: Client Async & IO Bridge" :id "phase-4"
(div :class "rounded border border-green-300 bg-green-50 p-4 mb-4"
(div :class "flex items-center gap-2 mb-2"
@@ -236,14 +236,14 @@
(p :class "text-green-900 font-medium" "What it enables")
(p :class "text-green-800" "Client fetches server-evaluated data and renders :data pages locally. Data cached with TTL to avoid redundant fetches on back/forward navigation. All IO stays server-side — no continuations needed."))
(~doc-subsection :title "Architecture"
(~docs/subsection :title "Architecture"
(p "Separates IO from rendering. Server evaluates :data expression (async, with DB/service access), serializes result as SX wire format. Client fetches pre-evaluated data, parses it, merges into env, renders pure :content client-side.")
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Abstract resolve-page-data")
(p "Spec-level primitive in orchestration.sx. The spec says \"I need data for this page\" — platform provides transport:")
(~doc-code :code (highlight "(resolve-page-data page-name params\n (fn (data)\n ;; data is a dict — merge into env and render\n (let ((env (merge closure params data))\n (rendered (try-eval-content content-src env)))\n (swap-rendered-content target rendered pathname))))" "lisp"))
(~docs/code :code (highlight "(resolve-page-data page-name params\n (fn (data)\n ;; data is a dict — merge into env and render\n (let ((env (merge closure params data))\n (rendered (try-eval-content content-src env)))\n (swap-rendered-content target rendered pathname))))" "lisp"))
(p "Browser platform: HTTP fetch to " (code "/sx/data/<page-name>") ". Future platforms could use IPC, cache, WebSocket, etc."))
(div
@@ -260,7 +260,7 @@
(li "After TTL: stale entry evicted, fresh fetch on next visit"))
(p "Try it: navigate to the " (a :href "/sx/(geography.(isomorphism.data-test))" :class "text-violet-700 underline" "data test page") ", go back, return within 30s — the server-time stays the same (cached). Wait 30s+ and return — new time (fresh fetch)."))))
(~doc-subsection :title "Files"
(~docs/subsection :title "Files"
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "shared/sx/ref/orchestration.sx — resolve-page-data spec, data cache")
(li "shared/sx/ref/bootstrap_js.py — platform resolvePageData (HTTP fetch)")
@@ -269,7 +269,7 @@
(li "sx/sx/data-test.sx — test component")
(li "shared/sx/tests/test_page_data.py — 30 unit tests")))
(~doc-subsection :title "Verification"
(~docs/subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "30 unit tests: serialize roundtrip, kebab-case, deps, full pipeline simulation, cache TTL")
(li "Console: " (code "sx:route client+data") " on first visit, " (code "sx:route client+cache") " on return within 30s")
@@ -280,7 +280,7 @@
;; Phase 5
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 5: Client IO Proxy" :id "phase-5"
(~docs/section :title "Phase 5: Client IO Proxy" :id "phase-5"
(div :class "rounded border border-green-300 bg-green-50 p-4 mb-4"
(div :class "flex items-center gap-2 mb-2"
@@ -288,7 +288,7 @@
(p :class "text-green-900 font-medium" "What it enables")
(p :class "text-green-800" "Components with IO dependencies render client-side. IO primitives are proxied to the server — the client evaluator calls them like normal functions, the proxy fetches results via HTTP, the async DOM renderer awaits the promises and continues."))
(~doc-subsection :title "How it works"
(~docs/subsection :title "How it works"
(p "Instead of async-aware continuations (originally planned), Phase 5 was solved by combining three mechanisms that emerged from Phases 3-4:")
(div :class "space-y-4"
@@ -304,18 +304,18 @@
(h4 :class "font-semibold text-stone-700" "3. Async DOM renderer")
(p (code "asyncRenderToDom") " walks the expression tree and handles Promises transparently. When a subexpression returns a Promise (from an IO proxy call), the renderer awaits it and continues building the DOM tree. No continuations needed — JavaScript's native Promise mechanism provides the suspension."))))
(~doc-subsection :title "Why continuations weren't needed"
(~docs/subsection :title "Why continuations weren't needed"
(p "The original Phase 5 plan called for async-aware shift/reset or a CPS transform of the evaluator. In practice, JavaScript's Promise mechanism provided the same capability: the async DOM renderer naturally suspends when it encounters a Promise and resumes when it resolves.")
(p "Delimited continuations remain valuable for Phase 6 (streaming/suspense on the " (em "server") " side, where Python doesn't have native Promise-based suspension in the evaluator). But for client-side IO, Promises + async render were sufficient."))
(~doc-subsection :title "Files"
(~docs/subsection :title "Files"
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "shared/sx/ref/orchestration.sx — registerIoDeps, IO proxy registration")
(li "shared/sx/ref/bootstrap_js.py — asyncRenderToDom, IO proxy HTTP transport")
(li "shared/sx/helpers.py — io_deps in page registry entries")
(li "shared/sx/deps.py — transitive IO ref computation")))
(~doc-subsection :title "Verification"
(~docs/subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Navigate to any page with IO deps (e.g. /testing/eval) — console shows IO proxy calls")
(li "Components using " (code "highlight") " render correctly via proxy")
@@ -326,7 +326,7 @@
;; Phase 6
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 6: Streaming & Suspense" :id "phase-6"
(~docs/section :title "Phase 6: Streaming & Suspense" :id "phase-6"
(div :class "rounded border border-green-300 bg-green-50 p-4 mb-4"
(div :class "flex items-center gap-2 mb-2"
@@ -335,9 +335,9 @@
(p :class "text-green-900 font-medium" "What it enables")
(p :class "text-green-800" "Server streams partially-evaluated SX as IO resolves. Client renders available subtrees immediately with loading skeletons, fills in suspended parts as data arrives."))
(~doc-subsection :title "What was built"
(~docs/subsection :title "What was built"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code "~suspense") " component — renders fallback content with a stable DOM ID, replaced when resolution arrives")
(li (code "~shared:pages/suspense") " component — renders fallback content with a stable DOM ID, replaced when resolution arrives")
(li (code "defpage :stream true") " — opts a page into streaming response mode")
(li (code "defpage :fallback expr") " — custom loading skeleton for streaming pages")
(li (code "execute_page_streaming()") " — Quart async generator response that yields HTML chunks")
@@ -347,13 +347,13 @@
(li "Concurrent IO: data eval + header eval run in parallel via " (code "asyncio.create_task"))
(li "Completion-order streaming: whichever IO finishes first gets sent first via " (code "asyncio.wait(FIRST_COMPLETED)"))))
(~doc-subsection :title "Architecture"
(~docs/subsection :title "Architecture"
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Suspense component")
(p "When streaming, the server renders the page with " (code "~suspense") " placeholders instead of awaiting IO:")
(~doc-code :code (highlight "(~app-body\n :header-rows (~suspense :id \"stream-headers\"\n :fallback (div :class \"h-12 bg-stone-200 animate-pulse\"))\n :content (~suspense :id \"stream-content\"\n :fallback (div :class \"p-8 animate-pulse\" ...)))" "lisp")))
(p "When streaming, the server renders the page with " (code "~shared:pages/suspense") " placeholders instead of awaiting IO:")
(~docs/code :code (highlight "(~app-body\n :header-rows (~shared:pages/suspense :id \"stream-headers\"\n :fallback (div :class \"h-12 bg-stone-200 animate-pulse\"))\n :content (~shared:pages/suspense :id \"stream-content\"\n :fallback (div :class \"p-8 animate-pulse\" ...)))" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "2. Chunked transfer")
@@ -366,19 +366,19 @@
(div
(h4 :class "font-semibold text-stone-700" "3. Client resolution")
(p "Each resolution chunk is an inline script:")
(~doc-code :code (highlight "<script>\n window.__sxResolve(\"stream-content\",\n \"(~article :title \\\"Hello\\\")\")\n</script>" "html"))
(~docs/code :code (highlight "<script>\n window.__sxResolve(\"stream-content\",\n \"(~article :title \\\"Hello\\\")\")\n</script>" "html"))
(p "The client parses the SX, renders to DOM, and replaces the suspense placeholder's children."))
(div
(h4 :class "font-semibold text-stone-700" "4. Concurrent IO")
(p "Data evaluation and header construction run in parallel. " (code "asyncio.wait(FIRST_COMPLETED)") " yields resolution chunks in whatever order IO completes — no artificial sequencing."))))
(~doc-subsection :title "Continuation foundation"
(~docs/subsection :title "Continuation foundation"
(p "Delimited continuations (" (code "reset") "/" (code "shift") ") are implemented in the Python evaluator (async_eval.py lines 586-624) and available as special forms. Phase 6 uses the simpler pattern of concurrent IO + completion-order streaming, but the continuation machinery is in place for Phase 7's more sophisticated evaluation-level suspension."))
(~doc-subsection :title "Files"
(~docs/subsection :title "Files"
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "shared/sx/templates/pages.sx — ~suspense component definition")
(li "shared/sx/templates/pages.sx — ~shared:pages/suspense component definition")
(li "shared/sx/types.py — PageDef.stream, PageDef.fallback_expr fields")
(li "shared/sx/evaluator.py — defpage :stream/:fallback parsing")
(li "shared/sx/pages.py — execute_page_streaming(), streaming route mounting")
@@ -391,7 +391,7 @@
(li "sx/sxc/pages/docs.sx — streaming-demo defpage")
(li "sx/sxc/pages/helpers.py — streaming-demo-data page helper")))
(~doc-subsection :title "Demonstration"
(~docs/subsection :title "Demonstration"
(p "The " (a :href "/sx/(geography.(isomorphism.streaming))" :class "text-violet-700 underline" "streaming demo page") " exercises the full pipeline:")
(ol :class "list-decimal pl-5 text-stone-700 space-y-1"
(li "Navigate to " (a :href "/sx/(geography.(isomorphism.streaming))" :class "text-violet-700 underline" "/sx/(geography.(isomorphism.streaming))"))
@@ -400,10 +400,10 @@
(li "Open the Network tab — observe " (code "Transfer-Encoding: chunked") " on the document response")
(li "The document response shows multiple chunks arriving over time: shell first, then resolution scripts")))
(~doc-subsection :title "What to verify"
(~docs/subsection :title "What to verify"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "Instant shell: ") "The page HTML arrives immediately — no waiting for the 1.5s data fetch")
(li (strong "Suspense placeholders: ") "The " (code "~suspense") " component renders a " (code "data-suspense") " wrapper with animated fallback content")
(li (strong "Suspense placeholders: ") "The " (code "~shared:pages/suspense") " component renders a " (code "data-suspense") " wrapper with animated fallback content")
(li (strong "Resolution: ") "The " (code "__sxResolve()") " inline script replaces the placeholder with real rendered content")
(li (strong "Chunked encoding: ") "Network tab shows the document as a chunked response with multiple frames")
(li (strong "Concurrent IO: ") "Header and content resolve independently — whichever finishes first appears first")
@@ -413,7 +413,7 @@
;; Phase 7
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 7: Full Isomorphism" :id "phase-7"
(~docs/section :title "Phase 7: Full Isomorphism" :id "phase-7"
(div :class "rounded border border-green-200 bg-green-50 p-4 mb-4"
(div :class "flex items-center gap-2 mb-2"
@@ -421,7 +421,7 @@
(p :class "text-green-900 font-medium" "What it enables")
(p :class "text-green-800" "Same SX code runs on either side. Runtime chooses optimal split via affinity annotations and render plans. Client data cache managed via invalidation headers and server-driven updates. Cross-host isomorphism verified by 61 automated tests."))
(~doc-subsection :title "7a. Affinity Annotations & Render Target"
(~docs/subsection :title "7a. Affinity Annotations & Render Target"
(div :class "rounded border border-green-300 bg-green-50 p-3 mb-4"
(div :class "flex items-center gap-2 mb-1"
@@ -429,7 +429,7 @@
(p :class "text-green-800 text-sm" "Components declare where they prefer to render. The spec combines affinity with IO analysis to produce a per-component render target decision."))
(p "Affinity annotations let component authors express rendering preferences:")
(~doc-code :code (highlight "(defcomp ~product-grid (&key products)\n :affinity :client ;; interactive, prefer client rendering\n (div ...))\n\n(defcomp ~auth-menu (&key user)\n :affinity :server ;; auth-sensitive, always server\n (div ...))\n\n(defcomp ~card (&key title)\n ;; no annotation = :affinity :auto (default)\n ;; runtime decides from IO analysis\n (div ...))" "lisp"))
(~docs/code :code (highlight "(defcomp ~plans/isomorphic/product-grid (&key products)\n :affinity :client ;; interactive, prefer client rendering\n (div ...))\n\n(defcomp ~plans/isomorphic/auth-menu (&key user)\n :affinity :server ;; auth-sensitive, always server\n (div ...))\n\n(defcomp ~plans/isomorphic/card (&key title)\n ;; no annotation = :affinity :auto (default)\n ;; runtime decides from IO analysis\n (div ...))" "lisp"))
(p "The " (code "render-target") " function in deps.sx combines affinity with IO analysis:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
@@ -439,7 +439,7 @@
(p "The server's partial evaluator (" (code "_aser") ") uses " (code "render_target") " instead of the previous " (code "is_pure") " check. Components with " (code ":affinity :client") " are serialized for client rendering even if they call IO primitives — the IO proxy (Phase 5) handles the calls.")
(~doc-subsection :title "Files"
(~docs/subsection :title "Files"
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "shared/sx/ref/eval.sx — defcomp annotation parsing, defcomp-kwarg helper")
(li "shared/sx/ref/deps.sx — render-target function, platform interface")
@@ -451,14 +451,14 @@
(li "shared/sx/ref/test-eval.sx — 4 new defcomp affinity tests")
(li "shared/sx/ref/test-deps.sx — 6 new render-target tests")))
(~doc-subsection :title "Verification"
(~docs/subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "269 spec tests pass (10 new: 4 eval + 6 deps)")
(li "79 Python unit tests pass")
(li "Bootstrapped to both hosts (sx_ref.py + sx-browser.js)")
(li "Backward compatible: existing defcomp without :affinity defaults to \"auto\""))))
(~doc-subsection :title "7b. Runtime Boundary Optimizer"
(~docs/subsection :title "7b. Runtime Boundary Optimizer"
(div :class "rounded border border-green-300 bg-green-50 p-3 mb-4"
(div :class "flex items-center gap-2 mb-1"
@@ -468,9 +468,9 @@
(p "Given component tree + IO dependency graph + affinity annotations, decide per-component: server-expand, client-render, or stream. Planning step cached at registration, recomputed on component change.")
(p (code "page-render-plan") " in deps.sx computes per-page boundary decisions:")
(~doc-code :code (highlight "(page-render-plan page-source env io-names)\n;; Returns:\n;; {:components {~name \"server\"|\"client\" ...}\n;; :server (list of server-expanded names)\n;; :client (list of client-rendered names)\n;; :io-deps (IO primitives needed by server components)}" "lisp"))
(~docs/code :code (highlight "(page-render-plan page-source env io-names)\n;; Returns:\n;; {:components {~plans/content-addressed-components/name \"server\"|\"client\" ...}\n;; :server (list of server-expanded names)\n;; :client (list of client-rendered names)\n;; :io-deps (IO primitives needed by server components)}" "lisp"))
(~doc-subsection :title "Integration Points"
(~docs/subsection :title "Integration Points"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code "shared/sx/ref/deps.sx") " — " (code "page-render-plan") " spec function")
(li (code "shared/sx/deps.py") " — Python wrapper, dispatches to bootstrapped code")
@@ -478,13 +478,13 @@
(li (code "shared/sx/helpers.py") " — " (code "_build_pages_sx()") " includes " (code ":render-plan") " in client page registry")
(li (code "shared/sx/types.py") " — " (code "PageDef.render_plan") " field")))
(~doc-subsection :title "Verification"
(~docs/subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "5 new spec tests (page-render-plan suite)")
(li "Render plans visible on " (a :href "/sx/(geography.(isomorphism.affinity))" "affinity demo page"))
(li "Client page registry includes :render-plan for each page"))))
(~doc-subsection :title "7c. Cache Invalidation & Optimistic Data Updates"
(~docs/subsection :title "7c. Cache Invalidation & Optimistic Data Updates"
(div :class "rounded border border-green-300 bg-green-50 p-3 mb-4"
(div :class "flex items-center gap-2 mb-1"
@@ -493,15 +493,15 @@
(p "The client-side page data cache (30-second TTL) now supports cache invalidation, server-driven updates, and optimistic mutations. The client predicts the result of a mutation, immediately re-renders with the predicted data, and confirms or reverts when the server responds.")
(~doc-subsection :title "Cache Invalidation"
(~docs/subsection :title "Cache Invalidation"
(p "Component authors can declare cache invalidation on elements that trigger mutations:")
(~doc-code :code (highlight ";; Clear specific page's cache after successful action\n(form :sx-post \"/cart/remove\"\n :sx-cache-invalidate \"cart-page\"\n ...)\n\n;; Clear ALL page caches after action\n(button :sx-post \"/admin/reset\"\n :sx-cache-invalidate \"*\")" "lisp"))
(~docs/code :code (highlight ";; Clear specific page's cache after successful action\n(form :sx-post \"/cart/remove\"\n :sx-cache-invalidate \"cart-page\"\n ...)\n\n;; Clear ALL page caches after action\n(button :sx-post \"/admin/reset\"\n :sx-cache-invalidate \"*\")" "lisp"))
(p "The server can also control client cache via response headers:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code "SX-Cache-Invalidate: page-name") " — clear cache for a page")
(li (code "SX-Cache-Update: page-name") " — replace cache with the response body (SX-format data)")))
(~doc-subsection :title "Optimistic Mutations"
(~docs/subsection :title "Optimistic Mutations"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "optimistic-cache-update") " — applies a mutator function to cached data, saves a snapshot for rollback")
(li (strong "optimistic-cache-revert") " — restores the pre-mutation snapshot if the server rejects")
@@ -509,19 +509,19 @@
(li (strong "submit-mutation") " — orchestration function: predict, submit, confirm/revert")
(li (strong "/sx/action/<name>") " — server endpoint for processing mutations (POST, returns SX wire format)")))
(~doc-subsection :title "Files"
(~docs/subsection :title "Files"
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "shared/sx/ref/orchestration.sx — cache management + optimistic cache functions + submit-mutation spec")
(li "shared/sx/ref/engine.sx — SX-Cache-Invalidate, SX-Cache-Update response headers")
(li "shared/sx/pages.py — mount_action_endpoint for /sx/action/<name>")
(li "sx/sx/optimistic-demo.sx — live demo component")))
(~doc-subsection :title "Verification"
(~docs/subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Live demo at " (a :href "/sx/(geography.(isomorphism.optimistic))" :class "text-violet-600 hover:underline" "/sx/(geography.(isomorphism.optimistic))"))
(li "Console log: " (code "sx:optimistic confirmed") " / " (code "sx:optimistic reverted")))))
(~doc-subsection :title "7d. Offline Data Layer"
(~docs/subsection :title "7d. Offline Data Layer"
(div :class "rounded border border-green-300 bg-green-50 p-3 mb-4"
(div :class "flex items-center gap-2 mb-1"
@@ -535,7 +535,7 @@
(li (strong "/static/* ") "— stale-while-revalidate via Cache API. Serves cached assets immediately, updates in background.")
(li (strong "Offline mutations") " — " (code "offline-aware-mutation") " routes to " (code "submit-mutation") " when online, " (code "offline-queue-mutation") " when offline. " (code "offline-sync") " replays the queue on reconnect."))
(~doc-subsection :title "How It Works"
(~docs/subsection :title "How It Works"
(ol :class "list-decimal list-inside text-stone-700 space-y-2"
(li "On boot, " (code "sx-browser.js") " registers the SW at " (code "/sx-sw.js") " (root scope)")
(li "SW intercepts fetch events and routes by URL pattern")
@@ -544,20 +544,20 @@
(li "Cache invalidation propagates: element attr / response header → in-memory cache → SW message → IndexedDB")
(li "Offline mutations queue locally, replay on reconnect via " (code "offline-sync"))))
(~doc-subsection :title "Files"
(~docs/subsection :title "Files"
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "shared/static/scripts/sx-sw.js — Service Worker (network-first + stale-while-revalidate)")
(li "shared/sx/ref/orchestration.sx — offline queue, sync, connectivity tracking, sw-post-message")
(li "shared/sx/pages.py — mount_service_worker() serves SW at /sx-sw.js")
(li "sx/sx/offline-demo.sx — live demo component")))
(~doc-subsection :title "Verification"
(~docs/subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Live demo at " (a :href "/sx/(geography.(isomorphism.offline))" :class "text-violet-600 hover:underline" "/sx/(geography.(isomorphism.offline))"))
(li "Test with DevTools Network → Offline mode")
(li "Console log: " (code "sx:offline queued") ", " (code "sx:offline syncing") ", " (code "sx:offline synced")))))
(~doc-subsection :title "7e. Isomorphic Testing"
(~docs/subsection :title "7e. Isomorphic Testing"
(div :class "rounded border border-green-300 bg-green-50 p-3 mb-4"
(div :class "flex items-center gap-2 mb-1"
@@ -569,12 +569,12 @@
(li "37 eval tests: arithmetic, comparison, strings, collections, logic, let/lambda, higher-order, dict, keywords, cond/case")
(li "24 render tests: elements, attributes, nesting, void elements, boolean attrs, conditionals, map, components, affinity, HTML escaping"))
(~doc-subsection :title "Files"
(~docs/subsection :title "Files"
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "shared/sx/tests/test_isomorphic.py — cross-host test suite")
(li "Run: " (code "python3 -m pytest shared/sx/tests/test_isomorphic.py -q")))))
(~doc-subsection :title "7f. Universal Page Descriptor"
(~docs/subsection :title "7f. Universal Page Descriptor"
(div :class "rounded border border-green-300 bg-green-50 p-3 mb-4"
(div :class "flex items-center gap-2 mb-1"
@@ -595,9 +595,9 @@
;; Cross-Cutting Concerns
;; -----------------------------------------------------------------------
(~doc-section :title "Cross-Cutting Concerns" :id "cross-cutting"
(~docs/section :title "Cross-Cutting Concerns" :id "cross-cutting"
(~doc-subsection :title "Error Reporting"
(~docs/subsection :title "Error Reporting"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Phase 1: \"Unknown component\" includes which page expected it and what bundle was sent")
(li "Phase 2: Server logs which components expanded server-side vs sent to client")
@@ -605,7 +605,7 @@
(li "Phase 4: Client data errors include page name, params, server response status")
(li "Source location tracking in parser → propagate through eval → include in error messages")))
(~doc-subsection :title "Backward Compatibility"
(~docs/subsection :title "Backward Compatibility"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Pages without annotations behave as today")
(li "SX-Request / SX-Components / SX-Css header protocol continues")
@@ -613,14 +613,14 @@
(li "_expand_components continues as override")
(li "Each phase is opt-in: disable → identical to previous behavior")))
(~doc-subsection :title "Spec Integrity"
(~docs/subsection :title "Spec Integrity"
(p "All new behavior specified in .sx files under shared/sx/ref/ before implementation. Bootstrappers transpile from spec. This ensures JS and Python stay in sync.")))
;; -----------------------------------------------------------------------
;; Critical Files
;; -----------------------------------------------------------------------
(~doc-section :title "Critical Files" :id "critical-files"
(~docs/section :title "Critical Files" :id "critical-files"
(div :class "overflow-x-auto rounded border border-stone-200"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-100"

View File

@@ -2,14 +2,14 @@
;; js.sx — Self-Hosting JavaScript Bootstrapper
;; ---------------------------------------------------------------------------
(defcomp ~plan-js-bootstrapper-content ()
(~doc-page :title "js.sx — JavaScript Bootstrapper"
(defcomp ~plans/js-bootstrapper/plan-js-bootstrapper-content ()
(~docs/page :title "js.sx — JavaScript Bootstrapper"
;; -----------------------------------------------------------------------
;; Overview
;; -----------------------------------------------------------------------
(~doc-section :title "Overview" :id "overview"
(~docs/section :title "Overview" :id "overview"
(p (code "bootstrap_js.py") " is a 4,361-line Python program that reads the "
(code ".sx") " spec files and emits " (code "sx-ref.js") " — the entire "
"browser runtime. Parser, evaluator, three rendering adapters (HTML, SX wire, DOM), "
@@ -28,12 +28,12 @@
;; Two Modes
;; -----------------------------------------------------------------------
(~doc-section :title "Two Compilation Modes" :id "modes"
(~docs/section :title "Two Compilation Modes" :id "modes"
(~doc-subsection :title "Mode 1: Spec Bootstrapper"
(~docs/subsection :title "Mode 1: Spec Bootstrapper"
(p "Same job as " (code "bootstrap_js.py") ". Read spec " (code ".sx") " files, "
"emit " (code "sx-ref.js") ".")
(~doc-code :code (highlight ";; Translate eval.sx to JavaScript
(~docs/code :code (highlight ";; Translate eval.sx to JavaScript
(js-translate-file (parse-file \"eval.sx\"))
;; → \"function evalExpr(expr, env) { ... }\"
@@ -44,12 +44,12 @@
(p "The output is identical to " (code "python bootstrap_js.py") ". "
"Verification: " (code "diff <(python bootstrap_js.py) <(python run_js_sx.py)") "."))
(~doc-subsection :title "Mode 2: Component Compiler"
(~docs/subsection :title "Mode 2: Component Compiler"
(p "Server-side SX evaluation + " (code "js.sx") " translation = static JS output. "
"Given a component tree that the server has already evaluated (data fetched, "
"conditionals resolved, loops expanded), " (code "js.sx") " compiles the "
"resulting DOM description into a JavaScript program that builds the same DOM.")
(~doc-code :code (highlight ";; Server evaluates the page (fetches data, expands components)
(~docs/code :code (highlight ";; Server evaluates the page (fetches data, expands components)
;; Result is a resolved SX tree: (div :class \"...\" (h1 \"Hello\") ...)
;; js.sx compiles that tree to standalone JS
@@ -70,7 +70,7 @@
;; Architecture
;; -----------------------------------------------------------------------
(~doc-section :title "Architecture" :id "architecture"
(~docs/section :title "Architecture" :id "architecture"
(p "The JS bootstrapper has more moving parts than the Python one because "
"JavaScript is the " (em "client") " host. The browser runtime includes "
"things Python never needs:")
@@ -141,11 +141,11 @@
;; Translation Rules
;; -----------------------------------------------------------------------
(~doc-section :title "Translation Rules" :id "translation"
(~docs/section :title "Translation Rules" :id "translation"
(p (code "js.sx") " shares the same pattern as " (code "py.sx") " — expression translator, "
"statement translator, name mangling — but with JavaScript-specific mappings:")
(~doc-subsection :title "Name Mangling"
(~docs/subsection :title "Name Mangling"
(p "SX uses kebab-case. JavaScript uses camelCase.")
(div :class "overflow-x-auto rounded border border-stone-200 my-4"
(table :class "w-full text-sm"
@@ -180,7 +180,7 @@
(td :class "px-4 py-2 font-mono" "delete_")
(td :class "px-4 py-2" "JS reserved word escape"))))))
(~doc-subsection :title "Special Forms → JavaScript"
(~docs/subsection :title "Special Forms → JavaScript"
(div :class "overflow-x-auto rounded border border-stone-200 my-4"
(table :class "w-full text-sm"
(thead :class "bg-stone-50"
@@ -216,7 +216,7 @@
(td :class "px-4 py-2 font-mono" "&rest args")
(td :class "px-4 py-2 font-mono" "...args (rest params)"))))))
(~doc-subsection :title "JavaScript Advantages"
(~docs/subsection :title "JavaScript Advantages"
(p "JavaScript is easier to target than Python in two key ways:")
(ul :class "list-disc pl-6 space-y-2 text-stone-700"
(li (strong "No mutation problem. ")
@@ -232,7 +232,7 @@
;; Component Compilation
;; -----------------------------------------------------------------------
(~doc-section :title "Component Compilation" :id "component-compiler"
(~docs/section :title "Component Compilation" :id "component-compiler"
(p "Mode 2 is the interesting one. The server already evaluates SX page "
"definitions — it fetches data, resolves conditionals, expands components, "
"and produces a complete DOM description as an SX tree. Currently this tree "
@@ -245,22 +245,22 @@
"to evaluate. It's just a description of DOM nodes. " (code "js.sx")
" walks this tree and emits imperative JavaScript that constructs the same DOM.")
(~doc-subsection :title "What Gets Compiled"
(~docs/subsection :title "What Gets Compiled"
(p "A resolved SX tree like:")
(~doc-code :code (highlight "(div :class \"container\"
(~docs/code :code (highlight "(div :class \"container\"
(h1 \"Hello\")
(ul (map (fn (item)
(li :class \"item\" (get item \"name\")))
items)))" "lisp"))
(p "After server-side evaluation (with " (code "items") " = "
(code "[{\"name\": \"Alice\"}, {\"name\": \"Bob\"}]") "):")
(~doc-code :code (highlight "(div :class \"container\"
(~docs/code :code (highlight "(div :class \"container\"
(h1 \"Hello\")
(ul
(li :class \"item\" \"Alice\")
(li :class \"item\" \"Bob\")))" "lisp"))
(p "Compiles to:")
(~doc-code :code (highlight "var _0 = document.createElement('div');
(~docs/code :code (highlight "var _0 = document.createElement('div');
_0.className = 'container';
var _1 = document.createElement('h1');
_1.textContent = 'Hello';
@@ -276,7 +276,7 @@ _4.textContent = 'Bob';
_2.appendChild(_4);
_0.appendChild(_2);" "javascript")))
(~doc-subsection :title "Why Not Just Use HTML?"
(~docs/subsection :title "Why Not Just Use HTML?"
(p "HTML already does this — " (code "innerHTML") " parses and builds DOM. "
"Why compile to JS instead?")
(ul :class "list-disc pl-6 space-y-2 text-stone-700"
@@ -301,7 +301,7 @@ _0.appendChild(_2);" "javascript")))
"browser, Node, Deno, Bun. Server-side rendered pages become "
"testable JavaScript programs.")))
(~doc-subsection :title "Hybrid Mode"
(~docs/subsection :title "Hybrid Mode"
(p "Not every page is fully static. Some parts are server-rendered, "
"some are interactive. " (code "js.sx") " handles this with a hybrid approach:")
(ul :class "list-disc pl-6 space-y-2 text-stone-700"
@@ -320,7 +320,7 @@ _0.appendChild(_2);" "javascript")))
;; The Bootstrap Chain
;; -----------------------------------------------------------------------
(~doc-section :title "The Bootstrap Chain" :id "chain"
(~docs/section :title "The Bootstrap Chain" :id "chain"
(p "With both " (code "py.sx") " and " (code "js.sx") ", the full picture:")
(div :class "overflow-x-auto rounded border border-stone-200 my-4"
(table :class "w-full text-sm"
@@ -370,9 +370,9 @@ _0.appendChild(_2);" "javascript")))
;; Implementation Plan
;; -----------------------------------------------------------------------
(~doc-section :title "Implementation" :id "implementation"
(~docs/section :title "Implementation" :id "implementation"
(~doc-subsection :title "Phase 1: Expression Translator"
(~docs/subsection :title "Phase 1: Expression Translator"
(p "Core SX-to-JavaScript expression translation.")
(ul :class "list-disc pl-6 space-y-1 text-stone-700"
(li (code "js-mangle") " — SX name → JavaScript identifier (RENAMES + kebab→camelCase)")
@@ -386,7 +386,7 @@ _0.appendChild(_2);" "javascript")))
(code "===") ", " (code "!==") ", " (code "%"))
(li (code "&rest") " → " (code "...args") " (rest parameters)")))
(~doc-subsection :title "Phase 2: Statement Translator"
(~docs/subsection :title "Phase 2: Statement Translator"
(p "Top-level and function body statement emission.")
(ul :class "list-disc pl-6 space-y-1 text-stone-700"
(li (code "js-statement") " — emit as JavaScript statement")
@@ -396,7 +396,7 @@ _0.appendChild(_2);" "javascript")))
(li (code "do") "/" (code "begin") " → comma expression or block")
(li "Function bodies with multiple expressions → explicit " (code "return"))))
(~doc-subsection :title "Phase 3: Spec Bootstrapper"
(~docs/subsection :title "Phase 3: Spec Bootstrapper"
(p "Process spec files identically to " (code "bootstrap_js.py") ".")
(ul :class "list-disc pl-6 space-y-1 text-stone-700"
(li (code "js-extract-defines") " — parse .sx source, collect top-level defines")
@@ -405,7 +405,7 @@ _0.appendChild(_2);" "javascript")))
(li "Dependency resolution: engine requires dom, boot requires engine + parser")
(li "Static sections (IIFE wrapper, platform interface) stay as string templates")))
(~doc-subsection :title "Phase 4: Component Compiler"
(~docs/subsection :title "Phase 4: Component Compiler"
(p "Ahead-of-time compilation of evaluated SX trees to JavaScript.")
(ul :class "list-disc pl-6 space-y-1 text-stone-700"
(li (code "js-compile-element") " — emit " (code "createElement") " + attribute setting")
@@ -415,8 +415,8 @@ _0.appendChild(_2);" "javascript")))
(li (code "js-compile-fragment") " — emit " (code "DocumentFragment") " construction")
(li "Runtime slicing: analyze tree → include only necessary runtime modules")))
(~doc-subsection :title "Phase 5: Verification"
(~doc-code :code (highlight "# Mode 1: spec bootstrapper parity
(~docs/subsection :title "Phase 5: Verification"
(~docs/code :code (highlight "# Mode 1: spec bootstrapper parity
python bootstrap_js.py > sx-ref-g0.js
python run_js_sx.py > sx-ref-g1.js
diff sx-ref-g0.js sx-ref-g1.js # must be empty
@@ -430,7 +430,7 @@ python test_js_compile.py # renders both, diffs DOM" "bash")))
;; Comparison with py.sx
;; -----------------------------------------------------------------------
(~doc-section :title "Comparison with py.sx" :id "comparison"
(~docs/section :title "Comparison with py.sx" :id "comparison"
(div :class "overflow-x-auto rounded border border-stone-200 my-4"
(table :class "w-full text-sm"
(thead :class "bg-stone-50"
@@ -472,8 +472,8 @@ python test_js_compile.py # renders both, diffs DOM" "bash")))
;; Implications
;; -----------------------------------------------------------------------
(~doc-section :title "Implications" :id "implications"
(~doc-subsection :title "Zero-Runtime Static Sites"
(~docs/section :title "Implications" :id "implications"
(~docs/subsection :title "Zero-Runtime Static Sites"
(p "A static page written in SX compiles to a JavaScript program with "
"no SX runtime dependency. The output is just DOM API calls — "
(code "createElement") ", " (code "appendChild") ", " (code "textContent")
@@ -484,7 +484,7 @@ python test_js_compile.py # renders both, diffs DOM" "bash")))
"The server returns a CID. The browser fetches and executes pre-compiled JavaScript. "
"No parser, no evaluator, no network round-trip for component definitions."))
(~doc-subsection :title "Progressive Enhancement Layers"
(~docs/subsection :title "Progressive Enhancement Layers"
(p "The component compiler naturally supports progressive enhancement:")
(ol :class "list-decimal pl-6 space-y-1 text-stone-700"
(li (strong "HTML") " — server renders to HTML string. No JS needed. Works everywhere.")
@@ -498,7 +498,7 @@ python test_js_compile.py # renders both, diffs DOM" "bash")))
"A single-page app needs layer 3. A real-time dashboard needs layer 4. "
(code "js.sx") " makes layer 2 possible — it didn't exist before."))
(~doc-subsection :title "The Bootstrap Completion"
(~docs/subsection :title "The Bootstrap Completion"
(p "With " (code "py.sx") " and " (code "js.sx") " both written in SX:")
(ul :class "list-disc pl-6 space-y-2 text-stone-700"
(li "The " (em "spec") " defines SX semantics (" (code "eval.sx") ", " (code "render.sx") ", ...)")

View File

@@ -2,12 +2,12 @@
;; Live Streaming — SSE & WebSocket
;; ---------------------------------------------------------------------------
(defcomp ~plan-live-streaming-content ()
(~doc-page :title "Live Streaming"
(defcomp ~plans/live-streaming/plan-live-streaming-content ()
(~docs/page :title "Live Streaming"
(~doc-section :title "Context" :id "context"
(~docs/section :title "Context" :id "context"
(p "SX streaming currently uses chunked transfer encoding: the server sends an HTML shell with "
(code "~suspense") " placeholders, then resolves each one via inline "
(code "~shared:pages/suspense") " placeholders, then resolves each one via inline "
(code "<script>__sxResolve(id, sx)</script>") " chunks as IO completes. "
"Once the response finishes, the connection closes. Each slot resolves exactly once.")
(p "This is powerful for initial page load but doesn't support live updates "
@@ -16,9 +16,9 @@
(p "The key insight: the client already has " (code "Sx.resolveSuspense(id, sxSource)") " which replaces "
"DOM content by suspense ID. A persistent connection just needs to keep calling it."))
(~doc-section :title "Design" :id "design"
(~docs/section :title "Design" :id "design"
(~doc-subsection :title "Transport Hierarchy"
(~docs/subsection :title "Transport Hierarchy"
(p "Three tiers, progressively more capable:")
(ol :class "list-decimal list-inside space-y-2 text-stone-700 text-sm"
(li (strong "Chunked streaming") " (done) — single HTTP response, each suspense resolves once. "
@@ -28,21 +28,21 @@
(li (strong "WebSocket") " — bidirectional, client can send events back. "
"Best for: chat, collaborative editing, interactive applications.")))
(~doc-subsection :title "SSE Protocol"
(~docs/subsection :title "SSE Protocol"
(p "A " (code "~live") " component declares a persistent connection to an SSE endpoint:")
(~doc-code :code (highlight "(~live :src \"/api/stream/dashboard\"\n (~suspense :id \"cpu\" :fallback (span \"Loading...\"))\n (~suspense :id \"memory\" :fallback (span \"Loading...\"))\n (~suspense :id \"requests\" :fallback (span \"Loading...\")))" "lisp"))
(~docs/code :code (highlight "(~live :src \"/api/stream/dashboard\"\n (~shared:pages/suspense :id \"cpu\" :fallback (span \"Loading...\"))\n (~shared:pages/suspense :id \"memory\" :fallback (span \"Loading...\"))\n (~shared:pages/suspense :id \"requests\" :fallback (span \"Loading...\")))" "lisp"))
(p "The server SSE endpoint yields SX resolve events:")
(~doc-code :code (highlight "async def dashboard_stream():\n while True:\n stats = await get_system_stats()\n yield sx_sse_event(\"cpu\", f'(~stat-badge :value \"{stats.cpu}%\")')\n yield sx_sse_event(\"memory\", f'(~stat-badge :value \"{stats.mem}%\")')\n await asyncio.sleep(1)" "python"))
(~docs/code :code (highlight "async def dashboard_stream():\n while True:\n stats = await get_system_stats()\n yield sx_sse_event(\"cpu\", f'(~stat-badge :value \"{stats.cpu}%\")')\n yield sx_sse_event(\"memory\", f'(~stat-badge :value \"{stats.mem}%\")')\n await asyncio.sleep(1)" "python"))
(p "SSE wire format — each event is a suspense resolve:")
(~doc-code :code (highlight "event: sx-resolve\ndata: {\"id\": \"cpu\", \"sx\": \"(~stat-badge :value \\\"42%\\\")\"}\n\nevent: sx-resolve\ndata: {\"id\": \"memory\", \"sx\": \"(~stat-badge :value \\\"68%\\\")\"}" "text")))
(~docs/code :code (highlight "event: sx-resolve\ndata: {\"id\": \"cpu\", \"sx\": \"(~stat-badge :value \\\"42%\\\")\"}\n\nevent: sx-resolve\ndata: {\"id\": \"memory\", \"sx\": \"(~stat-badge :value \\\"68%\\\")\"}" "text")))
(~doc-subsection :title "WebSocket Protocol"
(~docs/subsection :title "WebSocket Protocol"
(p "A " (code "~ws") " component establishes a bidirectional channel:")
(~doc-code :code (highlight "(~ws :src \"/ws/chat\"\n :on-message handle-chat-message\n (~suspense :id \"messages\" :fallback (div \"Connecting...\"))\n (~suspense :id \"typing\" :fallback (span)))" "lisp"))
(~docs/code :code (highlight "(~ws :src \"/ws/chat\"\n :on-message handle-chat-message\n (~shared:pages/suspense :id \"messages\" :fallback (div \"Connecting...\"))\n (~shared:pages/suspense :id \"typing\" :fallback (span)))" "lisp"))
(p "Client can send SX expressions back:")
(~doc-code :code (highlight ";; Client sends:\n(sx-send ws-conn '(chat-message :text \"hello\" :user \"alice\"))\n\n;; Server receives, broadcasts to all connected clients:\n;; event: sx-resolve for \"messages\" suspense" "lisp")))
(~docs/code :code (highlight ";; Client sends:\n(sx-send ws-conn '(chat-message :text \"hello\" :user \"alice\"))\n\n;; Server receives, broadcasts to all connected clients:\n;; event: sx-resolve for \"messages\" suspense" "lisp")))
(~doc-subsection :title "Shared Resolution Mechanism"
(~docs/subsection :title "Shared Resolution Mechanism"
(p "All three transports use the same client-side resolution:")
(ul :class "list-disc list-inside space-y-1 text-stone-600 text-sm"
(li (code "Sx.resolveSuspense(id, sxSource)") " — already exists, parses SX and renders to DOM")
@@ -51,9 +51,9 @@
(li "The component env (defs needed for rendering) can be sent once on connection open")
(li "Subsequent events only need the SX expression — lightweight wire format"))))
(~doc-section :title "Implementation" :id "implementation"
(~docs/section :title "Implementation" :id "implementation"
(~doc-subsection :title "Phase 1: SSE Infrastructure"
(~docs/subsection :title "Phase 1: SSE Infrastructure"
(ol :class "list-decimal list-inside space-y-2 text-stone-700 text-sm"
(li "Add " (code "~live") " component to " (code "shared/sx/templates/") " — renders child suspense placeholders, "
"emits " (code "data-sx-live") " attribute with SSE endpoint URL")
@@ -62,28 +62,28 @@
(li "Add " (code "sx_sse_event(id, sx)") " helper for Python SSE endpoints — formats SSE wire protocol")
(li "Add " (code "sse_stream()") " Quart helper — returns async generator Response with correct headers")))
(~doc-subsection :title "Phase 2: Defpage Integration"
(~docs/subsection :title "Phase 2: Defpage Integration"
(ol :class "list-decimal list-inside space-y-2 text-stone-700 text-sm"
(li "New " (code ":live") " defpage slot — declares SSE endpoint + suspense bindings")
(li "Auto-mount SSE endpoint alongside the page route")
(li "Component defs sent as first SSE event on connection open")
(li "Automatic reconnection with exponential backoff")))
(~doc-subsection :title "Phase 3: WebSocket"
(~docs/subsection :title "Phase 3: WebSocket"
(ol :class "list-decimal list-inside space-y-2 text-stone-700 text-sm"
(li "Add " (code "~ws") " component — bidirectional channel with send/receive")
(li "Add " (code "sx-ws.js") " client module — WebSocket management, message routing")
(li "Server-side: Quart WebSocket handlers that receive and broadcast SX events")
(li "Client-side: " (code "sx-send") " primitive for sending SX expressions to server")))
(~doc-subsection :title "Phase 4: Spec & Boundary"
(~docs/subsection :title "Phase 4: Spec & Boundary"
(ol :class "list-decimal list-inside space-y-2 text-stone-700 text-sm"
(li "Spec " (code "~live") " and " (code "~ws") " in " (code "render.sx") " (how they render in each mode)")
(li "Add SSE/WS IO primitives to " (code "boundary.sx"))
(li "Bootstrap SSE/WS connection management into " (code "sx-ref.js"))
(li "Spec-level tests for resolve, reconnection, and message routing"))))
(~doc-section :title "Files" :id "files"
(~docs/section :title "Files" :id "files"
(table :class "w-full text-left border-collapse"
(thead
(tr :class "border-b border-stone-200"

View File

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

View File

@@ -2,15 +2,15 @@
;; Predictive Component Prefetching
;; ---------------------------------------------------------------------------
(defcomp ~plan-predictive-prefetch-content ()
(~doc-page :title "Predictive Component Prefetching"
(defcomp ~plans/predictive-prefetch/plan-predictive-prefetch-content ()
(~docs/page :title "Predictive Component Prefetching"
(~doc-section :title "Context" :id "context"
(~docs/section :title "Context" :id "context"
(p "Phase 3 of the isomorphic roadmap added client-side routing with component dependency checking. When a user clicks a link, " (code "try-client-route") " checks " (code "has-all-deps?") " — if the target page needs components not yet loaded, the client falls back to a server fetch. This works correctly but misses an opportunity: " (strong "we can prefetch those missing components before the click happens."))
(p "The page registry already carries " (code ":deps") " metadata for every page. The client already knows which components are loaded via " (code "loaded-component-names") ". The gap is a mechanism to " (em "proactively") " resolve the difference — fetching missing component definitions so that by the time the user clicks, client-side routing succeeds.")
(p "But this goes beyond just hover-to-prefetch. The full spectrum includes: bundling linked routes' components with the initial page load, batch-prefetching after idle, predicting mouse trajectory toward links, and even splitting the component/data fetch so that " (code ":data") " pages can prefetch their components and only fetch data on click. Each strategy trades bandwidth for latency, and pages should be able to declare which tradeoff they want."))
(~doc-section :title "Current State" :id "current-state"
(~docs/section :title "Current State" :id "current-state"
(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"
@@ -47,7 +47,7 @@
;; Prefetch strategies
;; -----------------------------------------------------------------------
(~doc-section :title "Prefetch Strategies" :id "strategies"
(~docs/section :title "Prefetch Strategies" :id "strategies"
(p "Prefetching is a spectrum from conservative to aggressive. The system should support all of these, configured declaratively per link or per page via " (code "defpage") " metadata and " (code "sx-prefetch") " attributes.")
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
@@ -94,22 +94,22 @@
(td :class "px-3 py-2 text-stone-700" "Components " (em "and") " page data for " (code ":data") " pages")
(td :class "px-3 py-2 text-stone-600" "Zero for components; data fetch may still be in flight")))))
(~doc-subsection :title "Eager Bundle"
(~docs/subsection :title "Eager Bundle"
(p "The server already computes per-page component bundles. For key navigation paths — the main nav bar, section nav — the server can include " (em "linked routes' components") " in the initial bundle, not just the current page's.")
(~doc-code :code (highlight ";; defpage metadata declares eager prefetch targets\n(defpage docs-page\n :path \"/language/docs/<slug>\"\n :auth :public\n :prefetch :eager ;; bundle deps for all linked pure routes\n :content (case slug ...))" "lisp"))
(~docs/code :code (highlight ";; defpage metadata declares eager prefetch targets\n(defpage docs-page\n :path \"/language/docs/<slug>\"\n :auth :public\n :prefetch :eager ;; bundle deps for all linked pure routes\n :content (case slug ...))" "lisp"))
(p "Implementation: " (code "components_for_page()") " already scans the page SX for component refs. Extend it to also scan for " (code "href") " attributes, match them against the page registry, and include those pages' deps in the bundle. The cost is a larger initial payload; the benefit is zero-latency navigation within a section."))
(~doc-subsection :title "Idle Timer"
(~docs/subsection :title "Idle Timer"
(p "After page load and initial render, use " (code "requestIdleCallback") " (or a fallback " (code "setTimeout") ") to scan visible nav links and batch-prefetch their missing components in a single request.")
(~doc-code :code (highlight "(define prefetch-visible-links-on-idle\n (fn ()\n (request-idle-callback\n (fn ()\n (let ((links (dom-query-all \"a[href][sx-get]\"))\n (all-missing (list)))\n (for-each\n (fn (link)\n (let ((missing (compute-missing-deps\n (url-pathname (dom-get-attr link \"href\")))))\n (when missing\n (for-each (fn (d) (append! all-missing d))\n missing))))\n links)\n (when (not (empty? all-missing))\n (prefetch-components (dedupe all-missing))))))))" "lisp"))
(~docs/code :code (highlight "(define prefetch-visible-links-on-idle\n (fn ()\n (request-idle-callback\n (fn ()\n (let ((links (dom-query-all \"a[href][sx-get]\"))\n (all-missing (list)))\n (for-each\n (fn (link)\n (let ((missing (compute-missing-deps\n (url-pathname (dom-get-attr link \"href\")))))\n (when missing\n (for-each (fn (d) (append! all-missing d))\n missing))))\n links)\n (when (not (empty? all-missing))\n (prefetch-components (dedupe all-missing))))))))" "lisp"))
(p "Called once from " (code "boot-init") " after initial processing. Batches all missing deps into one network request. Low priority — browser handles it when idle."))
(~doc-subsection :title "Mouse Approach (Trajectory Prediction)"
(~docs/subsection :title "Mouse Approach (Trajectory Prediction)"
(p "Don't wait for the cursor to reach the link — predict where it's heading. Track the last few " (code "mousemove") " events, extrapolate the trajectory, and if it points toward a link, start prefetching before the hover event fires.")
(~doc-code :code (highlight "(define bind-approach-prefetch\n (fn (container)\n ;; Track mouse trajectory within a nav container.\n ;; On each mousemove, extrapolate position ~200ms ahead.\n ;; If projected point intersects a link's bounding box,\n ;; prefetch that link's route deps.\n (let ((last-x 0) (last-y 0) (last-t 0)\n (prefetched (dict)))\n (dom-add-listener container \"mousemove\"\n (fn (e)\n (let ((now (timestamp))\n (dt (- now last-t)))\n (when (> dt 16) ;; ~60fps throttle\n (let ((vx (/ (- (event-x e) last-x) dt))\n (vy (/ (- (event-y e) last-y) dt))\n (px (+ (event-x e) (* vx 200)))\n (py (+ (event-y e) (* vy 200)))\n (target (dom-element-at-point px py)))\n (when (and target (dom-has-attr? target \"href\")\n (not (get prefetched\n (dom-get-attr target \"href\"))))\n (let ((href (dom-get-attr target \"href\")))\n (set! prefetched\n (merge prefetched {href true}))\n (prefetch-route-deps\n (url-pathname href)))))\n (set! last-x (event-x e))\n (set! last-y (event-y e))\n (set! last-t now))))))))" "lisp"))
(~docs/code :code (highlight "(define bind-approach-prefetch\n (fn (container)\n ;; Track mouse trajectory within a nav container.\n ;; On each mousemove, extrapolate position ~200ms ahead.\n ;; If projected point intersects a link's bounding box,\n ;; prefetch that link's route deps.\n (let ((last-x 0) (last-y 0) (last-t 0)\n (prefetched (dict)))\n (dom-add-listener container \"mousemove\"\n (fn (e)\n (let ((now (timestamp))\n (dt (- now last-t)))\n (when (> dt 16) ;; ~60fps throttle\n (let ((vx (/ (- (event-x e) last-x) dt))\n (vy (/ (- (event-y e) last-y) dt))\n (px (+ (event-x e) (* vx 200)))\n (py (+ (event-y e) (* vy 200)))\n (target (dom-element-at-point px py)))\n (when (and target (dom-has-attr? target \"href\")\n (not (get prefetched\n (dom-get-attr target \"href\"))))\n (let ((href (dom-get-attr target \"href\")))\n (set! prefetched\n (merge prefetched {href true}))\n (prefetch-route-deps\n (url-pathname href)))))\n (set! last-x (event-x e))\n (set! last-y (event-y e))\n (set! last-t now))))))))" "lisp"))
(p "This is the most speculative strategy — best suited for dense navigation areas (section sidebars, nav bars) where the cursor trajectory is a strong predictor. The " (code "prefetched") " dict prevents duplicate fetches within the same container interaction."))
(~doc-subsection :title "Components + Data (Hybrid Prefetch)"
(~docs/subsection :title "Components + Data (Hybrid Prefetch)"
(p "The most interesting strategy. For pages with " (code ":data") " dependencies, current behavior is full server fallback. But the page's " (em "components") " are still pure and prefetchable. If we prefetch components ahead of time, the click only needs to fetch " (em "data") " — a much smaller, faster response.")
(p "This creates a new rendering path:")
(ol :class "list-decimal pl-5 text-stone-700 space-y-1"
@@ -118,84 +118,84 @@
(li "Server returns " (em "only data") " (JSON or SX bindings), not the full rendered page")
(li "Client evaluates the content expression with prefetched components + fetched data")
(li "Result: faster than full server render, no redundant component transfer"))
(~doc-code :code (highlight ";; Declarative: prefetch components, fetch data on click\n(defpage reference-page\n :path \"/reference/<slug>\"\n :auth :public\n :prefetch :components ;; prefetch components, data stays server-fetched\n :data (reference-data slug)\n :content (~reference-attrs-content :attrs attrs))\n\n;; On click, client-side flow:\n;; 1. Components already prefetched (from hover/idle)\n;; 2. GET /reference/attributes → server returns data bindings\n;; 3. Client evals (reference-data slug) result + content expr\n;; 4. Renders locally with cached components" "lisp"))
(~docs/code :code (highlight ";; Declarative: prefetch components, fetch data on click\n(defpage reference-page\n :path \"/reference/<slug>\"\n :auth :public\n :prefetch :components ;; prefetch components, data stays server-fetched\n :data (reference-data slug)\n :content (~reference/attrs-content :attrs attrs))\n\n;; On click, client-side flow:\n;; 1. Components already prefetched (from hover/idle)\n;; 2. GET /reference/attributes → server returns data bindings\n;; 3. Client evals (reference-data slug) result + content expr\n;; 4. Renders locally with cached components" "lisp"))
(p "This is a stepping stone toward full Phase 4 (client IO bridge) of the isomorphic roadmap — it achieves partial client rendering for data pages without needing a general-purpose client async evaluator. The server is a data service, the client is the renderer."))
(~doc-subsection :title "Declarative Configuration"
(~docs/subsection :title "Declarative Configuration"
(p "All strategies configured via " (code "defpage") " metadata and " (code "sx-prefetch") " attributes on links/containers:")
(~doc-code :code (highlight ";; Page-level: what to prefetch for routes linking TO this page\n(defpage docs-page\n :path \"/language/docs/<slug>\"\n :prefetch :eager) ;; bundle with linking page\n\n(defpage reference-page\n :path \"/reference/<slug>\"\n :prefetch :components) ;; prefetch components, data on click\n\n;; Link-level: override per-link\n(a :href \"/language/docs/components\"\n :sx-prefetch \"idle\") ;; prefetch after page idle\n\n;; Container-level: approach prediction for nav areas\n(nav :sx-prefetch \"approach\"\n (a :href \"/language/docs/\") (a :href \"/reference/\") ...)" "lisp"))
(~docs/code :code (highlight ";; Page-level: what to prefetch for routes linking TO this page\n(defpage docs-page\n :path \"/language/docs/<slug>\"\n :prefetch :eager) ;; bundle with linking page\n\n(defpage reference-page\n :path \"/reference/<slug>\"\n :prefetch :components) ;; prefetch components, data on click\n\n;; Link-level: override per-link\n(a :href \"/language/docs/components\"\n :sx-prefetch \"idle\") ;; prefetch after page idle\n\n;; Container-level: approach prediction for nav areas\n(nav :sx-prefetch \"approach\"\n (a :href \"/language/docs/\") (a :href \"/reference/\") ...)" "lisp"))
(p "Priority cascade: explicit " (code "sx-prefetch") " on link > " (code ":prefetch") " on target defpage > default (hover). The system never prefetches the same components twice — " (code "_prefetch-pending") " and " (code "loaded-component-names") " handle dedup.")))
;; -----------------------------------------------------------------------
;; Design
;; -----------------------------------------------------------------------
(~doc-section :title "Implementation Design" :id "design"
(~docs/section :title "Implementation Design" :id "design"
(p "Per the SX host architecture principle: all SX-specific logic goes in " (code ".sx") " spec files and gets bootstrapped. The prefetch logic — scanning links, computing missing deps, managing the component cache — must be specced in " (code ".sx") ", not written directly in JS or Python.")
(~doc-subsection :title "Phase 1: Component Fetch Endpoint (Python)"
(~docs/subsection :title "Phase 1: Component Fetch Endpoint (Python)"
(p "A new " (strong "public") " endpoint (not " (code "/internal/") " — the client's browser calls it) that returns component definitions by name.")
(~doc-code :code (highlight "GET /<service-prefix>/sx/components?names=~card,~essay-foo\n\nResponse (text/sx):\n(defcomp ~card (&key title &rest children)\n (div :class \"border rounded p-4\" (h2 title) children))\n(defcomp ~essay-foo (&key id)\n (div (~card :title id)))" "http"))
(~docs/code :code (highlight "GET /<service-prefix>/sx/components?names=~plans/predictive-prefetch/card,~essay-foo\n\nResponse (text/sx):\n(defcomp ~plans/predictive-prefetch/card (&key title &rest children)\n (div :class \"border rounded p-4\" (h2 title) children))\n(defcomp ~plans/predictive-prefetch/essay-foo (&key id)\n (div (~plans/predictive-prefetch/card :title id)))" "http"))
(p "The server resolves transitive deps via " (code "deps.py") ", subtracts anything listed in the " (code "SX-Components") " request header (already loaded), serializes and returns. This is essentially " (code "components_for_request()") " driven by an explicit " (code "?names=") " param.")
(p "Cache-friendly: the response is a pure function of component hash + requested names. " (code "Cache-Control: public, max-age=3600") " with the component hash as ETag."))
(~doc-subsection :title "Phase 2: Client Prefetch Logic (SX spec)"
(~docs/subsection :title "Phase 2: Client Prefetch Logic (SX spec)"
(p "New functions in " (code "orchestration.sx") " (or a new " (code "prefetch.sx") " if scope warrants):")
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. compute-missing-deps")
(p "Given a pathname, find the page, return dep names not in " (code "loaded-component-names") ". Returns nil if page not found or has data (can't client-route anyway).")
(~doc-code :code (highlight "(define compute-missing-deps\n (fn (pathname)\n (let ((match (find-matching-route pathname _page-routes)))\n (when (and match (not (get match \"has-data\")))\n (let ((deps (or (get match \"deps\") (list)))\n (loaded (loaded-component-names)))\n (filter (fn (d) (not (contains? loaded d))) deps))))))" "lisp")))
(~docs/code :code (highlight "(define compute-missing-deps\n (fn (pathname)\n (let ((match (find-matching-route pathname _page-routes)))\n (when (and match (not (get match \"has-data\")))\n (let ((deps (or (get match \"deps\") (list)))\n (loaded (loaded-component-names)))\n (filter (fn (d) (not (contains? loaded d))) deps))))))" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "2. prefetch-components")
(p "Fetch component definitions from the server for a list of names. Deduplicates in-flight requests. On success, parses and registers the returned definitions into the component env.")
(~doc-code :code (highlight "(define _prefetch-pending (dict))\n\n(define prefetch-components\n (fn (names)\n (let ((key (join \",\" (sort names))))\n (when (not (get _prefetch-pending key))\n (set! _prefetch-pending\n (merge _prefetch-pending {key true}))\n (fetch-components-from-server names\n (fn (sx-text)\n (sx-process-component-text sx-text)\n (dict-remove! _prefetch-pending key)))))))" "lisp")))
(~docs/code :code (highlight "(define _prefetch-pending (dict))\n\n(define prefetch-components\n (fn (names)\n (let ((key (join \",\" (sort names))))\n (when (not (get _prefetch-pending key))\n (set! _prefetch-pending\n (merge _prefetch-pending {key true}))\n (fetch-components-from-server names\n (fn (sx-text)\n (sx-process-component-text sx-text)\n (dict-remove! _prefetch-pending key)))))))" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "3. prefetch-route-deps")
(p "High-level composition: compute missing deps for a route, fetch if any.")
(~doc-code :code (highlight "(define prefetch-route-deps\n (fn (pathname)\n (let ((missing (compute-missing-deps pathname)))\n (when (and missing (not (empty? missing)))\n (log-info (str \"sx:prefetch \"\n (len missing) \" components for \" pathname))\n (prefetch-components missing)))))" "lisp")))
(~docs/code :code (highlight "(define prefetch-route-deps\n (fn (pathname)\n (let ((missing (compute-missing-deps pathname)))\n (when (and missing (not (empty? missing)))\n (log-info (str \"sx:prefetch \"\n (len missing) \" components for \" pathname))\n (prefetch-components missing)))))" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "4. Trigger: link hover")
(p "On mouseover of a boosted link, prefetch its route's missing components. Debounced 150ms to avoid fetching on quick mouse-throughs.")
(~doc-code :code (highlight "(define bind-prefetch-on-hover\n (fn (link)\n (let ((timer nil))\n (dom-add-listener link \"mouseover\"\n (fn (e)\n (clear-timeout timer)\n (set! timer (set-timeout\n (fn () (prefetch-route-deps\n (url-pathname (dom-get-attr link \"href\"))))\n 150))))\n (dom-add-listener link \"mouseout\"\n (fn (e) (clear-timeout timer))))))" "lisp")))
(~docs/code :code (highlight "(define bind-prefetch-on-hover\n (fn (link)\n (let ((timer nil))\n (dom-add-listener link \"mouseover\"\n (fn (e)\n (clear-timeout timer)\n (set! timer (set-timeout\n (fn () (prefetch-route-deps\n (url-pathname (dom-get-attr link \"href\"))))\n 150))))\n (dom-add-listener link \"mouseout\"\n (fn (e) (clear-timeout timer))))))" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "5. Trigger: viewport intersection (opt-in)")
(p "More aggressive strategy: when a link scrolls into view, prefetch its route's deps. Opt-in via " (code "sx-prefetch=\"visible\"") " attribute.")
(~doc-code :code (highlight "(define bind-prefetch-on-visible\n (fn (link)\n (observe-intersection link\n (fn () (prefetch-route-deps\n (url-pathname (dom-get-attr link \"href\"))))\n true 0)))" "lisp")))
(~docs/code :code (highlight "(define bind-prefetch-on-visible\n (fn (link)\n (observe-intersection link\n (fn () (prefetch-route-deps\n (url-pathname (dom-get-attr link \"href\"))))\n true 0)))" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "6. Integration into process-elements")
(p "During the existing hydration pass, for each boosted link:")
(~doc-code :code (highlight ";; In process-elements, after binding boost behavior:\n(when (and (should-boost-link? link)\n (dom-get-attr link \"href\"))\n (bind-prefetch-on-hover link))\n\n;; Explicit viewport prefetch:\n(when (dom-has-attr? link \"sx-prefetch\")\n (bind-prefetch-on-visible link))" "lisp")))))
(~docs/code :code (highlight ";; In process-elements, after binding boost behavior:\n(when (and (should-boost-link? link)\n (dom-get-attr link \"href\"))\n (bind-prefetch-on-hover link))\n\n;; Explicit viewport prefetch:\n(when (dom-has-attr? link \"sx-prefetch\")\n (bind-prefetch-on-visible link))" "lisp")))))
(~doc-subsection :title "Phase 3: Boundary Declaration"
(~docs/subsection :title "Phase 3: Boundary Declaration"
(p "Two new IO primitives in " (code "boundary.sx") " (browser-only):")
(~doc-code :code (highlight ";; IO primitives (browser-only)\n(io fetch-components-from-server (names callback) -> void)\n(io sx-process-component-text (sx-text) -> void)" "lisp"))
(~docs/code :code (highlight ";; IO primitives (browser-only)\n(io fetch-components-from-server (names callback) -> void)\n(io sx-process-component-text (sx-text) -> void)" "lisp"))
(p "These are thin wrappers around " (code "fetch()") " + the existing component script processing logic already in the boundary adapter."))
(~doc-subsection :title "Phase 4: Bootstrap"
(~docs/subsection :title "Phase 4: Bootstrap"
(p (code "bootstrap_js.py") " picks up the new functions from the spec and emits them into " (code "sx-browser.js") ". The two new boundary IO functions get implemented in the JS boundary adapter — the hand-written glue code that the bootstrapper doesn't generate.")
(~doc-code :code (highlight "// fetch-components-from-server: calls the endpoint\nfunction fetchComponentsFromServer(names, callback) {\n const url = `${routePrefix}/sx/components?names=${names.join(\",\")}`;\n const headers = {\n \"SX-Components\": loadedComponentNames().join(\",\")\n };\n fetch(url, { headers })\n .then(r => r.ok ? r.text() : \"\")\n .then(text => callback(text))\n .catch(() => {}); // silent fail — prefetch is best-effort\n}\n\n// sx-process-component-text: parse defcomp/defmacro into env\nfunction sxProcessComponentText(sxText) {\n if (!sxText) return;\n const frag = document.createElement(\"div\");\n frag.innerHTML =\n `<script type=\"text/sx\" data-components>${sxText}<\\/script>`;\n Sx.processScripts(frag);\n}" "javascript"))))
(~docs/code :code (highlight "// fetch-components-from-server: calls the endpoint\nfunction fetchComponentsFromServer(names, callback) {\n const url = `${routePrefix}/sx/components?names=${names.join(\",\")}`;\n const headers = {\n \"SX-Components\": loadedComponentNames().join(\",\")\n };\n fetch(url, { headers })\n .then(r => r.ok ? r.text() : \"\")\n .then(text => callback(text))\n .catch(() => {}); // silent fail — prefetch is best-effort\n}\n\n// sx-process-component-text: parse defcomp/defmacro into env\nfunction sxProcessComponentText(sxText) {\n if (!sxText) return;\n const frag = document.createElement(\"div\");\n frag.innerHTML =\n `<script type=\"text/sx\" data-components>${sxText}<\\/script>`;\n Sx.processScripts(frag);\n}" "javascript"))))
;; -----------------------------------------------------------------------
;; Request flow
;; -----------------------------------------------------------------------
(~doc-section :title "Request Flow" :id "request-flow"
(~docs/section :title "Request Flow" :id "request-flow"
(p "End-to-end example: user hovers a link, components prefetch, click goes client-side.")
(~doc-code :code (highlight "User hovers link \"/language/docs/sx-manifesto\"\n |\n +-- bind-prefetch-on-hover fires (150ms debounce)\n |\n +-- compute-missing-deps(\"/language/docs/sx-manifesto\")\n | +-- find-matching-route -> page with deps:\n | | [\"~essay-sx-manifesto\", \"~doc-code\"]\n | +-- loaded-component-names -> [\"~nav\", \"~footer\", \"~doc-code\"]\n | +-- missing: [\"~essay-sx-manifesto\"]\n |\n +-- prefetch-components([\"~essay-sx-manifesto\"])\n | +-- GET /sx/components?names=~essay-sx-manifesto\n | | Headers: SX-Components: ~nav,~footer,~doc-code\n | +-- Server resolves transitive deps\n | | (also needs ~rich-text, subtracts already-loaded)\n | +-- Response:\n | (defcomp ~essay-sx-manifesto ...) \n | (defcomp ~rich-text ...)\n |\n +-- sx-process-component-text registers defcomps in env\n |\n +-- User clicks link\n +-- try-client-route(\"/language/docs/sx-manifesto\")\n +-- has-all-deps? -> true (prefetched!)\n +-- eval content -> DOM\n +-- Client-side render, no server roundtrip" "text")))
(~docs/code :code (highlight "User hovers link \"/language/docs/sx-manifesto\"\n |\n +-- bind-prefetch-on-hover fires (150ms debounce)\n |\n +-- compute-missing-deps(\"/language/docs/sx-manifesto\")\n | +-- find-matching-route -> page with deps:\n | | [\"~essay-sx-manifesto\", \"~doc-code\"]\n | +-- loaded-component-names -> [\"~nav\", \"~footer\", \"~doc-code\"]\n | +-- missing: [\"~essay-sx-manifesto\"]\n |\n +-- prefetch-components([\"~essay-sx-manifesto\"])\n | +-- GET /sx/components?names=~essay-sx-manifesto\n | | Headers: SX-Components: ~plans/environment-images/nav,~footer,~doc-code\n | +-- Server resolves transitive deps\n | | (also needs ~plans/predictive-prefetch/rich-text, subtracts already-loaded)\n | +-- Response:\n | (defcomp ~plans/predictive-prefetch/essay-sx-manifesto ...) \n | (defcomp ~plans/predictive-prefetch/rich-text ...)\n |\n +-- sx-process-component-text registers defcomps in env\n |\n +-- User clicks link\n +-- try-client-route(\"/language/docs/sx-manifesto\")\n +-- has-all-deps? -> true (prefetched!)\n +-- eval content -> DOM\n +-- Client-side render, no server roundtrip" "text")))
;; -----------------------------------------------------------------------
;; File changes
;; -----------------------------------------------------------------------
(~doc-section :title "File Changes" :id "file-changes"
(~docs/section :title "File Changes" :id "file-changes"
(div :class "overflow-x-auto rounded border border-stone-200"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-100"
@@ -228,14 +228,14 @@
;; Non-goals & rollout
;; -----------------------------------------------------------------------
(~doc-section :title "Non-Goals (This Phase)" :id "non-goals"
(~docs/section :title "Non-Goals (This Phase)" :id "non-goals"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "Analytics-driven prediction") " — no ML models or click-frequency heuristics. Trajectory prediction uses geometry, not statistics.")
(li (strong "Cross-service prefetch") " — components are per-service. A link to a different service domain is always a server navigation.")
(li (strong "Service worker caching") " — could layer on later, but basic fetch + in-memory registration is sufficient.")
(li (strong "Full client-side data evaluation") " — the components+data strategy fetches data from the server, it doesn't replicate server IO on the client. That's Phase 4 of the isomorphic roadmap.")))
(~doc-section :title "Rollout" :id "rollout"
(~docs/section :title "Rollout" :id "rollout"
(p "Incremental, each step independently valuable:")
(ol :class "list-decimal pl-5 text-stone-700 space-y-2"
(li (strong "Component endpoint") " — purely additive. Refactor " (code "components_for_request()") " to accept explicit " (code "?names=") " param.")
@@ -246,7 +246,7 @@
(li (strong "Eager bundles") " — extend " (code "components_for_page()") " to include linked routes' deps. Heavier initial payload, zero-latency nav.")
(li (strong "Components + data split") " — new server response mode returning data bindings only. Client renders with prefetched components. Bridges toward Phase 4.")))
(~doc-section :title "Relationship to Isomorphic Roadmap" :id "relationship"
(~docs/section :title "Relationship to Isomorphic Roadmap" :id "relationship"
(p "This plan sits between Phase 3 (client-side routing) and Phase 4 (client async & IO bridge) of the "
(a :href "/sx/(etc.(plan.isomorphic-architecture))" :class "text-violet-700 underline" "isomorphic architecture roadmap")
". It extends Phase 3 by making more navigations go client-side without needing any IO bridge — purely by ensuring component definitions are available before they're needed.")

View File

@@ -2,19 +2,19 @@
;; Reader Macro Demo: #z3 — SX Spec to SMT-LIB (live translation via z3.sx)
;; ---------------------------------------------------------------------------
(defcomp ~z3-example (&key (sx-source :as string) (smt-output :as string))
(defcomp ~plans/reader-macro-demo/z3-example (&key (sx-source :as string) (smt-output :as string))
(div :class "grid grid-cols-1 md:grid-cols-2 gap-4"
(div
(p :class "text-xs text-stone-500 uppercase tracking-wider mb-1" "SX Source")
(~doc-code :code (highlight sx-source "lisp")))
(~docs/code :code (highlight sx-source "lisp")))
(div
(p :class "text-xs text-stone-500 uppercase tracking-wider mb-1" "SMT-LIB Output (live from z3.sx)")
(~doc-code :code (highlight smt-output "lisp")))))
(~docs/code :code (highlight smt-output "lisp")))))
(defcomp ~plan-reader-macro-demo-content ()
(~doc-page :title "Reader Macro Demo: #z3"
(defcomp ~plans/reader-macro-demo/plan-reader-macro-demo-content ()
(~docs/page :title "Reader Macro Demo: #z3"
(~doc-section :title "The idea" :id "idea"
(~docs/section :title "The idea" :id "idea"
(p :class "text-stone-600"
"SX spec files (" (code "primitives.sx") ", " (code "eval.sx") ") are machine-readable declarations. Reader macros transform these at parse time. " (code "#z3") " reads a " (code "define-primitive") " declaration and emits " (a :href "https://smtlib.cs.uiowa.edu/" :class "text-violet-600 hover:underline" "SMT-LIB") " — the standard input language for " (a :href "https://github.com/Z3Prover/z3" :class "text-violet-600 hover:underline" "Z3") " and other theorem provers.")
(p :class "text-stone-600"
@@ -24,67 +24,67 @@
(p :class "text-sm text-violet-700"
"The translator is written in SX itself (" (code "z3.sx") "). The SX evaluator executes " (code "z3.sx") " against the spec to produce SMT-LIB. Same pattern as " (code "bootstrap_js.py") " and " (code "bootstrap_py.py") ", but the transformation logic is an s-expression program transforming other s-expressions. All examples on this page are " (em "live output") " — not hardcoded strings.")))
(~doc-section :title "Arithmetic primitives" :id "arithmetic"
(~docs/section :title "Arithmetic primitives" :id "arithmetic"
(p :class "text-stone-600"
"Primitives with " (code ":body") " generate " (code "forall") " assertions. Z3 can verify the definition is satisfiable.")
(~doc-subsection :title "inc"
(~z3-example
(~docs/subsection :title "inc"
(~plans/reader-macro-demo/z3-example
:sx-source "(define-primitive \"inc\"\n :params (n)\n :returns \"number\"\n :doc \"Increment by 1.\"\n :body (+ n 1))"
:smt-output #z3(define-primitive "inc" :params (n) :returns "number" :doc "Increment by 1." :body (+ n 1))))
(~doc-subsection :title "clamp"
(~docs/subsection :title "clamp"
(p :class "text-stone-600 mb-2"
(code "max") " and " (code "min") " have no SMT-LIB equivalent — translated to " (code "ite") " (if-then-else).")
(~z3-example
(~plans/reader-macro-demo/z3-example
:sx-source "(define-primitive \"clamp\"\n :params (x lo hi)\n :returns \"number\"\n :doc \"Clamp x to range [lo, hi].\"\n :body (max lo (min hi x)))"
:smt-output #z3(define-primitive "clamp" :params (x lo hi) :returns "number" :doc "Clamp x to range [lo, hi]." :body (max lo (min hi x))))))
(~doc-section :title "Predicates" :id "predicates"
(~docs/section :title "Predicates" :id "predicates"
(p :class "text-stone-600"
"Predicates return " (code "Bool") " in SMT-LIB. " (code "mod") " and " (code "=") " are identity translations — same syntax in both languages.")
(~doc-subsection :title "odd?"
(~z3-example
(~docs/subsection :title "odd?"
(~plans/reader-macro-demo/z3-example
:sx-source "(define-primitive \"odd?\"\n :params (n)\n :returns \"boolean\"\n :doc \"True if n is odd.\"\n :body (= (mod n 2) 1))"
:smt-output #z3(define-primitive "odd?" :params (n) :returns "boolean" :doc "True if n is odd." :body (= (mod n 2) 1))))
(~doc-subsection :title "!= (inequality)"
(~z3-example
(~docs/subsection :title "!= (inequality)"
(~plans/reader-macro-demo/z3-example
:sx-source "(define-primitive \"!=\"\n :params (a b)\n :returns \"boolean\"\n :doc \"Inequality.\"\n :body (not (= a b)))"
:smt-output #z3(define-primitive "!=" :params (a b) :returns "boolean" :doc "Inequality." :body (not (= a b))))))
(~doc-section :title "Variadics and bodyless" :id "variadics"
(~docs/section :title "Variadics and bodyless" :id "variadics"
(p :class "text-stone-600"
"Variadic primitives (" (code "&rest") ") are declared as uninterpreted functions — Z3 can reason about their properties but not their implementation. Primitives without " (code ":body") " get only a declaration.")
(~doc-subsection :title "+ (variadic)"
(~z3-example
(~docs/subsection :title "+ (variadic)"
(~plans/reader-macro-demo/z3-example
:sx-source "(define-primitive \"+\"\n :params (&rest args)\n :returns \"number\"\n :doc \"Sum all arguments.\")"
:smt-output #z3(define-primitive "+" :params (&rest args) :returns "number" :doc "Sum all arguments.")))
(~doc-subsection :title "nil? (no body)"
(~z3-example
(~docs/subsection :title "nil? (no body)"
(~plans/reader-macro-demo/z3-example
:sx-source "(define-primitive \"nil?\"\n :params (x)\n :returns \"boolean\"\n :doc \"True if x is nil/null/None.\")"
:smt-output #z3(define-primitive "nil?" :params (x) :returns "boolean" :doc "True if x is nil/null/None."))))
(~doc-section :title "Translate entire spec files" :id "full-specs"
(~docs/section :title "Translate entire spec files" :id "full-specs"
(p :class "text-stone-600"
"The translator can process entire spec files. " (code "z3-translate-file") " in " (code "z3.sx") " filters for " (code "define-primitive") ", " (code "define-io-primitive") ", and " (code "define-special-form") " declarations, translates each, and concatenates the output.")
(p :class "text-stone-600"
"Below is the live SMT-LIB output from translating the full " (code "primitives.sx") " — all 87 primitive declarations. The composition is pure SX: " (code "(z3-translate-file (sx-parse (read-spec-file \"primitives.sx\")))") " — read the file, parse it, translate it. No Python glue.")
(~doc-subsection :title "primitives.sx (87 primitives)"
(~doc-code :code (highlight (z3-translate-file (sx-parse (read-spec-file "primitives.sx"))) "lisp"))))
(~docs/subsection :title "primitives.sx (87 primitives)"
(~docs/code :code (highlight (z3-translate-file (sx-parse (read-spec-file "primitives.sx"))) "lisp"))))
(~doc-section :title "The translator: z3.sx" :id "z3-source"
(~docs/section :title "The translator: z3.sx" :id "z3-source"
(p :class "text-stone-600"
"The entire translator is a single SX file — s-expressions that walk other s-expressions and emit strings. No host language logic. The same file runs in Python (server) and could run in JavaScript (browser) via the bootstrapped evaluator.")
(~doc-code :code (highlight (read-spec-file "z3.sx") "lisp"))
(~docs/code :code (highlight (read-spec-file "z3.sx") "lisp"))
(p :class "text-stone-600 mt-4"
"359 lines. The key functions: " (code "z3-sort") " maps SX types to SMT-LIB sorts. " (code "z3-expr") " recursively translates expressions — identity ops pass through unchanged, " (code "max") "/" (code "min") " become " (code "ite") ", predicates get renamed. " (code "z3-translate") " dispatches on form type. " (code "z3-translate-file") " filters and batch-translates."))
(~doc-section :title "The pattern: SX → anything" :id "sx-to-anything"
(~docs/section :title "The pattern: SX → anything" :id "sx-to-anything"
(p :class "text-stone-600"
"z3.sx proves the pattern: an SX program that transforms SX ASTs into a target language. The same pattern works for any target.")
(div :class "rounded border border-stone-200 bg-stone-50 p-4 mt-2 mb-4"
@@ -120,7 +120,7 @@
(p :class "text-stone-600"
"A " (code "py.sx") " wouldn't be limited to the spec. Any SX expression could be translated: " (code "#py(map (fn (x) (* x x)) items)") " → " (code "list(map(lambda x: x * x, items))") ". The bootstrappers (" (code "bootstrap_js.py") ", " (code "bootstrap_py.py") ") are Python programs that do this for the full spec. " (code "py.sx") " would be the same thing, written in SX — a self-hosting bootstrapper."))
(~doc-section :title "How it works" :id "how-it-works"
(~docs/section :title "How it works" :id "how-it-works"
(p :class "text-stone-600"
"The " (code "#z3") " reader macro is registered before parsing. When the parser hits " (code "#z3(define-primitive ...)") ", it:")
(ol :class "list-decimal pl-6 space-y-2 text-stone-600"
@@ -135,7 +135,7 @@
(p :class "text-stone-600"
"The handler is a pure function from AST to value. No side effects. No mutation. Reader macros are " (em "syntax transformations") " — they happen before evaluation, before rendering, before anything else. They are the earliest possible extension point."))
(~doc-section :title "The strange loop" :id "strange-loop"
(~docs/section :title "The strange loop" :id "strange-loop"
(p :class "text-stone-600"
"The SX specification files are simultaneously:")
(ul :class "list-disc pl-6 space-y-2 text-stone-600"

View File

@@ -2,32 +2,32 @@
;; Reader Macros
;; ---------------------------------------------------------------------------
(defcomp ~plan-reader-macros-content ()
(~doc-page :title "Reader Macros"
(defcomp ~plans/reader-macros/plan-reader-macros-content ()
(~docs/page :title "Reader Macros"
(~doc-section :title "Context" :id "context"
(~docs/section :title "Context" :id "context"
(p "SX has three hardcoded reader transformations: " (code "`") " → " (code "(quasiquote ...)") ", " (code ",") " → " (code "(unquote ...)") ", " (code ",@") " → " (code "(splice-unquote ...)") ". These are baked into the parser with no extensibility. The " (code "~") " prefix for components and " (code "&") " for param modifiers are just symbol characters, handled at eval time.")
(p "Reader macros add parse-time transformations triggered by a dispatch character. Motivating use case: a " (code "~md") " component that uses heredoc syntax for markdown source instead of string literals. More broadly: datum comments, raw strings, and custom literal syntax."))
(~doc-section :title "Design" :id "design"
(~docs/section :title "Design" :id "design"
(~doc-subsection :title "Dispatch Character: #"
(~docs/subsection :title "Dispatch Character: #"
(p "Lisp tradition. " (code "#") " is NOT in " (code "ident-start") " or " (code "ident-char") " — completely free. Pattern:")
(~doc-code :code (highlight "#;expr → (read and discard expr, return next)\n#|...| → raw string literal\n#'expr → (quote expr)" "lisp")))
(~docs/code :code (highlight "#;expr → (read and discard expr, return next)\n#|...| → raw string literal\n#'expr → (quote expr)" "lisp")))
(~doc-subsection :title "#; — Datum comment"
(~docs/subsection :title "#; — Datum comment"
(p "Scheme/Racket standard. Reads and discards the next expression. Preserves balanced parens.")
(~doc-code :code (highlight "(list 1 #;(this is commented out) 2 3) → (list 1 2 3)" "lisp")))
(~docs/code :code (highlight "(list 1 #;(this is commented out) 2 3) → (list 1 2 3)" "lisp")))
(~doc-subsection :title "#|...| — Raw string"
(~docs/subsection :title "#|...| — Raw string"
(p "No escape processing. Everything between " (code "#|") " and " (code "|") " is literal. Enables inline markdown, regex patterns, code examples.")
(~doc-code :code (highlight "(~md #|## Title\n\nSome **bold** text with \"quotes\" and \\backslashes.|)" "lisp")))
(~docs/code :code (highlight "(~md #|## Title\n\nSome **bold** text with \"quotes\" and \\backslashes.|)" "lisp")))
(~doc-subsection :title "#' — Quote shorthand"
(~docs/subsection :title "#' — Quote shorthand"
(p "Currently no single-char quote (" (code "`") " is quasiquote).")
(~doc-code :code (highlight "#'my-function → (quote my-function)" "lisp")))
(~docs/code :code (highlight "#'my-function → (quote my-function)" "lisp")))
(~doc-subsection :title "Extensible dispatch: #name"
(~docs/subsection :title "Extensible dispatch: #name"
(p "User-defined reader macros via " (code "#name expr") ". The parser reads an identifier after " (code "#") ", looks up a handler in the reader macro registry, and calls it with the next parsed expression. See the " (a :href "/sx/(etc.(plan.reader-macro-demo))" :class "text-violet-600 hover:underline" "#z3 demo") " for a working example that translates SX spec declarations to SMT-LIB.")))
@@ -35,35 +35,35 @@
;; Implementation
;; -----------------------------------------------------------------------
(~doc-section :title "Implementation" :id "implementation"
(~docs/section :title "Implementation" :id "implementation"
(~doc-subsection :title "1. Spec: parser.sx"
(~docs/subsection :title "1. Spec: parser.sx"
(p "Add " (code "#") " dispatch to " (code "read-expr") " (after the " (code ",") "/" (code ",@") " case, before number). Add " (code "read-raw-string") " helper function.")
(~doc-code :code (highlight ";; Reader macro dispatch\n(= ch \"#\")\n (do (set! pos (inc pos))\n (if (>= pos len-src)\n (error \"Unexpected end of input after #\")\n (let ((dispatch-ch (nth source pos)))\n (cond\n ;; #; — datum comment: read and discard next expr\n (= dispatch-ch \";\")\n (do (set! pos (inc pos))\n (read-expr) ;; read and discard\n (read-expr)) ;; return the NEXT expr\n\n ;; #| — raw string\n (= dispatch-ch \"|\")\n (do (set! pos (inc pos))\n (read-raw-string))\n\n ;; #' — quote shorthand\n (= dispatch-ch \"'\")\n (do (set! pos (inc pos))\n (list (make-symbol \"quote\") (read-expr)))\n\n :else\n (error (str \"Unknown reader macro: #\" dispatch-ch))))))" "lisp"))
(~docs/code :code (highlight ";; Reader macro dispatch\n(= ch \"#\")\n (do (set! pos (inc pos))\n (if (>= pos len-src)\n (error \"Unexpected end of input after #\")\n (let ((dispatch-ch (nth source pos)))\n (cond\n ;; #; — datum comment: read and discard next expr\n (= dispatch-ch \";\")\n (do (set! pos (inc pos))\n (read-expr) ;; read and discard\n (read-expr)) ;; return the NEXT expr\n\n ;; #| — raw string\n (= dispatch-ch \"|\")\n (do (set! pos (inc pos))\n (read-raw-string))\n\n ;; #' — quote shorthand\n (= dispatch-ch \"'\")\n (do (set! pos (inc pos))\n (list (make-symbol \"quote\") (read-expr)))\n\n :else\n (error (str \"Unknown reader macro: #\" dispatch-ch))))))" "lisp"))
(p "The " (code "read-raw-string") " helper:")
(~doc-code :code (highlight "(define read-raw-string\n (fn ()\n (let ((buf \"\"))\n (define raw-loop\n (fn ()\n (if (>= pos len-src)\n (error \"Unterminated raw string\")\n (let ((ch (nth source pos)))\n (if (= ch \"|\")\n (do (set! pos (inc pos)) nil) ;; done\n (do (set! buf (str buf ch))\n (set! pos (inc pos))\n (raw-loop)))))))\n (raw-loop)\n buf)))" "lisp")))
(~docs/code :code (highlight "(define read-raw-string\n (fn ()\n (let ((buf \"\"))\n (define raw-loop\n (fn ()\n (if (>= pos len-src)\n (error \"Unterminated raw string\")\n (let ((ch (nth source pos)))\n (if (= ch \"|\")\n (do (set! pos (inc pos)) nil) ;; done\n (do (set! buf (str buf ch))\n (set! pos (inc pos))\n (raw-loop)))))))\n (raw-loop)\n buf)))" "lisp")))
(~doc-subsection :title "2. Python: parser.py"
(~docs/subsection :title "2. Python: parser.py"
(p "Add " (code "#") " dispatch to " (code "_parse_expr()") " (after " (code ",") "/" (code ",@") " handling ~line 252). Add " (code "_read_raw_string()") " method to Tokenizer.")
(~doc-code :code (highlight "# In _parse_expr(), after the comma/splice-unquote block:\nif raw == \"#\":\n tok._advance(1) # consume the #\n if tok.pos >= len(tok.text):\n raise ParseError(\"Unexpected end of input after #\",\n tok.pos, tok.line, tok.col)\n dispatch = tok.text[tok.pos]\n if dispatch == \";\":\n tok._advance(1)\n _parse_expr(tok) # read and discard\n return _parse_expr(tok) # return next\n if dispatch == \"|\":\n tok._advance(1)\n return tok._read_raw_string()\n if dispatch == \"'\":\n tok._advance(1)\n return [Symbol(\"quote\"), _parse_expr(tok)]\n raise ParseError(f\"Unknown reader macro: #{dispatch}\",\n tok.pos, tok.line, tok.col)" "python"))
(~docs/code :code (highlight "# In _parse_expr(), after the comma/splice-unquote block:\nif raw == \"#\":\n tok._advance(1) # consume the #\n if tok.pos >= len(tok.text):\n raise ParseError(\"Unexpected end of input after #\",\n tok.pos, tok.line, tok.col)\n dispatch = tok.text[tok.pos]\n if dispatch == \";\":\n tok._advance(1)\n _parse_expr(tok) # read and discard\n return _parse_expr(tok) # return next\n if dispatch == \"|\":\n tok._advance(1)\n return tok._read_raw_string()\n if dispatch == \"'\":\n tok._advance(1)\n return [Symbol(\"quote\"), _parse_expr(tok)]\n raise ParseError(f\"Unknown reader macro: #{dispatch}\",\n tok.pos, tok.line, tok.col)" "python"))
(p "The " (code "_read_raw_string()") " method on Tokenizer:")
(~doc-code :code (highlight "def _read_raw_string(self) -> str:\n buf = []\n while self.pos < len(self.text):\n ch = self.text[self.pos]\n if ch == \"|\":\n self._advance(1)\n return \"\".join(buf)\n buf.append(ch)\n self._advance(1)\n raise ParseError(\"Unterminated raw string\",\n self.pos, self.line, self.col)" "python")))
(~docs/code :code (highlight "def _read_raw_string(self) -> str:\n buf = []\n while self.pos < len(self.text):\n ch = self.text[self.pos]\n if ch == \"|\":\n self._advance(1)\n return \"\".join(buf)\n buf.append(ch)\n self._advance(1)\n raise ParseError(\"Unterminated raw string\",\n self.pos, self.line, self.col)" "python")))
(~doc-subsection :title "3. JS: auto-transpiled"
(~docs/subsection :title "3. JS: auto-transpiled"
(p "JS parser comes from bootstrap of parser.sx — spec change handles it automatically."))
(~doc-subsection :title "4. Rebootstrap both targets"
(~docs/subsection :title "4. Rebootstrap both targets"
(p "Run " (code "bootstrap_js.py") " and " (code "bootstrap_py.py") " to regenerate " (code "sx-ref.js") " and " (code "sx_ref.py") " from the updated parser.sx spec."))
(~doc-subsection :title "5. Grammar update"
(~docs/subsection :title "5. Grammar update"
(p "Add reader macro syntax to the grammar comment at the top of parser.sx:")
(~doc-code :code (highlight ";; reader → '#;' expr (datum comment)\n;; | '#|' raw-chars '|' (raw string)\n;; | \"#'\" expr (quote shorthand)" "lisp"))))
(~docs/code :code (highlight ";; reader → '#;' expr (datum comment)\n;; | '#|' raw-chars '|' (raw string)\n;; | \"#'\" expr (quote shorthand)" "lisp"))))
;; -----------------------------------------------------------------------
;; Files
;; -----------------------------------------------------------------------
(~doc-section :title "Files" :id "files"
(~docs/section :title "Files" :id "files"
(div :class "overflow-x-auto rounded border border-stone-200"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-100"
@@ -87,9 +87,9 @@
;; Verification
;; -----------------------------------------------------------------------
(~doc-section :title "Verification" :id "verification"
(~docs/section :title "Verification" :id "verification"
(~doc-subsection :title "Parse tests"
(~docs/subsection :title "Parse tests"
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "#;(ignored) 42 → 42")
(li "(list 1 #;2 3) → (list 1 3)")
@@ -99,7 +99,7 @@
(li "# at EOF → error")
(li "#x unknown → error")))
(~doc-subsection :title "Regression"
(~docs/subsection :title "Regression"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "All existing parser tests pass after changes")
(li "Rebootstrapped JS and Python pass their test suites")

View File

@@ -2,10 +2,10 @@
;; Runtime Slicing
;; ---------------------------------------------------------------------------
(defcomp ~plan-runtime-slicing-content ()
(~doc-page :title "Runtime Slicing"
(defcomp ~plans/runtime-slicing/plan-runtime-slicing-content ()
(~docs/page :title "Runtime Slicing"
(~doc-section :title "The Problem" :id "problem"
(~docs/section :title "The Problem" :id "problem"
(p "sx-browser.js is the full SX client runtime — evaluator, parser, renderer, engine, morph, signals, routing, orchestration, boot. Every page loads all of it.")
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
@@ -39,7 +39,7 @@
;; Tiers
;; -----------------------------------------------------------------------
(~doc-section :title "Tiers" :id "tiers"
(~docs/section :title "Tiers" :id "tiers"
(p "Four tiers, matching the " (a :href "/sx/(geography.(reactive.plan))" :class "text-violet-700 underline" "reactive islands") " levels:")
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
@@ -81,11 +81,11 @@
;; The slicer is SX
;; -----------------------------------------------------------------------
(~doc-section :title "The Slicer is SX" :id "slicer-is-sx"
(~docs/section :title "The Slicer is SX" :id "slicer-is-sx"
(p "Per the " (a :href "/sx/(etc.(plan.self-hosting-bootstrapper))" :class "text-violet-700 underline" "self-hosting principle") ", the slicer is not a build tool — it's a spec module. " (code "slice.sx") " analyzes the spec's own dependency graph and determines which defines belong to which tier.")
(p (code "js.sx") " (the self-hosting SX-to-JavaScript translator) already compiles the full spec. Slicing is a filter: " (code "js.sx") " translates only the defines that " (code "slice.sx") " selects for a given tier.")
(~doc-code :code (highlight ";; slice.sx — determine which defines each tier needs\n;;\n;; Input: the full list of defines from all spec files\n;; Output: a filtered list for the requested tier\n\n(define tier-deps\n ;; Which spec modules each tier requires\n {:L0 (list \"engine\" \"boot-partial\")\n :L1 (list \"engine\" \"boot-partial\" \"dom-partial\")\n :L2 (list \"engine\" \"boot-partial\" \"dom-partial\"\n \"signals\" \"dom-island\")\n :L3 (list \"eval\" \"render\" \"parser\"\n \"engine\" \"orchestration\" \"boot\"\n \"dom\" \"signals\" \"router\")})\n\n(define slice-defines\n (fn (tier all-defines)\n ;; 1. Get the module list for this tier\n ;; 2. Walk each define's dependency references\n ;; 3. Include a define only if ALL its deps are\n ;; satisfiable within the tier's module set\n ;; 4. Return the filtered define list\n (let ((modules (get tier-deps tier)))\n (filter\n (fn (d) (tier-satisfies? modules (define-deps d)))\n all-defines))))" "lisp"))
(~docs/code :code (highlight ";; slice.sx — determine which defines each tier needs\n;;\n;; Input: the full list of defines from all spec files\n;; Output: a filtered list for the requested tier\n\n(define tier-deps\n ;; Which spec modules each tier requires\n {:L0 (list \"engine\" \"boot-partial\")\n :L1 (list \"engine\" \"boot-partial\" \"dom-partial\")\n :L2 (list \"engine\" \"boot-partial\" \"dom-partial\"\n \"signals\" \"dom-island\")\n :L3 (list \"eval\" \"render\" \"parser\"\n \"engine\" \"orchestration\" \"boot\"\n \"dom\" \"signals\" \"router\")})\n\n(define slice-defines\n (fn (tier all-defines)\n ;; 1. Get the module list for this tier\n ;; 2. Walk each define's dependency references\n ;; 3. Include a define only if ALL its deps are\n ;; satisfiable within the tier's module set\n ;; 4. Return the filtered define list\n (let ((modules (get tier-deps tier)))\n (filter\n (fn (d) (tier-satisfies? modules (define-deps d)))\n all-defines))))" "lisp"))
(p "The pipeline becomes:")
(ol :class "list-decimal pl-5 text-stone-700 space-y-2"
@@ -100,10 +100,10 @@
;; Dependency analysis
;; -----------------------------------------------------------------------
(~doc-section :title "Define-Level Dependency Analysis" :id "deps"
(~docs/section :title "Define-Level Dependency Analysis" :id "deps"
(p "The slicer needs to know which defines reference which other defines. This is a simpler version of what " (code "deps.sx") " does for components — but at the define level, not the component level.")
(~doc-code :code (highlight ";; Walk a define's body AST, collect all symbol references\n;; that resolve to other top-level defines.\n;;\n;; (define morph-children\n;; (fn (old-node new-node)\n;; ...uses morph-node, morph-attrs, create-element...))\n;;\n;; → deps: #{morph-node morph-attrs create-element}\n\n(define define-refs\n (fn (body all-define-names)\n ;; Collect all symbols in body that appear in all-define-names\n (let ((refs (make-set)))\n (walk-ast body\n (fn (node)\n (when (and (symbol? node)\n (set-has? all-define-names (symbol-name node)))\n (set-add! refs (symbol-name node)))))\n refs)))" "lisp"))
(~docs/code :code (highlight ";; Walk a define's body AST, collect all symbol references\n;; that resolve to other top-level defines.\n;;\n;; (define morph-children\n;; (fn (old-node new-node)\n;; ...uses morph-node, morph-attrs, create-element...))\n;;\n;; → deps: #{morph-node morph-attrs create-element}\n\n(define define-refs\n (fn (body all-define-names)\n ;; Collect all symbols in body that appear in all-define-names\n (let ((refs (make-set)))\n (walk-ast body\n (fn (node)\n (when (and (symbol? node)\n (set-has? all-define-names (symbol-name node)))\n (set-add! refs (symbol-name node)))))\n refs)))" "lisp"))
(p "From these per-define deps, we build the full dependency graph and compute transitive closures. A tier's define set is the transitive closure of its entry points:")
@@ -135,11 +135,11 @@
;; Platform interface slicing
;; -----------------------------------------------------------------------
(~doc-section :title "Platform Interface Slicing" :id "platform"
(~docs/section :title "Platform Interface Slicing" :id "platform"
(p "The bootstrapped code (from js.sx) is only half the story. Each define also depends on platform primitives — the hand-written JS glue that js.sx doesn't produce. These live in " (code "bootstrap_js.py") "'s PLATFORM_* blocks.")
(p "The slicer must track platform deps too:")
(~doc-code :code (highlight ";; Platform primitives referenced by tier\n;;\n;; L0 needs: createElement, setAttribute, morphAttrs,\n;; fetch (for sx-get/post), pushState, replaceState\n;;\n;; L1 adds: classList.toggle, addEventListener\n;;\n;; L2 adds: createComment, createTextNode, domRemove,\n;; domChildNodes — the reactive DOM primitives\n;;\n;; L3 adds: everything in PLATFORM_PARSER_JS,\n;; full DOM adapter, async IO bridge\n\n(define platform-deps\n {:L0 (list :dom-morph :fetch :history)\n :L1 (list :dom-morph :fetch :history :dom-events)\n :L2 (list :dom-morph :fetch :history :dom-events\n :dom-reactive :signal-constructors)\n :L3 (list :dom-morph :fetch :history :dom-events\n :dom-reactive :signal-constructors\n :parser :evaluator :async-io)})" "lisp"))
(~docs/code :code (highlight ";; Platform primitives referenced by tier\n;;\n;; L0 needs: createElement, setAttribute, morphAttrs,\n;; fetch (for sx-get/post), pushState, replaceState\n;;\n;; L1 adds: classList.toggle, addEventListener\n;;\n;; L2 adds: createComment, createTextNode, domRemove,\n;; domChildNodes — the reactive DOM primitives\n;;\n;; L3 adds: everything in PLATFORM_PARSER_JS,\n;; full DOM adapter, async IO bridge\n\n(define platform-deps\n {:L0 (list :dom-morph :fetch :history)\n :L1 (list :dom-morph :fetch :history :dom-events)\n :L2 (list :dom-morph :fetch :history :dom-events\n :dom-reactive :signal-constructors)\n :L3 (list :dom-morph :fetch :history :dom-events\n :dom-reactive :signal-constructors\n :parser :evaluator :async-io)})" "lisp"))
(p "The platform JS blocks in " (code "bootstrap_js.py") " are already organized by adapter (" (code "PLATFORM_DOM_JS") ", " (code "PLATFORM_ENGINE_PURE_JS") ", etc). Slicing further subdivides these into the minimal set each tier needs.")
(p "This subdivision also happens in SX: " (code "slice.sx") " declares which platform blocks each tier requires. " (code "js.sx") " doesn't need to change — it translates defines. The bootstrapper script reads the slice spec and assembles the platform preamble accordingly."))
@@ -148,18 +148,18 @@
;; Progressive loading
;; -----------------------------------------------------------------------
(~doc-section :title "Progressive Loading" :id "progressive"
(~docs/section :title "Progressive Loading" :id "progressive"
(p "The simplest approach: one file per tier. The server knows each page's tier (from " (code "defpage") " metadata or component analysis) and serves the right script tag.")
(p "Better: a base file (L0) that all pages load, plus tier deltas loaded on demand.")
(~doc-code :code (highlight ";; Server emits the appropriate script for the page's tier\n;;\n;; L0 page (blog post, product listing):\n;; <script src=\"/static/scripts/sx-L0.js\"></script>\n;;\n;; L2 page (reactive island):\n;; <script src=\"/static/scripts/sx-L0.js\"></script>\n;; <script src=\"/static/scripts/sx-L2-delta.js\"></script>\n;;\n;; Client-side navigation from L0 → L2:\n;; 1. L0 runtime handles the swap\n;; 2. New page declares tier=L2 in response header\n;; 3. L0 runtime loads sx-L2-delta.js dynamically\n;; 4. Island hydration proceeds\n\n(define page-tier\n (fn (page)\n ;; Analyze the page's component tree\n ;; If any component is defisland → L2\n ;; If any component uses on-event/toggle! → L1\n ;; Otherwise → L0\n (cond\n ((page-has-islands? page) :L2)\n ((page-has-dom-ops? page) :L1)\n (true :L0))))" "lisp"))
(~docs/code :code (highlight ";; Server emits the appropriate script for the page's tier\n;;\n;; L0 page (blog post, product listing):\n;; <script src=\"/static/scripts/sx-L0.js\"></script>\n;;\n;; L2 page (reactive island):\n;; <script src=\"/static/scripts/sx-L0.js\"></script>\n;; <script src=\"/static/scripts/sx-L2-delta.js\"></script>\n;;\n;; Client-side navigation from L0 → L2:\n;; 1. L0 runtime handles the swap\n;; 2. New page declares tier=L2 in response header\n;; 3. L0 runtime loads sx-L2-delta.js dynamically\n;; 4. Island hydration proceeds\n\n(define page-tier\n (fn (page)\n ;; Analyze the page's component tree\n ;; If any component is defisland → L2\n ;; If any component uses on-event/toggle! → L1\n ;; Otherwise → L0\n (cond\n ((page-has-islands? page) :L2)\n ((page-has-dom-ops? page) :L1)\n (true :L0))))" "lisp"))
(~doc-subsection :title "SX-Tier Response Header"
(~docs/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"))
(~docs/code :code (highlight "HTTP/1.1 200 OK\nSX-Tier: L0\nSX-Components: ~card:bafy...,~plans/environment-images/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"
(~docs/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 "/sx/(etc.(plan.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.")))
@@ -167,10 +167,10 @@
;; Automatic tier detection
;; -----------------------------------------------------------------------
(~doc-section :title "Automatic Tier Detection" :id "auto-detect"
(~docs/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"))
(~docs/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."))
@@ -178,7 +178,7 @@
;; What L0 actually needs
;; -----------------------------------------------------------------------
(~doc-section :title "What L0 Actually Needs" :id "l0-detail"
(~docs/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"
@@ -233,10 +233,10 @@
;; Build pipeline
;; -----------------------------------------------------------------------
(~doc-section :title "Build Pipeline" :id "pipeline"
(~docs/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"))
(~docs/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.")
@@ -248,7 +248,7 @@
;; Spec modules
;; -----------------------------------------------------------------------
(~doc-section :title "Spec Modules" :id "spec-modules"
(~docs/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"
@@ -271,7 +271,7 @@
;; Relationships
;; -----------------------------------------------------------------------
(~doc-section :title "Relationships" :id "relationships"
(~docs/section :title "Relationships" :id "relationships"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (a :href "/sx/(etc.(plan.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 "/sx/(etc.(plan.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.")

View File

@@ -3,8 +3,8 @@
;; ---------------------------------------------------------------------------
;; @css bg-green-100 text-green-800 bg-green-50 border-green-200 text-green-700 text-green-600
(defcomp ~plan-self-hosting-bootstrapper-content ()
(~doc-page :title "Self-Hosting Bootstrapper"
(defcomp ~plans/self-hosting-bootstrapper/plan-self-hosting-bootstrapper-content ()
(~docs/page :title "Self-Hosting Bootstrapper"
;; -----------------------------------------------------------------------
;; Status banner
@@ -24,7 +24,7 @@
;; The Idea
;; -----------------------------------------------------------------------
(~doc-section :title "The Idea" :id "idea"
(~docs/section :title "The Idea" :id "idea"
(p "We have " (code "bootstrap_py.py") " — a Python program that reads "
(code ".sx") " spec files and emits " (code "sx_ref.py")
", a standalone Python evaluator. It's a compiler written in the host language.")
@@ -42,7 +42,7 @@
;; Results
;; -----------------------------------------------------------------------
(~doc-section :title "Results" :id "results"
(~docs/section :title "Results" :id "results"
(div :class "overflow-x-auto rounded border border-stone-200 my-4"
(table :class "w-full text-sm"
(thead :class "bg-stone-50"
@@ -73,7 +73,7 @@
;; Architecture
;; -----------------------------------------------------------------------
(~doc-section :title "Architecture" :id "architecture"
(~docs/section :title "Architecture" :id "architecture"
(p "Three bootstrapper generations:")
(div :class "overflow-x-auto rounded border border-stone-200 my-4"
(table :class "w-full text-sm"
@@ -112,9 +112,9 @@
;; Translation Rules
;; -----------------------------------------------------------------------
(~doc-section :title "Translation Rules" :id "translation"
(~docs/section :title "Translation Rules" :id "translation"
(~doc-subsection :title "Name Mangling"
(~docs/subsection :title "Name Mangling"
(p "SX identifiers become valid Python identifiers. "
"The RENAMES dict (200+ entries) handles explicit mappings; "
"general rules handle the rest:")
@@ -147,7 +147,7 @@
(td :class "px-4 py-2 font-mono" "type_")
(td :class "px-4 py-2" "Python reserved word escape"))))))
(~doc-subsection :title "Special Forms"
(~docs/subsection :title "Special Forms"
(p "Each SX special form maps to a Python expression pattern:")
(div :class "overflow-x-auto rounded border border-stone-200 my-4"
(table :class "w-full text-sm"
@@ -172,11 +172,11 @@
(td :class "px-4 py-2 font-mono" "(case x \"a\" 1)")
(td :class "px-4 py-2 font-mono" "_sx_case(x, [(\"a\", lambda: 1)])"))))))
(~doc-subsection :title "Mutation: set! and Cell Variables"
(~docs/subsection :title "Mutation: set! and Cell Variables"
(p "Python closures can read but not rebind outer variables. "
(code "py.sx") " detects " (code "set!") " targets that cross lambda boundaries "
"and routes them through a " (code "_cells") " dict:")
(~doc-code :code (highlight ";; SX ;; Python
(~docs/code :code (highlight ";; SX ;; Python
(define counter def counter():
(fn () _cells = {}
(let ((n 0)) _cells['n'] = 0
@@ -192,7 +192,7 @@
;; Scope
;; -----------------------------------------------------------------------
(~doc-section :title "Scope" :id "scope"
(~docs/section :title "Scope" :id "scope"
(p (code "py.sx") " is a general-purpose SX-to-Python translator. "
"The translation rules are mechanical and apply to " (em "all") " SX, "
"not just the spec subset.")
@@ -214,19 +214,19 @@
;; Implications
;; -----------------------------------------------------------------------
(~doc-section :title "Implications" :id "implications"
(~doc-subsection :title "Practical"
(~docs/section :title "Implications" :id "implications"
(~docs/subsection :title "Practical"
(p "One less Python file to maintain. Changes to the transpilation logic "
"are written in SX and tested with the SX test harness. The spec and its "
"compiler live in the same language."))
(~doc-subsection :title "Architectural"
(~docs/subsection :title "Architectural"
(p "With " (code "z3.sx") " (SMT-LIB) and " (code "py.sx") " (Python), "
"the pattern is clear: SX translators are SX programs. "
"Adding a new target language means writing one " (code ".sx") " file, "
"not a new Python compiler."))
(~doc-subsection :title "Philosophical"
(~docs/subsection :title "Philosophical"
(p "A self-hosting bootstrapper is a fixed point. The spec defines behavior. "
"The translator is itself defined in terms of that behavior. Running the "
"translator on the spec produces a program that can run the translator on "

View File

@@ -2,15 +2,15 @@
;; Social Sharing
;; ---------------------------------------------------------------------------
(defcomp ~plan-social-sharing-content ()
(~doc-page :title "Social Network Sharing"
(defcomp ~plans/social-sharing/plan-social-sharing-content ()
(~docs/page :title "Social Network Sharing"
(~doc-section :title "Context" :id "context"
(~docs/section :title "Context" :id "context"
(p "Rose Ash already has ActivityPub for federated social sharing. This plan adds OAuth-based sharing to mainstream social networks — Facebook, Instagram, Threads, Twitter/X, LinkedIn, and Mastodon.")
(p "All social logic lives in the " (strong "account") " microservice. Content apps get a share button that opens the account share page."))
(~doc-section :title "What remains" :id "remains"
(~doc-note "Nothing has been implemented. This is the full scope of work.")
(~docs/section :title "What remains" :id "remains"
(~docs/note "Nothing has been implemented. This is the full scope of work.")
(div :class "space-y-4"

View File

@@ -2,10 +2,10 @@
;; Spec Explorer — The Fifth Ring
;; ---------------------------------------------------------------------------
(defcomp ~plan-spec-explorer-content ()
(~doc-page :title "Spec Explorer"
(defcomp ~plans/spec-explorer/plan-spec-explorer-content ()
(~docs/page :title "Spec Explorer"
(~doc-section :title "The Five Rings" :id "five-rings"
(~docs/section :title "The Five Rings" :id "five-rings"
(p "SX has a peculiar architecture. At its centre sits a specification — a set of s-expression files that define the language. Not a description of the language. Not documentation about the language. The specification " (em "is") " the language. It is simultaneously a formal definition and executable code. You can read it as a document or run it as a program. It does not describe how to build an SX evaluator; it " (em "is") " an SX evaluator, expressed in the language it defines.")
(p "This is the nucleus. Everything else radiates outward from it.")
@@ -46,7 +46,7 @@
;; What the explorer shows
;; -----------------------------------------------------------------------
(~doc-section :title "Per-Function Cards" :id "cards"
(~docs/section :title "Per-Function Cards" :id "cards"
(p "Each function in the spec gets a card showing all five rings:")
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
@@ -86,7 +86,7 @@
;; Effect system
;; -----------------------------------------------------------------------
(~doc-section :title "Effect Annotations" :id "effects"
(~docs/section :title "Effect Annotations" :id "effects"
(p "Every function in the spec now carries an " (code ":effects") " annotation declaring what kind of side effects it performs:")
(div :class "flex flex-wrap gap-3 my-4"
@@ -97,68 +97,68 @@
(p "The explorer shows effect badges on each function card, and the stats bar aggregates them across the whole file. Pure functions (green) are the nucleus — no side effects, fully deterministic, safe to cache, reorder, or parallelise.")
(~doc-code :code (highlight "(define signal :effects []\n (fn ((initial-value :as any))\n (make-signal initial-value)))\n\n(define reset! :effects [mutation]\n (fn ((s :as signal) value)\n (when (signal? s)\n (let ((old (signal-value s)))\n (when (not (identical? old value))\n (signal-set-value! s value)\n (notify-subscribers s))))))" "sx")))
(~docs/code :code (highlight "(define signal :effects []\n (fn ((initial-value :as any))\n (make-signal initial-value)))\n\n(define reset! :effects [mutation]\n (fn ((s :as signal) value)\n (when (signal? s)\n (let ((old (signal-value s)))\n (when (not (identical? old value))\n (signal-set-value! s value)\n (notify-subscribers s))))))" "sx")))
;; -----------------------------------------------------------------------
;; Bootstrapper translations
;; -----------------------------------------------------------------------
(~doc-section :title "Bootstrapper Translations" :id "translations"
(~docs/section :title "Bootstrapper Translations" :id "translations"
(p "Each function is translated by the actual bootstrappers that build the production runtime. The same " (code "signal") " function shown in three target languages:")
(~doc-subsection :title "Python (via bootstrap_py.py)"
(~doc-code :code (highlight "def signal(initial_value):\n return make_signal(initial_value)" "python"))
(~docs/subsection :title "Python (via bootstrap_py.py)"
(~docs/code :code (highlight "def signal(initial_value):\n return make_signal(initial_value)" "python"))
(p :class "text-sm text-stone-500" (code "PyEmitter._emit_define()") " — the exact same code path that generates " (code "sx_ref.py") "."))
(~doc-subsection :title "JavaScript (via js.sx)"
(~doc-code :code (highlight "var signal = function(initial_value) {\n return make_signal(initial_value);\n};" "javascript"))
(~docs/subsection :title "JavaScript (via js.sx)"
(~docs/code :code (highlight "var signal = function(initial_value) {\n return make_signal(initial_value);\n};" "javascript"))
(p :class "text-sm text-stone-500" (code "js-emit-define") " — the self-hosting JS bootstrapper, written in SX, evaluated by the Python evaluator."))
(~doc-subsection :title "Z3 / SMT-LIB (via z3.sx)"
(~doc-code :code (highlight "; signal — Create a reactive signal container with an initial value.\n(declare-fun signal (Value) Value)" "lisp"))
(~docs/subsection :title "Z3 / SMT-LIB (via z3.sx)"
(~docs/code :code (highlight "; signal — Create a reactive signal container with an initial value.\n(declare-fun signal (Value) Value)" "lisp"))
(p :class "text-sm text-stone-500" (code "z3-translate") " — the first self-hosted bootstrapper, translating spec declarations to verification conditions for theorem provers.")))
;; -----------------------------------------------------------------------
;; Testing and proving
;; -----------------------------------------------------------------------
(~doc-section :title "Tests and Proofs" :id "runtime"
(~docs/section :title "Tests and Proofs" :id "runtime"
(p "Ring 4 shows that the spec does what it claims.")
(~doc-subsection :title "Tests"
(~docs/subsection :title "Tests"
(p "Test files (" (code "test-signals.sx") ", " (code "test-eval.sx") ", etc.) use the " (code "defsuite") "/" (code "deftest") " framework. The explorer matches tests to functions by suite name and shows them on the function card.")
(~doc-code :code (highlight "(defsuite \"signal basics\"\n (deftest \"creates signal with value\"\n (let ((s (signal 42)))\n (assert-equal (deref s) 42)))\n (deftest \"reset changes value\"\n (let ((s (signal 0)))\n (reset! s 99)\n (assert-equal (deref s) 99))))" "sx")))
(~docs/code :code (highlight "(defsuite \"signal basics\"\n (deftest \"creates signal with value\"\n (let ((s (signal 42)))\n (assert-equal (deref s) 42)))\n (deftest \"reset changes value\"\n (let ((s (signal 0)))\n (reset! s 99)\n (assert-equal (deref s) 99))))" "sx")))
(~doc-subsection :title "Proofs"
(~docs/subsection :title "Proofs"
(p (code "prove.sx") " verifies algebraic properties of SX primitives by bounded model checking. For each " (code "define-primitive") " with a " (code ":body") ", " (code "prove-translate") " translates to SMT-LIB and verifies satisfiability by construction.")
(p "Properties from the " (code "sx-properties") " library are matched to functions and shown on their cards:")
(~doc-code :code (highlight ";; prove.sx property: +-commutative\n{:name \"+-commutative\"\n :vars (list \"a\" \"b\")\n :test (fn (a b) (= (+ a b) (+ b a)))\n :holds '(= (+ a b) (+ b a))}\n\n;; Result: verified — 1,681 ground instances tested" "sx"))))
(~docs/code :code (highlight ";; prove.sx property: +-commutative\n{:name \"+-commutative\"\n :vars (list \"a\" \"b\")\n :test (fn (a b) (= (+ a b) (+ b a)))\n :holds '(= (+ a b) (+ b a))}\n\n;; Result: verified — 1,681 ground instances tested" "sx"))))
;; -----------------------------------------------------------------------
;; Architecture
;; -----------------------------------------------------------------------
(~doc-section :title "Implementation" :id "implementation"
(~docs/section :title "Implementation" :id "implementation"
(p "Three layers, three increments.")
(~doc-subsection :title "Layer 1: Python helper"
(~docs/subsection :title "Layer 1: Python helper"
(p (code "spec-explorer-data(slug)") " in " (code "helpers.py") " — parses a " (code ".sx") " spec file via " (code "parse_all()") ", extracts sections/defines/effects/params, calls each bootstrapper for per-function translations, matches tests, runs proofs.")
(p "This is the only new Python code. Everything else is SX components."))
(~doc-subsection :title "Layer 2: SX components"
(~docs/subsection :title "Layer 2: SX components"
(p (code "specs-explorer.sx") " — 12-15 " (code "defcomp") " components rendering the structured data:")
(div :class "overflow-x-auto"
(pre :class "text-xs bg-stone-100 rounded p-3"
(code "~spec-explorer-content top-level, receives parsed data\n ~spec-explorer-header filename, title, source link\n ~spec-explorer-stats aggregate badges: effects, tests, proofs\n ~spec-explorer-toc section table of contents\n ~spec-explorer-section one section with its defines\n ~spec-explorer-define one function card (all five rings)\n ~spec-effect-badge colored effect badge\n ~spec-param-list typed parameter list\n ~spec-ring-translations SX / Python / JS / Z3 panels\n ~spec-ring-bridge cross-references + platform deps\n ~spec-ring-runtime tests + proofs\n ~spec-ring-examples usage examples\n ~spec-platform-interface platform primitives table"))))
(code "~specs-explorer/spec-explorer-content top-level, receives parsed data\n ~specs-explorer/spec-explorer-header filename, title, source link\n ~specs-explorer/spec-explorer-stats aggregate badges: effects, tests, proofs\n ~spec-explorer-toc section table of contents\n ~specs-explorer/spec-explorer-section one section with its defines\n ~specs-explorer/spec-explorer-define one function card (all five rings)\n ~specs-explorer/spec-effect-badge colored effect badge\n ~specs-explorer/spec-param-list typed parameter list\n ~specs-explorer/spec-ring-translations SX / Python / JS / Z3 panels\n ~specs-explorer/spec-ring-bridge cross-references + platform deps\n ~specs-explorer/spec-ring-runtime tests + proofs\n ~spec-ring-examples usage examples\n ~specs-explorer/spec-platform-interface platform primitives table"))))
(~doc-subsection :title "Layer 3: Routing"
(~docs/subsection :title "Layer 3: Routing"
(p "New route at " (code "/language/specs/explore/<slug>") " — parallel to existing raw source at " (code "/language/specs/<slug>") ". Each spec page gets an \"Explore\" link; the explorer gets a \"Source\" link.")))
;; -----------------------------------------------------------------------
;; Increments
;; -----------------------------------------------------------------------
(~doc-section :title "Incremental Delivery" :id "increments"
(~docs/section :title "Incremental Delivery" :id "increments"
(div :class "space-y-4"
(div :class "rounded border border-stone-200 p-4"
@@ -201,7 +201,7 @@
;; The strange loop
;; -----------------------------------------------------------------------
(~doc-section :title "The Strange Loop" :id "strange-loop"
(~docs/section :title "The Strange Loop" :id "strange-loop"
(p "When you view " (code "/language/specs/explore/eval") ", what happens is this:")
(ol :class "list-decimal pl-5 text-stone-700 space-y-2"
(li "The SX evaluator — bootstrapped from " (code "eval.sx") " — evaluates the page definition.")

View File

@@ -2,8 +2,8 @@
;; Plan Status Overview
;; ---------------------------------------------------------------------------
(defcomp ~plan-status-content ()
(~doc-page :title "Plan Status"
(defcomp ~plans/status/plan-status-content ()
(~docs/page :title "Plan Status"
(p :class "text-lg text-stone-600 mb-6"
"Audit of all plans across the SX language and Rose Ash platform. Last updated March 2026.")
@@ -12,7 +12,7 @@
;; Completed
;; -----------------------------------------------------------------------
(~doc-section :title "Completed" :id "completed"
(~docs/section :title "Completed" :id "completed"
(div :class "space-y-4"
@@ -74,7 +74,7 @@
;; In Progress / Partial
;; -----------------------------------------------------------------------
(~doc-section :title "In Progress" :id "in-progress"
(~docs/section :title "In Progress" :id "in-progress"
(div :class "space-y-4"
@@ -88,7 +88,7 @@
;; Not Started
;; -----------------------------------------------------------------------
(~doc-section :title "Not Started" :id "not-started"
(~docs/section :title "Not Started" :id "not-started"
(div :class "space-y-4"
@@ -124,7 +124,7 @@
(div :class "flex items-center gap-2 mb-1"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
(a :href "/sx/(geography.(isomorphism))" :class "font-semibold text-stone-800 underline" "Isomorphic Phase 6: Streaming & Suspense"))
(p :class "text-sm text-stone-600" "Server streams partially-evaluated SX as IO resolves. ~suspense component renders fallbacks, inline resolution scripts fill in content. Concurrent IO via asyncio, chunked transfer encoding.")
(p :class "text-sm text-stone-600" "Server streams partially-evaluated SX as IO resolves. ~shared:pages/suspense component renders fallbacks, inline resolution scripts fill in content. Concurrent IO via asyncio, chunked transfer encoding.")
(p :class "text-sm text-stone-500 mt-1" "Demo: " (a :href "/sx/(geography.(isomorphism.streaming))" "/sx/(geography.(isomorphism.streaming))")))
(div :class "rounded border border-green-300 bg-green-50 p-4"

View File

@@ -2,15 +2,15 @@
;; SX-Activity: Federated SX over ActivityPub
;; ---------------------------------------------------------------------------
(defcomp ~plan-sx-activity-content ()
(~doc-page :title "SX-Activity"
(defcomp ~plans/sx-activity/plan-sx-activity-content ()
(~docs/page :title "SX-Activity"
(~doc-section :title "Context" :id "context"
(~docs/section :title "Context" :id "context"
(p "The web is six incompatible formats duct-taped together: HTML for structure, CSS for style, JavaScript for behavior, JSON for data, server languages for backend logic, build tools for compilation. Moving anything between layers requires serialization, template languages, API contracts, and glue code. Federation (ActivityPub) adds a seventh — JSON-LD — which is inert data that every consumer must interpret from scratch and wrap in their own UI.")
(p "SX is already one evaluable format that does all six. A component definition is simultaneously structure, style (components apply classes and respond to data), behavior (event handlers), data (the AST " (em "is") " data), server-renderable (Python evaluator), and client-renderable (JS evaluator). The pieces already exist: content-addressed DAG execution (artdag), IPFS storage with CIDs, OpenTimestamps Bitcoin anchoring, boundary-enforced sandboxing.")
(p "SX-Activity wires these together into a new web. Everything — content, UI components, markdown parsers, syntax highlighters, validation logic, media, processing pipelines — is the same executable format, stored on a content-addressed network, running within each participant's own security context. " (strong "The wire format is the programming language is the component system is the package manager.")))
(~doc-section :title "Current State" :id "current-state"
(~docs/section :title "Current State" :id "current-state"
(ul :class "space-y-2 text-stone-700 list-disc pl-5"
(li (strong "ActivityPub: ") "Full implementation — virtual per-app actors, HTTP signatures, webfinger, inbox/outbox, followers/following, delivery with idempotent logging.")
(li (strong "Activity bus: ") "Unified event bus with NOTIFY/LISTEN wakeup, at-least-once delivery, handler registry keyed by (activity_type, object_type).")
@@ -23,28 +23,28 @@
;; Phase 1: SX Wire Format for Activities
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 1: SX Wire Format for Activities" :id "phase-1"
(~docs/section :title "Phase 1: SX Wire Format for Activities" :id "phase-1"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What it enables")
(p :class "text-violet-800" "Activities expressed as s-expressions instead of JSON-LD. Same semantics as ActivityStreams, but compact, parseable, and directly evaluable. Dual-format support for backward compatibility with existing AP servers."))
(~doc-subsection :title "The Problem"
(~docs/subsection :title "The Problem"
(p "JSON-LD activities are verbose and require context resolution:")
(~doc-code :code (highlight "{\"@context\": \"https://www.w3.org/ns/activitystreams\",\n \"type\": \"Create\",\n \"actor\": \"https://example.com/users/alice\",\n \"object\": {\n \"type\": \"Note\",\n \"content\": \"<p>Hello world</p>\",\n \"attributedTo\": \"https://example.com/users/alice\"\n }}" "json"))
(~docs/code :code (highlight "{\"@context\": \"https://www.w3.org/ns/activitystreams\",\n \"type\": \"Create\",\n \"actor\": \"https://example.com/users/alice\",\n \"object\": {\n \"type\": \"Note\",\n \"content\": \"<p>Hello world</p>\",\n \"attributedTo\": \"https://example.com/users/alice\"\n }}" "json"))
(p "Every consumer parses JSON, resolves @context, extracts fields, then builds their own UI around the raw data. The content is HTML embedded in a JSON string — two formats nested, neither evaluable."))
(~doc-subsection :title "SX Activity Format"
(~docs/subsection :title "SX Activity Format"
(p "The same activity as SX:")
(~doc-code :code (highlight "(Create\n :actor \"https://example.com/users/alice\"\n :published \"2026-03-06T12:00:00Z\"\n :object (Note\n :attributed-to \"https://example.com/users/alice\"\n :content (p \"Hello world\")\n :media-type \"text/sx\"))" "lisp"))
(~docs/code :code (highlight "(Create\n :actor \"https://example.com/users/alice\"\n :published \"2026-03-06T12:00:00Z\"\n :object (Note\n :attributed-to \"https://example.com/users/alice\"\n :content (p \"Hello world\")\n :media-type \"text/sx\"))" "lisp"))
(p "The content isn't a string containing markup — it " (em "is") " markup. The receiving server can evaluate it directly. The Note's content is a renderable SX expression."))
(~doc-subsection :title "Approach"
(~docs/subsection :title "Approach"
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Activity vocabulary in SX")
(p "Map ActivityStreams types to SX symbols. Activities are lists with a type head and keyword properties:")
(~doc-code :code (highlight ";; Core activity types\n(Create :actor ... :object ...)\n(Update :actor ... :object ...)\n(Delete :actor ... :object ...)\n(Follow :actor ... :object ...)\n(Like :actor ... :object ...)\n(Announce :actor ... :object ...)\n\n;; Object types\n(Note :content ... :attributed-to ...)\n(Article :name ... :content ... :summary ...)\n(Image :url ... :media-type ... :cid ...)\n(Collection :total-items ... :items ...)" "lisp")))
(~docs/code :code (highlight ";; Core activity types\n(Create :actor ... :object ...)\n(Update :actor ... :object ...)\n(Delete :actor ... :object ...)\n(Follow :actor ... :object ...)\n(Like :actor ... :object ...)\n(Announce :actor ... :object ...)\n\n;; Object types\n(Note :content ... :attributed-to ...)\n(Article :name ... :content ... :summary ...)\n(Image :url ... :media-type ... :cid ...)\n(Collection :total-items ... :items ...)" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "2. Content negotiation")
@@ -58,7 +58,7 @@
(h4 :class "font-semibold text-stone-700" "4. HTTP Signatures over SX")
(p "Same RSA signature mechanism. Digest header computed over the SX body. Existing keypair infrastructure unchanged."))))
(~doc-subsection :title "Verification"
(~docs/subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Round-trip: SX → JSON-LD → SX produces identical output")
(li "Legacy AP servers receive valid JSON-LD (Mastodon can display the post)")
@@ -69,28 +69,28 @@
;; Phase 2: Content-Addressed Components on IPFS
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 2: Content-Addressed Components on IPFS" :id "phase-2"
(~docs/section :title "Phase 2: Content-Addressed Components on IPFS" :id "phase-2"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What it enables")
(p :class "text-violet-800" "Component definitions stored on IPFS, referenced by CID. Any server can publish components. Any browser can fetch them. No central registry — content addressing IS the registry."))
(~doc-subsection :title "The Insight"
(~docs/subsection :title "The Insight"
(p "SX components are pure functions — they take data and return markup. They can't do IO (boundary enforcement guarantees this). That means they're " (strong "safe to load from any source") ". And if they're content-addressed, the CID " (em "is") " the identity — you don't need to trust the source, you just verify the hash.")
(p "Currently, component definitions travel with each page via " (code "<script type=\"text/sx\" data-components>") ". Each server bundles its own. With IPFS, components become shared infrastructure — define once, use everywhere."))
(~doc-subsection :title "Approach"
(~docs/subsection :title "Approach"
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Component CID computation")
(p "Each " (code "defcomp") " definition gets a content address:")
(~doc-code :code (highlight ";; Component source\n(defcomp ~card (&key title &rest children)\n (div :class \"border rounded p-4\"\n (h2 title) children))\n\n;; CID = SHA3-256 of canonical serialized form\n;; → bafy...abc123\n;; Stored: ipfs://bafy...abc123 → component source" "lisp"))
(~docs/code :code (highlight ";; Component source\n(defcomp ~plans/sx-activity/card (&key title &rest children)\n (div :class \"border rounded p-4\"\n (h2 title) children))\n\n;; CID = SHA3-256 of canonical serialized form\n;; → bafy...abc123\n;; Stored: ipfs://bafy...abc123 → component source" "lisp"))
(p "Canonical form: normalize whitespace, sort keyword args alphabetically, strip comments. Same component always produces same CID regardless of formatting."))
(div
(h4 :class "font-semibold text-stone-700" "2. Component references in activities")
(p "Activities declare which components they need by CID:")
(~doc-code :code (highlight "(Create\n :actor \"https://rose-ash.com/users/alice\"\n :requires (list\n \"bafy...card\" ;; ~card component\n \"bafy...avatar\") ;; ~avatar component\n :object (Note\n :content (~card :title \"Hello\"\n (~avatar :src \"ipfs://bafy...photo\")\n (p \"This renders with the card component.\"))))" "lisp"))
(~docs/code :code (highlight "(Create\n :actor \"https://rose-ash.com/users/alice\"\n :requires (list\n \"bafy...card\" ;; ~plans/sx-activity/card component\n \"bafy...avatar\") ;; ~shared:misc/avatar component\n :object (Note\n :content (~plans/sx-activity/card :title \"Hello\"\n (~shared:misc/avatar :src \"ipfs://bafy...photo\")\n (p \"This renders with the card component.\"))))" "lisp"))
(p "The receiving browser fetches missing components from IPFS, verifies CIDs, registers them, then renders the content."))
(div
@@ -106,9 +106,9 @@
(div
(h4 :class "font-semibold text-stone-700" "4. Component publication")
(p "Server-side: on component registration, compute CID and pin to IPFS. Track in " (code "IPFSPin") " model (already exists). Publish component availability via AP outbox:")
(~doc-code :code (highlight "(Create\n :actor \"https://rose-ash.com/apps/market\"\n :object (SxComponent\n :name \"~product-card\"\n :cid \"bafy...productcard\"\n :version \"1.0.0\"\n :deps (list \"bafy...card\" \"bafy...price-tag\")))" "lisp")))))
(~docs/code :code (highlight "(Create\n :actor \"https://rose-ash.com/apps/market\"\n :object (SxComponent\n :name \"~product-card\"\n :cid \"bafy...productcard\"\n :version \"1.0.0\"\n :deps (list \"bafy...card\" \"bafy...price-tag\")))" "lisp")))))
(~doc-subsection :title "Verification"
(~docs/subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Component pinned to IPFS → fetchable via gateway → CID verifies")
(li "Browser renders federated post using IPFS-fetched components")
@@ -119,32 +119,32 @@
;; Phase 3: Federated Media & Content Store
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 3: Federated Media & Content Store" :id "phase-3"
(~docs/section :title "Phase 3: Federated Media & Content Store" :id "phase-3"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What it enables")
(p :class "text-violet-800" "All media (images, video, audio, DAG outputs) stored content-addressed on IPFS. Activities reference media by CID. No hotlinking, no broken links, no dependence on the origin server staying online."))
(~doc-subsection :title "Current Mechanism"
(~docs/subsection :title "Current Mechanism"
(p "artdag already content-addresses all DAG outputs with SHA3-256 and tracks IPFS CIDs in " (code "IPFSPin") ". But media in the web platform (blog images, product photos, event banners) is stored as regular files on the origin server. Federated posts include " (code "url") " fields pointing to the origin — if the server goes down, the media is gone."))
(~doc-subsection :title "Approach"
(~docs/subsection :title "Approach"
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Media CID pipeline")
(p "On upload: hash content → pin to IPFS → store CID in database. Activities reference media by CID alongside URL fallback:")
(~doc-code :code (highlight "(Image\n :cid \"bafy...photo123\"\n :url \"https://rose-ash.com/media/photo.jpg\" ;; fallback\n :media-type \"image/jpeg\"\n :width 1200 :height 800)" "lisp")))
(~docs/code :code (highlight "(Image\n :cid \"bafy...photo123\"\n :url \"https://rose-ash.com/media/photo.jpg\" ;; fallback\n :media-type \"image/jpeg\"\n :width 1200 :height 800)" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "2. DAG output federation")
(p "artdag processing results (rendered video, processed images) already have CIDs. Federate them as activities:")
(~doc-code :code (highlight "(Create\n :actor \"https://rose-ash.com/users/alice\"\n :object (Artwork\n :name \"Sunset Remix\"\n :cid \"bafy...artwork\"\n :dag-cid \"bafy...dag\" ;; full DAG for reproduction\n :media-type \"video/mp4\"\n :sources (list\n (Image :cid \"bafy...src1\" :attribution \"...\")\n (Image :cid \"bafy...src2\" :attribution \"...\"))))" "lisp"))
(~docs/code :code (highlight "(Create\n :actor \"https://rose-ash.com/users/alice\"\n :object (Artwork\n :name \"Sunset Remix\"\n :cid \"bafy...artwork\"\n :dag-cid \"bafy...dag\" ;; full DAG for reproduction\n :media-type \"video/mp4\"\n :sources (list\n (Image :cid \"bafy...src1\" :attribution \"...\")\n (Image :cid \"bafy...src2\" :attribution \"...\"))))" "lisp"))
(p "The " (code ":dag-cid") " lets anyone re-execute the processing pipeline. The artwork is both a result and a reproducible recipe."))
(div
(h4 :class "font-semibold text-stone-700" "3. Shared SX content store")
(p "Not just components and media — full page content can be content-addressed. An Article's body is SX, pinned to IPFS:")
(~doc-code :code (highlight "(Article\n :name \"Why S-Expressions\"\n :content-cid \"bafy...article-body\" ;; SX source on IPFS\n :requires (list \"bafy...doc-page\" \"bafy...code-block\")\n :summary \"Why SX uses s-expressions instead of HTML.\")" "lisp"))
(~docs/code :code (highlight "(Article\n :name \"Why S-Expressions\"\n :content-cid \"bafy...article-body\" ;; SX source on IPFS\n :requires (list \"bafy...doc-page\" \"bafy...code-block\")\n :summary \"Why SX uses s-expressions instead of HTML.\")" "lisp"))
(p "The content outlives the server. Anyone with the CID can fetch, parse, and render the article with its original components."))
(div
@@ -156,7 +156,7 @@
(li "Large media uses IPFS streaming (chunked CIDs)")
(li "Integrates with Phase 6 of isomorphic plan (streaming/suspense)")))))
(~doc-subsection :title "Verification"
(~docs/subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Origin server offline → content still resolvable via IPFS gateway")
(li "DAG CID → re-executing DAG produces identical output")
@@ -166,23 +166,23 @@
;; Phase 4: Component Registry & Discovery
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 4: Component Registry & Discovery" :id "phase-4"
(~docs/section :title "Phase 4: Component Registry & Discovery" :id "phase-4"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What it enables")
(p :class "text-violet-800" "Federated component discovery. Servers publish component collections. Other servers follow component feeds. Like npm, but federated, content-addressed, and the packages are safe to run (pure functions, no IO)."))
(~doc-subsection :title "Approach"
(~docs/subsection :title "Approach"
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Component collections as AP actors")
(p "Each server exposes a component registry actor:")
(~doc-code :code (highlight "(Service\n :id \"https://rose-ash.com/sx-registry\"\n :type \"SxComponentRegistry\"\n :name \"Rose Ash Components\"\n :outbox \"https://rose-ash.com/sx-registry/outbox\"\n :followers \"https://rose-ash.com/sx-registry/followers\")" "lisp"))
(~docs/code :code (highlight "(Service\n :id \"https://rose-ash.com/sx-registry\"\n :type \"SxComponentRegistry\"\n :name \"Rose Ash Components\"\n :outbox \"https://rose-ash.com/sx-registry/outbox\"\n :followers \"https://rose-ash.com/sx-registry/followers\")" "lisp"))
(p "Follow the registry to receive component updates. The outbox is a chronological feed of Create/Update/Delete activities for components."))
(div
(h4 :class "font-semibold text-stone-700" "2. Component metadata")
(~doc-code :code (highlight "(SxComponent\n :name \"~data-table\"\n :cid \"bafy...datatable\"\n :version \"2.1.0\"\n :deps (list \"bafy...sortable\" \"bafy...paginator\")\n :params (list\n (dict :name \"rows\" :type \"list\" :required true)\n (dict :name \"columns\" :type \"list\" :required true)\n (dict :name \"sortable\" :type \"boolean\" :default false))\n :css-atoms (list :border :rounded :p-4 :text-sm)\n :preview-cid \"bafy...screenshot\"\n :license \"MIT\")" "lisp"))
(~docs/code :code (highlight "(SxComponent\n :name \"~data-table\"\n :cid \"bafy...datatable\"\n :version \"2.1.0\"\n :deps (list \"bafy...sortable\" \"bafy...paginator\")\n :params (list\n (dict :name \"rows\" :type \"list\" :required true)\n (dict :name \"columns\" :type \"list\" :required true)\n (dict :name \"sortable\" :type \"boolean\" :default false))\n :css-atoms (list :border :rounded :p-4 :text-sm)\n :preview-cid \"bafy...screenshot\"\n :license \"MIT\")" "lisp"))
(p "Dependencies are transitive CID references. CSS atoms declare which CSSX rules the component needs. Preview CID is a screenshot for registry browsing."))
(div
@@ -197,9 +197,9 @@
(div
(h4 :class "font-semibold text-stone-700" "4. Version resolution")
(p "Components are immutable (CID = identity). \"Updating\" a component publishes a new CID. Activities reference specific CIDs, so old content always renders correctly. The registry tracks version history:")
(~doc-code :code (highlight "(Update\n :actor \"https://rose-ash.com/sx-registry\"\n :object (SxComponent\n :name \"~card\"\n :cid \"bafy...card-v2\" ;; new version\n :replaces \"bafy...card-v1\" ;; previous version\n :changelog \"Added subtitle slot\"))" "lisp")))))
(~docs/code :code (highlight "(Update\n :actor \"https://rose-ash.com/sx-registry\"\n :object (SxComponent\n :name \"~card\"\n :cid \"bafy...card-v2\" ;; new version\n :replaces \"bafy...card-v1\" ;; previous version\n :changelog \"Added subtitle slot\"))" "lisp")))))
(~doc-subsection :title "Verification"
(~docs/subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Follow registry → receive component Create activities → components available locally")
(li "Render post using component from foreign registry → works")
@@ -209,16 +209,16 @@
;; Phase 5: Bitcoin-Anchored Provenance
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 5: Bitcoin-Anchored Provenance" :id "phase-5"
(~docs/section :title "Phase 5: Bitcoin-Anchored Provenance" :id "phase-5"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What it enables")
(p :class "text-violet-800" "Cryptographic proof that content existed at a specific time, authored by a specific actor. Leverages the existing APAnchor/OpenTimestamps infrastructure. Unforgeable, independently verifiable, survives server shutdown."))
(~doc-subsection :title "Current Mechanism"
(~docs/subsection :title "Current Mechanism"
(p "The " (code "APAnchor") " model already batches activities into Merkle trees, stores the tree on IPFS, creates an OpenTimestamps proof, and records the Bitcoin txid. This runs but isn't surfaced to users or integrated with the full activity lifecycle."))
(~doc-subsection :title "Approach"
(~docs/subsection :title "Approach"
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Automatic anchoring pipeline")
@@ -232,7 +232,7 @@
(div
(h4 :class "font-semibold text-stone-700" "2. Provenance chain in activities")
(~doc-code :code (highlight "(Create\n :actor \"https://rose-ash.com/users/alice\"\n :object (Note :content (p \"Hello\") :cid \"bafy...note\")\n :provenance (Anchor\n :tree-cid \"bafy...merkle-tree\"\n :leaf-index 42\n :ots-cid \"bafy...ots-proof\"\n :btc-txid \"abc123...def\"\n :btc-block 890123\n :anchored-at \"2026-03-06T12:00:00Z\"))" "lisp"))
(~docs/code :code (highlight "(Create\n :actor \"https://rose-ash.com/users/alice\"\n :object (Note :content (p \"Hello\") :cid \"bafy...note\")\n :provenance (Anchor\n :tree-cid \"bafy...merkle-tree\"\n :leaf-index 42\n :ots-cid \"bafy...ots-proof\"\n :btc-txid \"abc123...def\"\n :btc-block 890123\n :anchored-at \"2026-03-06T12:00:00Z\"))" "lisp"))
(p "Any party can verify: fetch the OTS proof from IPFS, check the Merkle path from the activity's CID to the tree root, confirm the tree root is committed in the Bitcoin block."))
(div
@@ -246,9 +246,9 @@
(div
(h4 :class "font-semibold text-stone-700" "4. Verification UI")
(p "Client-side provenance badge on federated content:")
(~doc-code :code (highlight "(defcomp ~provenance-badge (&key anchor)\n (when anchor\n (details :class \"inline text-xs text-stone-400\"\n (summary \"✓ Anchored\")\n (dl :class \"mt-1 space-y-1\"\n (dt \"Bitcoin block\") (dd (get anchor \"btc-block\"))\n (dt \"Timestamp\") (dd (get anchor \"anchored-at\"))\n (dt \"Proof\") (dd (a :href (str \"ipfs://\" (get anchor \"ots-cid\"))\n \"OTS proof\"))))))" "lisp")))))
(~docs/code :code (highlight "(defcomp ~plans/sx-activity/provenance-badge (&key anchor)\n (when anchor\n (details :class \"inline text-xs text-stone-400\"\n (summary \"✓ Anchored\")\n (dl :class \"mt-1 space-y-1\"\n (dt \"Bitcoin block\") (dd (get anchor \"btc-block\"))\n (dt \"Timestamp\") (dd (get anchor \"anchored-at\"))\n (dt \"Proof\") (dd (a :href (str \"ipfs://\" (get anchor \"ots-cid\"))\n \"OTS proof\"))))))" "lisp")))))
(~doc-subsection :title "Verification"
(~docs/subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Activity anchored → OTS proof fetchable from IPFS → Merkle path validates → txid confirms in Bitcoin")
(li "Tampered activity → Merkle proof fails → provenance badge shows ✗")
@@ -258,18 +258,18 @@
;; Phase 6: The Evaluable Web
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 6: The Evaluable Web" :id "phase-6"
(~docs/section :title "Phase 6: The Evaluable Web" :id "phase-6"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What this really is")
(p :class "text-violet-800" "Not ActivityPub-with-SX. A new web. One where everything — content, components, parsers, renderers, server logic, client logic — is the same executable format, shared on a content-addressed network, running within each participant's own security context."))
(~doc-subsection :title "The insight"
(~docs/subsection :title "The insight"
(p "The web has six layers that don't talk to each other: HTML (structure), CSS (style), JavaScript (behavior), JSON (data interchange), server frameworks (backend logic), and build tools (compilation). Each has its own syntax, its own semantics, its own ecosystem. Moving data between them requires serialization, deserialization, template languages, API contracts, type coercion, and an endless parade of glue code.")
(p "SX collapses all six into one evaluable format. A component definition is simultaneously structure, style (components apply classes and respond to data), behavior (event handlers), data (the AST is data), server-renderable (Python evaluator), and client-renderable (JS evaluator). There is no boundary between \"data\" and \"program\" — s-expressions are both.")
(p "Once that's true, " (strong "everything becomes shareable.") " Not just UI components — markdown parsers, syntax highlighters, date formatters, validation logic, layout algorithms, color systems, animation curves. Any pure function over data. All content-addressed, all on IPFS, all executable within your own security context."))
(~doc-subsection :title "What travels on the network"
(~docs/subsection :title "What travels on the network"
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "Content")
@@ -282,7 +282,7 @@
(div
(h4 :class "font-semibold text-stone-700" "Parsers and transforms")
(p "A markdown parser is just an SX function: takes a string, returns an SX tree. Publish it to IPFS. Now anyone can use your markdown dialect. Same for: syntax highlighters, BBCode parsers, wiki markup, LaTeX subsets, CSV-to-table converters, JSON-to-SX adapters. " (strong "The parser ecosystem becomes shared infrastructure."))
(~doc-code :code (highlight ";; A markdown parser, published to IPFS\n;; CID: bafy...md-parser\n(define parse-markdown\n (fn (source)\n ;; tokenize → build AST → return SX tree\n ;; (parse-markdown \"# Hello\\n**bold**\")\n ;; → (h1 \"Hello\") (p (strong \"bold\"))\n ...))\n\n;; Anyone can use it in their components\n(defcomp ~blog-post (&key markdown-source)\n (div :class \"prose\"\n (parse-markdown markdown-source)))" "lisp")))
(~docs/code :code (highlight ";; A markdown parser, published to IPFS\n;; CID: bafy...md-parser\n(define parse-markdown\n (fn (source)\n ;; tokenize → build AST → return SX tree\n ;; (parse-markdown \"# Hello\\n**bold**\")\n ;; → (h1 \"Hello\") (p (strong \"bold\"))\n ...))\n\n;; Anyone can use it in their components\n(defcomp ~plans/sx-activity/blog-post (&key markdown-source)\n (div :class \"prose\"\n (parse-markdown markdown-source)))" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "Server-side and client-side logic")
@@ -292,7 +292,7 @@
(h4 :class "font-semibold text-stone-700" "Media and processing pipelines")
(p "Images, video, audio — all content-addressed on IPFS. But also the " (em "processing pipelines") " that created them. artdag DAGs are SX. Publish a DAG CID alongside the output CID and anyone can verify the provenance, re-render at different resolution, or fork the pipeline for their own work."))))
(~doc-subsection :title "The security model"
(~docs/subsection :title "The security model"
(p "This only works because of boundary enforcement. Every piece of SX fetched from the network runs within the receiver's security context:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "Pure functions can't do IO. ") "A component from IPFS can produce markup — it cannot read your cookies, make network requests, access localStorage, or call any IO primitive. The boundary spec (boundary.sx) is enforced at registration time. This isn't a policy — it's structural. The evaluator literally doesn't have IO primitives available when running untrusted code.")
@@ -302,7 +302,7 @@
(li (strong "Provenance proves authorship. ") "Bitcoin-anchored timestamps prove who published what and when. Not \"trust me\" — independently verifiable against the Bitcoin blockchain."))
(p "This is the opposite of the npm model. npm packages run with full access to your system — a malicious package can exfiltrate secrets, install backdoors, modify the filesystem. SX components are structurally sandboxed. The worst a malicious component can do is render a " (code "(div \"haha got you\")") "."))
(~doc-subsection :title "What this replaces"
(~docs/subsection :title "What this replaces"
(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"
@@ -347,10 +347,10 @@
(td :class "px-3 py-2 text-stone-700" "IPFS CID")
(td :class "px-3 py-2 text-stone-600" "Entire applications are content-addressed, no infrastructure needed"))))))
(~doc-subsection :title "Serverless applications on IPFS"
(~docs/subsection :title "Serverless applications on IPFS"
(p "The logical conclusion: " (strong "entire web applications hosted on IPFS with no server at all."))
(p "An SX application is a tree of content-addressed artifacts: a root page definition, component dependencies, media, stylesheets, parsers, transforms. Pin the root CID to IPFS and the application is live. No server, no DNS, no hosting provider, no deployment pipeline. Someone gives you a CID, you paste it into an SX-aware browser, and the application runs.")
(~doc-code :code (highlight ";; An entire blog — one CID\n;; ipfs://bafy...my-blog\n(defpage blog-home\n :path \"/\"\n :requires (list\n \"bafy...article-layout\" ;; layout component\n \"bafy...md-parser\" ;; markdown parser\n \"bafy...syntax-highlight\" ;; code highlighting\n \"bafy...nav-component\") ;; navigation\n :content\n (~article-layout\n :title \"My Blog\"\n :nav (~nav-component\n :items (list\n (dict :label \"Post 1\" :cid \"bafy...post-1\")\n (dict :label \"Post 2\" :cid \"bafy...post-2\")))\n :body (~markdown-page\n :source-cid \"bafy...homepage-md\")))" "lisp"))
(~docs/code :code (highlight ";; An entire blog — one CID\n;; ipfs://bafy...my-blog\n(defpage blog-home\n :path \"/\"\n :requires (list\n \"bafy...article-layout\" ;; layout component\n \"bafy...md-parser\" ;; markdown parser\n \"bafy...syntax-highlight\" ;; code highlighting\n \"bafy...nav-component\") ;; navigation\n :content\n (~article-layout\n :title \"My Blog\"\n :nav (~nav-component\n :items (list\n (dict :label \"Post 1\" :cid \"bafy...post-1\")\n (dict :label \"Post 2\" :cid \"bafy...post-2\")))\n :body (~markdown-page\n :source-cid \"bafy...homepage-md\")))" "lisp"))
(p "What this looks like in practice:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "Personal sites: ") "A portfolio or blog is a handful of SX files + media. Pin to IPFS. Share the CID. No hosting costs, no domain renewal, no SSL certificates. The site is permanent.")
@@ -361,7 +361,7 @@
(p "For applications that " (em "do") " need a server — user accounts, payments, real-time collaboration, database queries — the server provides IO primitives via the existing boundary system. The SX application fetches data from the server's IO endpoints, but the application itself (all the rendering, routing, component logic) lives on IPFS. The server is a " (em "data service") ", not an application host.")
(p "This inverts the current model. Today: server hosts the application, client is a thin renderer. SX web: IPFS hosts the application, server is an optional IO provider. " (strong "The application is the content. The content is the application. Both are just s-expressions.")))
(~doc-subsection :title "The end state"
(~docs/subsection :title "The end state"
(p "A browser with an SX evaluator and an IPFS gateway is a complete web platform. Given a CID — for a page, a post, an application — it can:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Fetch the content from IPFS")
@@ -378,30 +378,30 @@
;; Cross-Cutting Concerns
;; -----------------------------------------------------------------------
(~doc-section :title "Cross-Cutting Concerns" :id "cross-cutting"
(~docs/section :title "Cross-Cutting Concerns" :id "cross-cutting"
(~doc-subsection :title "Security"
(~docs/subsection :title "Security"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "Boundary enforcement is the foundation. ") "IPFS-fetched components are parsed and registered like any other component. SX_BOUNDARY_STRICT ensures they can't call IO primitives. A malicious component can produce ugly markup but can't exfiltrate data or make network requests.")
(li (strong "CID verification: ") "Content fetched from IPFS is hashed and compared to the expected CID before use. Tampered content is rejected.")
(li (strong "Signature chain: ") "Actor signatures (RSA/HTTP Signatures) prove authorship. Bitcoin anchors prove timing. Together they establish non-repudiable provenance.")
(li (strong "Resource limits: ") "Evaluation of untrusted components runs with step limits (max eval steps, max recursion depth). Infinite loops are caught and terminated.")))
(~doc-subsection :title "Backward Compatibility"
(~docs/subsection :title "Backward Compatibility"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Content negotiation ensures legacy AP servers always receive valid JSON-LD")
(li "SX-Activity is strictly opt-in — servers that don't understand it get standard AP")
(li "Existing internal activity bus unchanged — SX format is for federation, not internal events")
(li "URL fallbacks on all media references — CID is preferred, URL is fallback")))
(~doc-subsection :title "Performance"
(~docs/subsection :title "Performance"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Component CIDs cached in localStorage forever (content-addressed = immutable)")
(li "IPFS gateway responses cached with long TTL (content can't change)")
(li "Local IPFS node (if present) eliminates gateway latency")
(li "Provenance verification is lazy — badge shows unverified until user clicks to verify")))
(~doc-subsection :title "Integration with Isomorphic Architecture"
(~docs/subsection :title "Integration with Isomorphic Architecture"
(p "SX-Activity builds on the isomorphic architecture plan:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Phase 1 (component distribution) → IPFS replaces per-server bundles")
@@ -413,7 +413,7 @@
;; Critical Files
;; -----------------------------------------------------------------------
(~doc-section :title "Critical Files" :id "critical-files"
(~docs/section :title "Critical Files" :id "critical-files"
(div :class "overflow-x-auto rounded border border-stone-200"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-100"

View File

@@ -2,26 +2,26 @@
;; SX CI Pipeline
;; ---------------------------------------------------------------------------
(defcomp ~plan-sx-ci-content ()
(~doc-page :title "SX CI Pipeline"
(defcomp ~plans/sx-ci/plan-sx-ci-content ()
(~docs/page :title "SX CI Pipeline"
(p :class "text-stone-500 text-sm italic mb-8"
"Build, test, and deploy Rose Ash using the same language the application is written in.")
(~doc-section :title "Context" :id "context"
(~docs/section :title "Context" :id "context"
(p :class "text-stone-600"
"Rose Ash currently uses shell scripts for CI: " (code "deploy.sh") " auto-detects changed services via git diff, builds Docker images, pushes to the registry, and restarts Swarm services. " (code "dev.sh") " starts the dev environment and runs tests. These work, but they are opaque imperative scripts with no reuse, no composition, and no relationship to SX.")
(p :class "text-stone-600"
"The CI pipeline is the last piece of infrastructure not expressed in s-expressions. Fixing that completes the \"one representation for everything\" claim — the same language that defines the spec, the components, the pages, the essays, and the deployment config also defines the build pipeline."))
(~doc-section :title "Design" :id "design"
(~docs/section :title "Design" :id "design"
(p :class "text-stone-600"
"Pipeline definitions are " (code ".sx") " files. A minimal Python CLI runner evaluates them using " (code "sx_ref.py") ". CI-specific IO primitives (shell execution, Docker, git) are boundary-declared and only available to the pipeline runner — never to web components.")
(~doc-code :code (highlight ";; pipeline/deploy.sx\n(let ((targets (if (= (length ARGS) 0)\n (~detect-changed :base \"HEAD~1\")\n (filter (fn (svc) (some (fn (a) (= a (get svc \"name\"))) ARGS))\n services))))\n (when (= (length targets) 0)\n (log-step \"No changes detected\")\n (exit 0))\n\n (log-step (str \"Deploying: \" (join \" \" (map (fn (s) (get s \"name\")) targets))))\n\n ;; Tests first\n (~unit-tests)\n (~sx-spec-tests)\n\n ;; Build, push, restart\n (for-each (fn (svc) (~build-service :service svc)) targets)\n (for-each (fn (svc) (~restart-service :service svc)) targets)\n\n (log-step \"Deploy complete\"))" "lisp"))
(~docs/code :code (highlight ";; pipeline/deploy.sx\n(let ((targets (if (= (length ARGS) 0)\n (~plans/sx-ci/detect-changed :base \"HEAD~1\")\n (filter (fn (svc) (some (fn (a) (= a (get svc \"name\"))) ARGS))\n services))))\n (when (= (length targets) 0)\n (log-step \"No changes detected\")\n (exit 0))\n\n (log-step (str \"Deploying: \" (join \" \" (map (fn (s) (get s \"name\")) targets))))\n\n ;; Tests first\n (~unit-tests)\n (~sx-spec-tests)\n\n ;; Build, push, restart\n (for-each (fn (svc) (~plans/sx-ci/build-service :service svc)) targets)\n (for-each (fn (svc) (~restart-service :service svc)) targets)\n\n (log-step \"Deploy complete\"))" "lisp"))
(p :class "text-stone-600"
"Pipeline steps are components. " (code "~unit-tests") ", " (code "~build-service") ", " (code "~detect-changed") " are " (code "defcomp") " definitions that compose by nesting — the same mechanism used for page layouts, navigation, and every other piece of the system."))
"Pipeline steps are components. " (code "~unit-tests") ", " (code "~plans/sx-ci/build-service") ", " (code "~plans/sx-ci/detect-changed") " are " (code "defcomp") " definitions that compose by nesting — the same mechanism used for page layouts, navigation, and every other piece of the system."))
(~doc-section :title "CI Primitives" :id "primitives"
(~docs/section :title "CI Primitives" :id "primitives"
(p :class "text-stone-600"
"New IO primitives declared in " (code "boundary.sx") ", implemented only in the CI runner context:")
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
@@ -70,14 +70,14 @@
(p :class "text-stone-600"
"The boundary system ensures these primitives are " (em "only") " available in the CI context. Web components cannot call " (code "shell-run!") " — the evaluator will refuse to resolve the symbol, just as it refuses to resolve any other unregistered IO primitive. The sandbox is structural, not a convention."))
(~doc-section :title "Reusable Steps" :id "steps"
(~docs/section :title "Reusable Steps" :id "steps"
(p :class "text-stone-600"
"Pipeline steps are components — same " (code "defcomp") " as UI components, same " (code "&key") " params, same composition by nesting:")
(~doc-code :code (highlight "(defcomp ~detect-changed (&key base)\n (let ((files (git-diff-files (or base \"HEAD~1\") \"HEAD\")))\n (if (some (fn (f) (starts-with? f \"shared/\")) files)\n services\n (filter (fn (svc)\n (some (fn (f) (starts-with? f (str (get svc \"dir\") \"/\"))) files))\n services))))\n\n(defcomp ~build-service (&key service)\n (let ((name (get service \"name\"))\n (tag (str registry \"/\" name \":latest\")))\n (log-step (str \"Building \" name))\n (docker-build :file (str (get service \"dir\") \"/Dockerfile\") :tag tag :context \".\")\n (docker-push tag)))\n\n(defcomp ~bootstrap-check ()\n (log-step \"Checking bootstrapped files are up to date\")\n (shell-run! \"python shared/sx/ref/bootstrap_js.py\")\n (shell-run! \"python shared/sx/ref/bootstrap_py.py\")\n (let ((diff (shell-run \"git diff --name-only shared/static/scripts/sx-ref.js shared/sx/ref/sx_ref.py\")))\n (when (not (= (get diff \"stdout\") \"\"))\n (fail! \"Bootstrapped files are stale — rebootstrap and commit\"))))" "lisp"))
(~docs/code :code (highlight "(defcomp ~plans/sx-ci/detect-changed (&key base)\n (let ((files (git-diff-files (or base \"HEAD~1\") \"HEAD\")))\n (if (some (fn (f) (starts-with? f \"shared/\")) files)\n services\n (filter (fn (svc)\n (some (fn (f) (starts-with? f (str (get svc \"dir\") \"/\"))) files))\n services))))\n\n(defcomp ~plans/sx-ci/build-service (&key service)\n (let ((name (get service \"name\"))\n (tag (str registry \"/\" name \":latest\")))\n (log-step (str \"Building \" name))\n (docker-build :file (str (get service \"dir\") \"/Dockerfile\") :tag tag :context \".\")\n (docker-push tag)))\n\n(defcomp ~plans/sx-ci/bootstrap-check ()\n (log-step \"Checking bootstrapped files are up to date\")\n (shell-run! \"python shared/sx/ref/bootstrap_js.py\")\n (shell-run! \"python shared/sx/ref/bootstrap_py.py\")\n (let ((diff (shell-run \"git diff --name-only shared/static/scripts/sx-ref.js shared/sx/ref/sx_ref.py\")))\n (when (not (= (get diff \"stdout\") \"\"))\n (fail! \"Bootstrapped files are stale — rebootstrap and commit\"))))" "lisp"))
(p :class "text-stone-600"
"Compare this to GitHub Actions YAML, where \"reuse\" means composite actions with " (code "uses:") " references, input/output mappings, shell script blocks inside YAML strings, and a " (a :href "https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions" :class "text-violet-600 hover:underline" "100-page syntax reference") ". SX pipeline reuse is function composition. That is all it has ever been."))
(~doc-section :title "Pipelines" :id "pipelines"
(~docs/section :title "Pipelines" :id "pipelines"
(p :class "text-stone-600"
"Two primary pipelines, each a single " (code ".sx") " file:")
(div :class "space-y-4"
@@ -90,7 +90,7 @@
(p :class "text-sm text-stone-600" "Auto-detect changed services (or accept explicit args), run tests, build Docker images, push to registry, restart Swarm services.")
(p :class "text-sm font-mono text-violet-700 mt-1" "python -m shared.sx.ci pipeline/deploy.sx blog market"))))
(~doc-section :title "Why this matters" :id "why"
(~docs/section :title "Why this matters" :id "why"
(p :class "text-stone-600"
"CI pipelines are the strongest test case for \"one representation for everything.\" GitHub Actions, GitLab CI, CircleCI — all use YAML. YAML is not a programming language. So every CI system reinvents conditionals (" (code "if:") " expressions evaluated as strings), iteration (" (code "matrix:") " strategies), composition (" (code "uses:") " references with input/output schemas), and error handling (" (code "continue-on-error:") " booleans) — all in a data format that was never designed for any of it.")
(p :class "text-stone-600"
@@ -98,7 +98,7 @@
(p :class "text-stone-600"
"SX pipelines use real conditionals, real functions, real composition, and real error handling — because SX is a real language. The pipeline definition and the application code are the same thing. An AI that can generate SX components can generate SX pipelines. A developer who reads SX pages can read SX deploys. The representation is universal."))
(~doc-section :title "Files" :id "files"
(~docs/section :title "Files" :id "files"
(div :class "overflow-x-auto rounded border border-stone-200"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-100"

View File

@@ -2,10 +2,10 @@
;; Plan: a Gitea/Forgejo-style git hosting platform where everything —
;; repositories, issues, pull requests, CI, permissions — is SX.
(defcomp ~plan-sx-forge-content ()
(~doc-page :title "sx-forge: Git Forge in SX"
(defcomp ~plans/sx-forge/plan-sx-forge-content ()
(~docs/page :title "sx-forge: Git Forge in SX"
(~doc-section :title "Vision" :id "vision"
(~docs/section :title "Vision" :id "vision"
(p "A git forge where the entire interface, configuration, and automation layer "
"is written in SX. Repositories are browsed, issues are filed, pull requests "
"are reviewed, and CI pipelines are triggered — all through SX components "
@@ -14,7 +14,7 @@
"that expand to permission checks. Repository templates are defcomps. "
"The forge doesn't use SX — it " (em "is") " SX."))
(~doc-section :title "Why" :id "why"
(~docs/section :title "Why" :id "why"
(p "Gitea/Forgejo are excellent but they're Go binaries with YAML/INI config, "
"Markdown rendering, and a template engine that's separate from the application logic. "
"Every layer speaks a different language.")
@@ -28,7 +28,7 @@
(li "Access control = SX macros that expand to permission predicates")
(li "API = SX wire format (text/sx) alongside JSON for compatibility")))
(~doc-section :title "Architecture" :id "architecture"
(~docs/section :title "Architecture" :id "architecture"
(div :class "overflow-x-auto mt-4"
(table :class "w-full text-sm text-left"
(thead
@@ -66,7 +66,7 @@
(td :class "py-2 px-3" "sx-activity (ActivityPub)")
(td :class "py-2 px-3" "Cross-instance PRs, issues, stars, forks."))))))
(~doc-section :title "Configuration as SX" :id "config"
(~docs/section :title "Configuration as SX" :id "config"
(p "Instance configuration is an SX file, not YAML or INI:")
(highlight "(define forge-config
{:name \"Rose Ash Forge\"
@@ -100,10 +100,10 @@
:merge-pr (and (ci-passed?) (approved-by? 1))
:admin (role? :admin)}})" "lisp"))
(~doc-section :title "SX Diff Viewer" :id "diff-viewer"
(~docs/section :title "SX Diff Viewer" :id "diff-viewer"
(p "Diffs rendered as SX components, not pre-formatted text:")
(highlight ";; The diff viewer is a defcomp, composable like any other
(defcomp ~diff-view (&key (diff :as dict))
(defcomp ~plans/sx-forge/diff-view (&key (diff :as dict))
(map (fn (hunk)
(~diff-hunk
:file (get hunk \"file\")
@@ -118,7 +118,7 @@
(li "Suggestion blocks — click to apply a proposed change")
(li "SX-aware diffs — show component-level changes, not just line changes")))
(~doc-section :title "Federated Forge" :id "federation"
(~docs/section :title "Federated Forge" :id "federation"
(p "sx-activity enables cross-instance collaboration:")
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
(li (strong "Cross-instance PRs") " — open a PR from your fork on another instance")
@@ -129,7 +129,7 @@
(p "Every issue, comment, and review is a content-addressed SX document on IPFS. "
"Federation distributes references. The content is permanent and verifiable."))
(~doc-section :title "Git Operations as IO Primitives" :id "git-ops"
(~docs/section :title "Git Operations as IO Primitives" :id "git-ops"
(p "Git operations exposed as SX IO primitives in boundary.sx:")
(highlight ";; boundary.sx additions
(io git-log (repo &key branch limit offset) list)
@@ -142,7 +142,7 @@
(io git-merge (repo source target &key strategy) dict)" "lisp")
(p "Pages use these directly — no controller layer, no ORM:"))
(~doc-section :title "Implementation Path" :id "implementation"
(~docs/section :title "Implementation Path" :id "implementation"
(ol :class "space-y-3 text-stone-600 list-decimal pl-5"
(li (strong "Phase 1: Read-only browser") " — git-tree, git-blob, git-log, git-diff as IO primitives. "
"SX components for tree view, blob view, commit log, diff view.")

View File

@@ -1,16 +1,16 @@
;; SX Protocol — A Proposal
;; S-expressions as a universal protocol for networked hypermedia.
(defcomp ~plan-sx-protocol-content ()
(~doc-page :title "SX Protocol — A Proposal"
(defcomp ~plans/sx-protocol/plan-sx-protocol-content ()
(~docs/page :title "SX Protocol — A Proposal"
(~doc-section :title "Abstract" :id "abstract"
(~docs/section :title "Abstract" :id "abstract"
(p "SX is a Lisp dialect and a proposed universal protocol for networked hypermedia. "
"It replaces URLs, HTTP verbs, query strings, API query languages, and rendering layers "
"with a single unified concept: " (strong "the s-expression") ".")
(p "Everything is an expression. Everything is evaluable. Everything is composable."))
(~doc-section :title "The Problem With the Current Web" :id "problem"
(~docs/section :title "The Problem With the Current Web" :id "problem"
(p "The modern web stack has accumulated layers of incompatible syntax to express "
"what are fundamentally the same things:")
(table :class "w-full text-sm border-collapse mb-4"
@@ -47,13 +47,13 @@
(p "Each layer invented its own syntax. None of them compose. None of them are executable. "
"None of them are data."))
(~doc-section :title "The SX Approach" :id "approach"
(~docs/section :title "The SX Approach" :id "approach"
(h3 :class "text-lg font-semibold mt-6 mb-2" "URLs as S-Expressions")
(p "A conventional URL:")
(~doc-code :code (highlight "https://site.com/blog/my-post?filter=published&sort=date" "text"))
(~docs/code :code (highlight "https://site.com/blog/my-post?filter=published&sort=date" "text"))
(p "As an SX expression:")
(~doc-code :code (highlight "(get.site.com.(blog.(my-post.(filter.published.sort.date))))" "lisp"))
(~docs/code :code (highlight "(get.site.com.(blog.(my-post.(filter.published.sort.date))))" "lisp"))
(ul
(li "The protocol/verb is the first atom: " (code "get"))
(li "The domain follows: " (code "site.com"))
@@ -64,15 +64,15 @@
(p "Lisp conventionally uses spaces as separators. In URLs, spaces become " (code "%20")
". SX uses dots instead, which are URL-safe and semantically meaningful — a dot between "
"two atoms is a " (strong "cons pair") ", the fundamental unit of Lisp structure.")
(~doc-code :code (highlight ";; Clean, URL-safe, valid Lisp\n(blog.(filter.published).(sort.date.desc))" "lisp"))
(~docs/code :code (highlight ";; Clean, URL-safe, valid Lisp\n(blog.(filter.published).(sort.date.desc))" "lisp"))
(h3 :class "text-lg font-semibold mt-6 mb-2" "Verbs Are Just Atoms")
(p "HTTP methods are not special syntax — they are simply the first element of the expression:")
(~doc-code :code (highlight "(get.site.com.(post.my-first-post)) ; read\n(post.site.com.(submit-post.(title.hello))) ; write\n(ws.site.com.(live-feed)) ; websocket / subscribe" "lisp"))
(~docs/code :code (highlight "(get.site.com.(post.my-first-post)) ; read\n(post.site.com.(submit-post.(title.hello))) ; write\n(ws.site.com.(live-feed)) ; websocket / subscribe" "lisp"))
(p "No special protocol prefixes. No " (code "https://") " vs " (code "wss://")
". The verb is data, like everything else."))
(~doc-section :title "Graph-SX: Hypermedia Queries" :id "graph-sx"
(~docs/section :title "Graph-SX: Hypermedia Queries" :id "graph-sx"
(p "GraphQL was a major advance over REST, but it made two compromises:")
(ol
(li "Queries are sent as POST bodies, sacrificing cacheability and shareability")
@@ -81,7 +81,7 @@
(h3 :class "text-lg font-semibold mt-6 mb-2" "Queries Are URLs")
(p "Because SX expressions are URLs, every query is a GET request:")
(~doc-code :code (highlight ";; This is a URL and a query simultaneously\n(get.site.com.(blog.(filter.(tag.lisp)).(limit.10)))" "lisp"))
(~docs/code :code (highlight ";; This is a URL and a query simultaneously\n(get.site.com.(blog.(filter.(tag.lisp)).(limit.10)))" "lisp"))
(ul
(li "Fully cacheable by CDNs")
(li "Bookmarkable and shareable")
@@ -91,19 +91,19 @@
(h3 :class "text-lg font-semibold mt-6 mb-2" "Responses Include Rendering")
(p "GraphQL returns data. Graph-SX returns " (strong "hypermedia")
" — data and its presentation in the same expression:")
(~doc-code :code (highlight ";; GraphQL response (dead data)\n{\"title\": \"My Post\", \"body\": \"Hello world\"}\n\n;; Graph-SX response (live hypermedia)\n(article\n (h1 \"My Post\")\n (p \"Hello world\")\n (a (href (get.site.com.(post.next-post))) \"Next\"))" "lisp"))
(~docs/code :code (highlight ";; GraphQL response (dead data)\n{\"title\": \"My Post\", \"body\": \"Hello world\"}\n\n;; Graph-SX response (live hypermedia)\n(article\n (h1 \"My Post\")\n (p \"Hello world\")\n (a (href (get.site.com.(post.next-post))) \"Next\"))" "lisp"))
(p "The server returns what the resource " (strong "is") " and how to "
(strong "present") " it in one unified structure. There is no separate rendering layer.")
(h3 :class "text-lg font-semibold mt-6 mb-2" "Queries Are Transformations")
(p "Because SX is a full programming language, the query and the transformation "
"are the same expression:")
(~doc-code :code (highlight ";; Fetch, filter, and transform in one expression\n(map (lambda (p) (title p))\n (filter published?\n (posts (after \"2025\"))))" "lisp"))
(~docs/code :code (highlight ";; Fetch, filter, and transform in one expression\n(map (lambda (p) (title p))\n (filter published?\n (posts (after \"2025\"))))" "lisp"))
(p "No separate processing step. No client-side data manipulation layer."))
(~doc-section :title "Components" :id "components"
(~docs/section :title "Components" :id "components"
(p "SX supports server-side composable components via the " (code "~") " prefix convention:")
(~doc-code :code (highlight "(~get.everything-under-the-sun)" "lisp"))
(~docs/code :code (highlight "(~get.everything-under-the-sun)" "lisp"))
(p "A " (code "~component") " is a named server-side function that:")
(ol
(li "Receives the expression as arguments")
@@ -111,22 +111,22 @@
(li "Processes and composes results")
(li "Returns hypermedia"))
(p "Components compose naturally:")
(~doc-code :code (highlight "(~page.home\n (~hero.banner)\n (~get.latest-posts.(limit.5))\n (~get.featured.(filter.pinned)))" "lisp"))
(~docs/code :code (highlight "(~page.home\n (~hero.banner)\n (~get.latest-posts.(limit.5))\n (~get.featured.(filter.pinned)))" "lisp"))
(p "This is equivalent to React Server Components — but without a framework, "
"without a build step, and without leaving Lisp."))
(~doc-section :title "Cross-Domain Composition" :id "cross-domain"
(~docs/section :title "Cross-Domain Composition" :id "cross-domain"
(p "Because domain and verb are just atoms, cross-domain queries are structurally "
"identical to local ones:")
(~doc-code :code (highlight ";; Local\n(post.my-first-post)\n\n;; Remote — identical structure, qualified\n(get.site.com.(post.my-first-post))\n\n;; Composed across domains\n(~render\n (get.site.com.(post.my-first-post))\n (get.cdn.com.(image.hero)))" "lisp"))
(~docs/code :code (highlight ";; Local\n(post.my-first-post)\n\n;; Remote — identical structure, qualified\n(get.site.com.(post.my-first-post))\n\n;; Composed across domains\n(~render\n (get.site.com.(post.my-first-post))\n (get.cdn.com.(image.hero)))" "lisp"))
(p "Network calls are function calls. Remote resources are just namespaced expressions."))
(~doc-section :title "Self-Describing and Introspectable" :id "introspectable"
(~docs/section :title "Self-Describing and Introspectable" :id "introspectable"
(p "Because the site is implemented in SX and served as SX, every page is introspectable:")
(~doc-code :code (highlight "(get.sx.dev.(about)) ; the about page\n(get.sx.dev.(source.(about))) ; the SX source for the about page\n(get.sx.dev.(eval.(source.about))) ; re-evaluate it live" "lisp"))
(~docs/code :code (highlight "(get.sx.dev.(about)) ; the about page\n(get.sx.dev.(source.(about))) ; the SX source for the about page\n(get.sx.dev.(eval.(source.about))) ; re-evaluate it live" "lisp"))
(p "The site is its own documentation. The source is always one expression away."))
(~doc-section :title "Comparison" :id "comparison"
(~docs/section :title "Comparison" :id "comparison"
(table :class "w-full text-sm border-collapse mb-4"
(thead
(tr :class "border-b border-stone-300"
@@ -181,17 +181,17 @@
(td :class "py-2 pr-4 text-stone-500" "Partial")
(td :class "py-2 text-green-700" "Yes")))))
(~doc-section :title "Future Direction" :id "future"
(~docs/section :title "Future Direction" :id "future"
(p "The logical conclusion of SX is a " (strong "new internet protocol")
" in which the URL, the HTTP verb, the query language, the response format, "
"and the rendering layer are all unified under one evaluable expression format.")
(~doc-code :code (highlight ";; The entire network request — protocol, domain, verb, query, all one expression\n(get.sx.dev.(blog.(filter.(tag.lisp)).(limit.10)))" "lisp"))
(~docs/code :code (highlight ";; The entire network request — protocol, domain, verb, query, all one expression\n(get.sx.dev.(blog.(filter.(tag.lisp)).(limit.10)))" "lisp"))
(p "HTTP becomes one possible implementation of a more general principle:")
(blockquote :class "border-l-4 border-violet-300 pl-4 italic text-stone-600 my-4"
(p (strong "Evaluate this expression. Return an expression."))))
(~doc-section :title "Reference Implementation" :id "reference"
(~docs/section :title "Reference Implementation" :id "reference"
(p "SX is implemented in SX. The reference implementation is self-hosting and available at:")
(~doc-code :code (highlight "(get.sx.dev.(source.evaluator))" "lisp"))
(~docs/code :code (highlight "(get.sx.dev.(source.evaluator))" "lisp"))
(p :class "text-sm text-stone-500 mt-4 italic"
"This proposal was written in conversation with Claude (Anthropic). The ideas are the author's own."))))

View File

@@ -2,10 +2,10 @@
;; Plan: a Caddy-style reverse proxy where routes, TLS, middleware,
;; and load balancing are SX s-expressions manipulated by macros.
(defcomp ~plan-sx-proxy-content ()
(~doc-page :title "sx-proxy: Reverse Proxy in SX"
(defcomp ~plans/sx-proxy/plan-sx-proxy-content ()
(~docs/page :title "sx-proxy: Reverse Proxy in SX"
(~doc-section :title "Vision" :id "vision"
(~docs/section :title "Vision" :id "vision"
(p "A reverse proxy where routing rules, TLS configuration, middleware chains, "
"and load balancing policies are SX. Not a config file that gets parsed — "
"actual SX that gets evaluated. Macros generate routes from service definitions. "
@@ -14,7 +14,7 @@
"No functions, no macros, no computed values, no integration with the rest of the stack. "
"sx-proxy goes all the way: the proxy configuration " (em "is") " the application."))
(~doc-section :title "Why" :id "why"
(~docs/section :title "Why" :id "why"
(p "Every proxy config language reinvents the same features badly:")
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
(li "Nginx: custom DSL with if-is-evil, no real conditionals, include for composition")
@@ -25,7 +25,7 @@
"SX unifies them. The proxy reads the same service definitions "
"that the orchestrator deploys."))
(~doc-section :title "Route Definitions" :id "routes"
(~docs/section :title "Route Definitions" :id "routes"
(p "Routes as SX, with macros for common patterns:")
(highlight ";; Basic route definition
(route blog.rose-ash.com
@@ -48,7 +48,7 @@
(p "One macro generates all routes from the same service definitions "
"that sx-swarm uses for deployment. Change the service, the route updates."))
(~doc-section :title "Middleware as Composition" :id "middleware"
(~docs/section :title "Middleware as Composition" :id "middleware"
(p "Middleware chains are function composition:")
(highlight ";; Middleware are functions: request -> response -> response
(define rate-limit
@@ -82,7 +82,7 @@
"Apply different chains to different routes. "
"No nginx location blocks, no Caddy handle nesting."))
(~doc-section :title "TLS Configuration" :id "tls"
(~docs/section :title "TLS Configuration" :id "tls"
(p "TLS as SX with macro-generated cert management:")
(highlight ";; Auto TLS via ACME (like Caddy)
(define tls-auto
@@ -103,7 +103,7 @@
(defmacro with-tls (config &rest routes)
`(map (fn (r) (assoc r :tls ,config)) (list ,@routes)))" "lisp"))
(~doc-section :title "Load Balancing" :id "load-balancing"
(~docs/section :title "Load Balancing" :id "load-balancing"
(p "Load balancing policies as SX values:")
(highlight ";; Load balancing strategies
(define round-robin (lb :strategy :round-robin))
@@ -124,7 +124,7 @@
:upstream blog-pool
:tls :auto)" "lisp"))
(~doc-section :title "Dynamic Reconfiguration" :id "dynamic"
(~docs/section :title "Dynamic Reconfiguration" :id "dynamic"
(p "The proxy evaluates SX — so config can be dynamic:")
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
(li (strong "Hot reload") " — change the SX, proxy re-evaluates. No restart.")
@@ -149,7 +149,7 @@
\"blog-prod:8000\")
:tls :auto)" "lisp"))
(~doc-section :title "Integration with sx-swarm" :id "integration"
(~docs/section :title "Integration with sx-swarm" :id "integration"
(p "sx-proxy and sx-swarm share the same service definitions:")
(highlight ";; One source of truth
(defservice blog
@@ -169,7 +169,7 @@
"The proxy, the orchestrator, the CI, and the forge all consume "
"the same SX service definitions. No YAML-to-Caddyfile-to-Dockerfile translation."))
(~doc-section :title "Implementation Path" :id "implementation"
(~docs/section :title "Implementation Path" :id "implementation"
(ol :class "space-y-3 text-stone-600 list-decimal pl-5"
(li (strong "Phase 1: Config compiler") " — SX route definitions compiled to Caddy JSON API. "
"Use Caddy as the runtime, SX as the config language.")

View File

@@ -2,10 +2,10 @@
;; Plan: Docker Swarm management where stack definitions, service configs,
;; and deployment logic are SX s-expressions manipulated by macros.
(defcomp ~plan-sx-swarm-content ()
(~doc-page :title "sx-swarm: Container Orchestration in SX"
(defcomp ~plans/sx-swarm/plan-sx-swarm-content ()
(~docs/page :title "sx-swarm: Container Orchestration in SX"
(~doc-section :title "Vision" :id "vision"
(~docs/section :title "Vision" :id "vision"
(p "Replace docker-compose.yml and Docker Swarm stack files with SX. "
"Service definitions are defcomps. Environment configs are macros that expand "
"differently per target (dev, staging, production). Deployments are SX pipelines "
@@ -15,7 +15,7 @@
"SX has all of these. A service definition is a value. A deployment is a function. "
"An environment override is a macro."))
(~doc-section :title "Why Not YAML" :id "why-not-yaml"
(~docs/section :title "Why Not YAML" :id "why-not-yaml"
(p "Docker Compose and Swarm stack files share a fundamental problem: "
"they're static data with ad-hoc templating bolted on. "
"Variable substitution (${VAR}), extension fields (x-), YAML anchors (&/*) — "
@@ -29,7 +29,7 @@
(li "No verification — can't check constraints before deploy"))
(p "SX solves all of these because it's a programming language, not a data format."))
(~doc-section :title "Service Definitions" :id "services"
(~docs/section :title "Service Definitions" :id "services"
(p "Services defined as SX, with macros for common patterns:")
(highlight ";; Base service macro — shared defaults
(defmacro defservice (name &rest body)
@@ -53,7 +53,7 @@
"Every service gets restart policy, logging, and network config for free. "
"Override any field by specifying it in the body."))
(~doc-section :title "Environment Macros" :id "environments"
(~docs/section :title "Environment Macros" :id "environments"
(p "Environment-specific config via macros, not file merging:")
(highlight ";; Environment macro — expands differently per target
(defmacro env-for (service)
@@ -81,7 +81,7 @@
(p "No more docker-compose.yml + docker-compose.dev.yml + docker-compose.prod.yml. "
"One definition, macros handle the rest."))
(~doc-section :title "Stack Composition" :id "composition"
(~docs/section :title "Stack Composition" :id "composition"
(p "Stacks compose like functions:")
(highlight ";; Infrastructure services shared across all stacks
(define infra-services
@@ -108,7 +108,7 @@
(volume :name \"pg-data\" :driver :local)
(volume :name \"redis-data\" :driver :local))))" "lisp"))
(~doc-section :title "Deploy as SX Pipeline" :id "deploy"
(~docs/section :title "Deploy as SX Pipeline" :id "deploy"
(p "Deployment is an sx-ci pipeline, not a shell script:")
(highlight "(define deploy-pipeline
(pipeline :name \"deploy\"
@@ -128,7 +128,7 @@
:rolling true
:health-check-interval \"5s\"))))" "lisp"))
(~doc-section :title "Swarm Operations as IO" :id "swarm-ops"
(~docs/section :title "Swarm Operations as IO" :id "swarm-ops"
(p "Swarm management exposed as SX IO primitives:")
(highlight ";; boundary.sx additions
(io swarm-deploy (stack &key target rolling) dict)
@@ -144,7 +144,7 @@
(p "These compose with sx-ci and the forge — push to forge triggers CI, "
"CI runs tests, tests pass, deploy pipeline executes, swarm updates."))
(~doc-section :title "Health and Monitoring" :id "monitoring"
(~docs/section :title "Health and Monitoring" :id "monitoring"
(p "Service health as live SX components:")
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
(li (strong "Dashboard") " — defcomp rendering swarm-status, auto-refreshing via sx-get polling")
@@ -154,7 +154,7 @@
(p "The monitoring dashboard is an SX page like any other. "
"No Grafana, no separate monitoring stack. Same language, same renderer, same platform."))
(~doc-section :title "Implementation Path" :id "implementation"
(~docs/section :title "Implementation Path" :id "implementation"
(ol :class "space-y-3 text-stone-600 list-decimal pl-5"
(li (strong "Phase 1: Stack definition") " — SX data structures for services, networks, volumes. "
"Compiler to docker-compose.yml / docker stack deploy format.")

View File

@@ -2,10 +2,10 @@
;; Plan: replace path-based routing with s-expression URLs where the URL
;; IS the query, the render instruction, and the address — all at once.
(defcomp ~plan-sx-urls-content ()
(~doc-page :title "SX Expression URLs"
(defcomp ~plans/sx-urls/plan-sx-urls-content ()
(~docs/page :title "SX Expression URLs"
(~doc-section :title "Vision" :id "vision"
(~docs/section :title "Vision" :id "vision"
(p "URLs become s-expressions. The entire routing layer collapses into eval. "
"Every page is a function, every URL is a function call, and the nav tree hierarchy "
"is encoded directly in the nesting of the expression.")
@@ -13,14 +13,14 @@
(code "/sx/(language.(doc.introduction))") ". Dots replace spaces as the URL-friendly "
"separator — they are unreserved in RFC 3986, never percent-encoded, and visually clean. "
"The parser treats dot as whitespace: " (code "s/./ /") " before parsing as SX.")
(~doc-code :code (highlight
";; Current → SX URLs (dots = spaces)\n/language/specs/signals → /(language.(spec.signals))\n/language/specs/explore/signals → /(language.(spec.(explore.signals)))\n/language/docs/introduction → /(language.(doc.introduction))\n/etc/plans/spec-explorer → /(etc.(plan.spec-explorer))\n\n;; Direct component access — any defcomp is addressable\n/(~essay-sx-sucks)\n/(~plan-sx-urls-content)\n/(~bundle-analyzer-content)"
(~docs/code :code (highlight
";; Current → SX URLs (dots = spaces)\n/language/specs/signals → /(language.(spec.signals))\n/language/specs/explore/signals → /(language.(spec.(explore.signals)))\n/language/docs/introduction → /(language.(doc.introduction))\n/etc/plans/spec-explorer → /(etc.(plan.spec-explorer))\n\n;; Direct component access — any defcomp is addressable\n/(~essays/sx-sucks/essay-sx-sucks)\n/(~plans/sx-urls/plan-sx-urls-content)\n/(~analyzer/bundle-analyzer-content)"
"lisp")))
(~doc-section :title "Scoping — The 30-Year Ambiguity, Fixed" :id "scoping"
(~docs/section :title "Scoping — The 30-Year Ambiguity, Fixed" :id "scoping"
(p "REST URLs have an inherent ambiguity: does a filter/parameter apply to "
"the last segment, or the whole path? Consider:")
(~doc-code :code (highlight
(~docs/code :code (highlight
";; REST — ambiguous:\n/users/123/posts?filter=published\n;; Is the filter scoped to posts? Or to the user? Or the whole query?\n;; Nobody knows. Conventions vary. Documentation required.\n\n;; SX URLs — explicit scoping via nesting:\n/(hello.(sailor.(filter.hhh))) ;; filter scoped to sailor\n/(hello.sailor.(filter.hhh)) ;; filter scoped to hello\n\n;; These mean different things, both expressible.\n;; Parens make scope visible. No ambiguity. No documentation needed."
"lisp"))
(p "This is not a minor syntactic preference. REST has never been able to express "
@@ -32,20 +32,20 @@
"What took REST 30 years of convention documents to approximate, "
"SX URLs express in the syntax itself."))
(~doc-section :title "Dots as URL-Safe Whitespace" :id "dots"
(~docs/section :title "Dots as URL-Safe Whitespace" :id "dots"
(p "Spaces in URLs are ugly — they become " (code "%20") " in copy-paste, curl, logs, and proxies. "
"Dots are unreserved in RFC 3986, never encoded, and read naturally as \"drill down.\"")
(p "The rule is simple: " (strong "dot = space, nothing more") ". "
"Parens carry all the structural meaning. Dots are syntactic sugar for URLs only:")
(~doc-code :code (highlight
(~docs/code :code (highlight
";; These are identical after dot→space transform:\n/(language.(doc.introduction)) → (language (doc introduction))\n/(geography.(hypermedia.(reference.attributes)))\n → (geography (hypermedia (reference attributes)))\n\n;; Parens are still required for nesting:\n/(language.doc.introduction) → (language doc introduction)\n;; = language(\"doc\", \"introduction\") — WRONG\n\n;; Correct nesting:\n/(language.(doc.introduction)) → (language (doc introduction))\n;; = language(doc(\"introduction\")) — RIGHT"
"lisp"))
(p "The server's URL handler does one thing before parsing: "
(code "url_expr = raw_path[1:].replace('.', ' ')") ". Then standard SX parsing takes over."))
(~doc-section :title "The Lisp Tax" :id "parens"
(~docs/section :title "The Lisp Tax" :id "parens"
(p "People will hate the parentheses. But consider what developers already accept:")
(~doc-code :code (highlight
(~docs/code :code (highlight
";; Developers happily write this every day:\nhttps://api.site.com/v2/users/123/posts?filter=published&sort=date&order=desc&limit=10&offset=20\n\n;; And they would complain about this?\nhttps://site.com/(users.(posts.123.(filter.published.sort.date.limit.10)))\n\n;; The second is shorter, structured, unambiguous, and composable."
"lisp"))
(p "The real question: who is reading these URLs?")
@@ -58,9 +58,9 @@
"Every URL on the site is a live example of SX in action. "
"Visiting a page is evaluating an expression."))
(~doc-section :title "The Site Is a REPL" :id "repl"
(~docs/section :title "The Site Is a REPL" :id "repl"
(p "The address bar becomes the input line of a REPL. The page is the output.")
(~doc-code :code (highlight
(~docs/code :code (highlight
"/sx/(about) ;; renders the about page\n/(source.(about)) ;; returns the SX source for the about page\n/(eval.(source.(about))) ;; re-evaluates it live\n\n;; The killer demo:\n/(eval.(map.double.(list.1.2.3))) ;; actually returns (2 4 6)\n\n;; The website IS a REPL. The address bar IS the input."
"lisp"))
(p "You do not need to explain what SX is. You show someone a URL and they "
@@ -68,11 +68,11 @@
"The whole site becomes a self-hosting proof of concept — "
"that is not just elegant, that is the pitch."))
(~doc-section :title "Components as Query Resolvers" :id "resolvers"
(~docs/section :title "Components as Query Resolvers" :id "resolvers"
(p "The " (code "~") " sigil means \"find and execute this component.\" "
"Components can make onward queries, process results, and return composed content — "
"like server-side includes but Lispy and composable.")
(~doc-code :code (highlight
(~docs/code :code (highlight
";; ~get is a component that fetches, processes, and returns\n/(~get.everything-under-the-sun)\n\n;; The flow:\n;; 1. Server finds ~get component in env\n;; 2. ~get makes onward queries\n;; 3. Processes and transforms results\n;; 4. Returns composed hypermedia\n\n;; Because it's all SX, you nest and compose:\n/(~page.home\n .(~hero.banner)\n .(~get.latest-posts.(limit.5))\n .(~get.featured.(filter.pinned)))\n\n;; Each ~component is independently:\n;; - cacheable (by its expression)\n;; - reusable (same component, different args)\n;; - testable (evaluate in isolation)"
"lisp"))
(p "This is what React Server Components are trying to do — server-side data resolution "
@@ -80,7 +80,7 @@
"a serialization protocol, and \"use server\" pragmas. "
"SX gets it from a sigil and an evaluator."))
(~doc-section :title "HTTP Semantics — REST Re-Aligned" :id "http"
(~docs/section :title "HTTP Semantics — REST Re-Aligned" :id "http"
(p "GraphQL uses POST for queries even though they are pure reads — "
"because queries can be long and the body feels more natural for structured data. "
"But this violates HTTP semantics: POST implies side effects, "
@@ -106,7 +106,7 @@
"This is what REST always wanted but GraphQL abandoned. "
"SX re-aligns with HTTP while being more powerful than both."))
(~doc-section :title "GraphSX — This Is a Query Language" :id "graphsx"
(~docs/section :title "GraphSX — This Is a Query Language" :id "graphsx"
(p "The SX URL scheme is not just a routing convention — it is the emergence of "
(strong "GraphSX") ": GraphQL but Lisp. The structural parallel is exact:")
(div :class "overflow-x-auto mt-4"
@@ -163,17 +163,17 @@
"GraphQL had to invent a special syntax for queries because JSON is data, not code. "
"S-expressions are both."))
(~doc-section :title "Direct Component URLs" :id "components"
(p "Any " (code "defcomp") " is directly addressable via its " (code "~name") ". "
"The URL evaluator sees " (code "~essay-sx-sucks") ", looks it up in the component env, "
"evaluates it, wraps in " (code "~sx-doc") ", and returns.")
(~doc-code :code (highlight
";; Page functions are convenience wrappers:\n/(etc.(essay.sx-sucks)) ;; dispatches via case statement\n\n;; But you can bypass them entirely:\n/(~essay-sx-sucks) ;; direct component — no routing needed\n\n;; Every defcomp is instantly URL-accessible:\n/(~plan-sx-urls-content) ;; this very page\n/(~bundle-analyzer-content) ;; tools\n/(~docs-evaluator-content) ;; docs"
(~docs/section :title "Direct Component URLs" :id "components"
(p "Any " (code "defcomp") " is directly addressable via its " (code "~plans/content-addressed-components/name") ". "
"The URL evaluator sees " (code "~essays/sx-sucks/essay-sx-sucks") ", looks it up in the component env, "
"evaluates it, wraps in " (code "~layouts/doc") ", and returns.")
(~docs/code :code (highlight
";; Page functions are convenience wrappers:\n/(etc.(essay.sx-sucks)) ;; dispatches via case statement\n\n;; But you can bypass them entirely:\n/(~essays/sx-sucks/essay-sx-sucks) ;; direct component — no routing needed\n\n;; Every defcomp is instantly URL-accessible:\n/(~plans/sx-urls/plan-sx-urls-content) ;; this very page\n/(~analyzer/bundle-analyzer-content) ;; tools\n/(~docs-content/docs-evaluator-content) ;; docs"
"lisp"))
(p "New components are instantly URL-accessible without routing wiring. "
"Debugging is trivial — render any component in isolation."))
(~doc-section :title "URL Special Forms" :id "special-forms"
(~docs/section :title "URL Special Forms" :id "special-forms"
(p "URL-level functions that transform how content is resolved or displayed:")
(div :class "overflow-x-auto mt-4"
(table :class "w-full text-sm text-left"
@@ -185,7 +185,7 @@
(tbody :class "text-stone-600"
(tr :class "border-b border-stone-100"
(td :class "py-2 px-3 font-mono text-violet-700" "source")
(td :class "py-2 px-3 font-mono text-sm" "/sx/(source.(~essay-sx-sucks))")
(td :class "py-2 px-3 font-mono text-sm" "/sx/(source.(~essays/sx-sucks/essay-sx-sucks))")
(td :class "py-2 px-3" "Show defcomp source instead of rendering"))
(tr :class "border-b border-stone-100"
(td :class "py-2 px-3 font-mono text-violet-700" "inspect")
@@ -202,7 +202,7 @@
(tr :class "border-b border-stone-100"
(td :class "py-2 px-3 font-mono text-violet-700" "raw")
(td :class "py-2 px-3 font-mono text-sm" "/sx/(raw.(~some-component))")
(td :class "py-2 px-3" "Skip ~sx-doc nav wrapping"))
(td :class "py-2 px-3" "Skip ~layouts/doc nav wrapping"))
(tr :class "border-b border-stone-100"
(td :class "py-2 px-3 font-mono text-violet-700" "eval")
(td :class "py-2 px-3 font-mono text-sm" "/sx/(eval.(map.double.(list.1.2.3)))")
@@ -212,31 +212,31 @@
(td :class "py-2 px-3 font-mono text-sm" "/sx/(json.(language.(doc.primitives)))")
(td :class "py-2 px-3" "Return data as JSON — pure query mode"))))))
(~doc-section :title "Evaluation Model" :id "eval"
(~docs/section :title "Evaluation Model" :id "eval"
(p "The URL path (after stripping " (code "/") " and replacing dots with spaces) "
"is parsed as SX and evaluated with a " (strong "soft eval") ": "
"known function names are called; unknown symbols self-evaluate to their name as a string; "
"components (" (code "~name") ") are looked up in the component env.")
(~doc-code :code (highlight
"/sx/(language.(doc.introduction))\n\n;; After dot→space: (language (doc introduction))\n;; 1. Eval `introduction` → not a known function → \"introduction\"\n;; 2. Eval (doc \"introduction\") → call doc(\"introduction\") → page content\n;; 3. Eval (language content) → call language(content) → passes through\n;; 4. Router wraps result in (~sx-doc :path \"(language (doc introduction))\" ...)\n\n/(~essay-sx-sucks)\n;; 1. Eval ~essay-sx-sucks → component lookup → evaluate → content\n;; 2. Router wraps in ~sx-doc"
"components (" (code "~plans/content-addressed-components/name") ") are looked up in the component env.")
(~docs/code :code (highlight
"/sx/(language.(doc.introduction))\n\n;; After dot→space: (language (doc introduction))\n;; 1. Eval `introduction` → not a known function → \"introduction\"\n;; 2. Eval (doc \"introduction\") → call doc(\"introduction\") → page content\n;; 3. Eval (language content) → call language(content) → passes through\n;; 4. Router wraps result in (~layouts/doc :path \"(language (doc introduction))\" ...)\n\n/(~essays/sx-sucks/essay-sx-sucks)\n;; 1. Eval ~essays/sx-sucks/essay-sx-sucks → component lookup → evaluate → content\n;; 2. Router wraps in ~layouts/doc"
"lisp"))
(~doc-subsection :title "Section Functions"
(~docs/subsection :title "Section Functions"
(p "Structural functions that encode hierarchy and pass through content:")
(~doc-code :code (highlight
(~docs/code :code (highlight
"(define language\n (fn (&rest args)\n (if (empty? args) (language-index-content) (first args))))\n\n(define geography\n (fn (&rest args)\n (if (empty? args) (geography-index-content) (first args))))\n\n;; Sub-sections also pass through\n(define hypermedia\n (fn (&rest args)\n (if (empty? args) (hypermedia-index-content) (first args))))"
"lisp")))
(~doc-subsection :title "Page Functions"
(~docs/subsection :title "Page Functions"
(p "Leaf functions that dispatch to content components. "
"Data-dependent pages call helpers directly — the async evaluator handles IO:")
(~doc-code :code (highlight
"(define doc\n (fn (&rest args)\n (let ((slug (first-or-nil args)))\n (if (nil? slug)\n (~docs-introduction-content)\n (case slug\n \"introduction\" (~docs-introduction-content)\n \"getting-started\" (~docs-getting-started-content)\n ...)))))\n\n(define bootstrapper\n (fn (&rest args)\n (let ((slug (first-or-nil args))\n (data (when slug (bootstrapper-data slug))))\n (if (nil? slug)\n (~bootstrappers-index-content)\n (if (get data \"bootstrapper-not-found\")\n (~spec-not-found :slug slug)\n (case slug\n \"python\" (~bootstrapper-py-content ...)\n ...))))))"
(~docs/code :code (highlight
"(define doc\n (fn (&rest args)\n (let ((slug (first-or-nil args)))\n (if (nil? slug)\n (~docs-content/docs-introduction-content)\n (case slug\n \"introduction\" (~docs-content/docs-introduction-content)\n \"getting-started\" (~docs-content/docs-getting-started-content)\n ...)))))\n\n(define bootstrapper\n (fn (&rest args)\n (let ((slug (first-or-nil args))\n (data (when slug (bootstrapper-data slug))))\n (if (nil? slug)\n (~specs/bootstrappers-index-content)\n (if (get data \"bootstrapper-not-found\")\n (~specs/not-found :slug slug)\n (case slug\n \"python\" (~specs/bootstrapper-py-content ...)\n ...))))))"
"lisp"))))
(~doc-section :title "The Catch-All Route" :id "route"
(~docs/section :title "The Catch-All Route" :id "route"
(p "The entire routing layer becomes one handler:")
(~doc-code :code (highlight
(~docs/code :code (highlight
"@app.get(\"/\")\nasync def sx_home():\n return await eval_sx_url(\"/\")\n\n@app.get(\"/<path:expr>\")\nasync def sx_eval_route(expr):\n return await eval_sx_url(f\"/{expr}\")"
"python"))
(p (code "eval_sx_url") " in seven steps:")
@@ -246,17 +246,17 @@
(li "Parse as SX expression")
(li "Auto-quote unknown symbols (slugs become strings)")
(li "Evaluate with components + helpers + page/section functions in env")
(li "Wrap result in " (code "~sx-doc") " with the URL expression as " (code ":path"))
(li "Wrap result in " (code "~layouts/doc") " with the URL expression as " (code ":path"))
(li "Return HTML or SX wire format depending on HTMX request"))
(p "Defhandler API endpoints and Python demo routes are registered " (em "before") " the catch-all, "
"so they match first."))
(~doc-section :title "Composability" :id "composability"
(~doc-code :code (highlight
";; Direct component access\n/(~essay-sx-sucks)\n/(~spec-explorer-content)\n\n;; URL special forms\n/(source.(~essay-sx-sucks)) ;; view defcomp source\n/(inspect.(language.(doc.primitives))) ;; deps, render plan\n/(diff.(language.(spec.signals)).(language.(spec.eval))) ;; side by side\n/(eval.(map.double.(list.1.2.3))) ;; REPL in the URL bar\n\n;; Components as query resolvers\n/(~page.home\n .(~hero.banner)\n .(~get.latest-posts.(limit.5))\n .(~get.featured.(filter.pinned)))\n\n;; Scoping is explicit\n/(users.(posts.123.(filter.published))) ;; filter scoped to posts\n/(users.posts.123.(filter.published)) ;; filter scoped to users\n\n;; Cross-service (future)\n/(market.(product.42.:fields.(name.price)))\n/(subscribe.(etc.(plan.status)))"
(~docs/section :title "Composability" :id "composability"
(~docs/code :code (highlight
";; Direct component access\n/(~essays/sx-sucks/essay-sx-sucks)\n/(~specs-explorer/spec-explorer-content)\n\n;; URL special forms\n/(source.(~essays/sx-sucks/essay-sx-sucks)) ;; view defcomp source\n/(inspect.(language.(doc.primitives))) ;; deps, render plan\n/(diff.(language.(spec.signals)).(language.(spec.eval))) ;; side by side\n/(eval.(map.double.(list.1.2.3))) ;; REPL in the URL bar\n\n;; Components as query resolvers\n/(~page.home\n .(~hero.banner)\n .(~get.latest-posts.(limit.5))\n .(~get.featured.(filter.pinned)))\n\n;; Scoping is explicit\n/(users.(posts.123.(filter.published))) ;; filter scoped to posts\n/(users.posts.123.(filter.published)) ;; filter scoped to users\n\n;; Cross-service (future)\n/(market.(product.42.:fields.(name.price)))\n/(subscribe.(etc.(plan.status)))"
"lisp")))
(~doc-section :title "Implementation Phases" :id "phases"
(~docs/section :title "Implementation Phases" :id "phases"
(div :class "space-y-4"
(div :class "rounded border border-violet-200 bg-violet-50 p-4"
(p :class "font-semibold text-violet-800 mb-2" "Phase 1: Page Functions + Catch-All Route")
@@ -292,7 +292,7 @@
(li "Delete " (code "docs.sx") " (all 46 defpages)")
(li "Grep content files for stale old-style hrefs")))))
(~doc-section :title "What Stays the Same" :id "unchanged"
(~docs/section :title "What Stays the Same" :id "unchanged"
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
(li (strong "Defhandler API paths") " — registered before the catch-all, match first")
(li (strong "Python demo routes") " — registered via blueprint before the catch-all")

View File

@@ -2,10 +2,10 @@
;; 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"
(defcomp ~plans/sx-web-platform/plan-sx-web-platform-content ()
(~docs/page :title "sx-web.org Development Platform"
(~doc-section :title "Vision" :id "vision"
(~docs/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. "
@@ -15,7 +15,7 @@
"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"
(~docs/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"
@@ -50,7 +50,7 @@
(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"
(~docs/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.")
@@ -64,7 +64,7 @@
(li "Stage changes as content-addressed preview")
(li "Publish via sx-activity when approved")))
(~doc-section :title "Workflow" :id "workflow"
(~docs/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. "
@@ -80,7 +80,7 @@
(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"
(~docs/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")
@@ -90,7 +90,7 @@
(li (strong "Components") " — reusable UI components shared via IPFS")
(li (strong "Tests") " — defsuite/deftest written and executed live")))
(~doc-section :title "Prerequisites" :id "prerequisites"
(~docs/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")

View File

@@ -3,7 +3,7 @@
;; ---------------------------------------------------------------------------
;; Helper: render a Phase 1 result row
(defcomp ~prove-phase1-row (&key (name :as string) (status :as string))
(defcomp ~plans/theorem-prover/prove-phase1-row (&key (name :as string) (status :as string))
(tr :class "border-t border-stone-100"
(td :class "py-1.5 px-3 font-mono text-xs text-stone-700" name)
(td :class "py-1.5 px-3 text-xs"
@@ -12,7 +12,7 @@
(span :class "text-red-600 font-medium" status)))))
;; Helper: render a Phase 2 result row
(defcomp ~prove-phase2-row (&key (name :as string) (status :as string) (tested :as number) (skipped :as number) (counterexample :as string?))
(defcomp ~plans/theorem-prover/prove-phase2-row (&key (name :as string) (status :as string) (tested :as number) (skipped :as number) (counterexample :as string?))
(tr :class "border-t border-stone-100"
(td :class "py-1.5 px-3 font-mono text-xs text-stone-700" name)
(td :class "py-1.5 px-3 text-xs"
@@ -27,11 +27,11 @@
(or counterexample ""))))
(defcomp ~plan-theorem-prover-content ()
(~doc-page :title "Theorem Prover"
(defcomp ~plans/theorem-prover/plan-theorem-prover-content ()
(~docs/page :title "Theorem Prover"
;; --- Intro ---
(~doc-section :title "SX proves itself" :id "intro"
(~docs/section :title "SX proves itself" :id "intro"
(p :class "text-stone-600"
(code "prove.sx") " is a constraint solver and property prover written in SX. It takes the SX specification (" (code "primitives.sx") "), translates it to formal logic via " (code "z3.sx") ", and proves properties about the result. Every step in the pipeline is an s-expression program operating on other s-expressions.")
(p :class "text-stone-600"
@@ -42,10 +42,10 @@
"No external solver. No Python proof logic. " (code "prove.sx") " is 400+ lines of s-expressions that parse SMT-LIB, evaluate expressions, generate test domains, compute cartesian products, search for counterexamples, and produce verification conditions. The same code would work client-side via the bootstrapped JavaScript evaluator.")))
;; --- Phase 1 Results ---
(~doc-section :title "Phase 1: Definitional satisfiability" :id "phase1"
(~docs/section :title "Phase 1: Definitional satisfiability" :id "phase1"
(p :class "text-stone-600"
"Every " (code "define-primitive") " with a " (code ":body") " produces a " (code "forall") " assertion in SMT-LIB. For example, " (code "(define-primitive \"inc\" :params (n) :body (+ n 1))") " becomes:")
(~doc-code :code (highlight "; inc\n(declare-fun inc (Int) Int)\n(assert (forall (((n Int)))\n (= (inc n) (+ n 1))))\n(check-sat)" "lisp"))
(~docs/code :code (highlight "; inc\n(declare-fun inc (Int) Int)\n(assert (forall (((n Int)))\n (= (inc n) (+ n 1))))\n(check-sat)" "lisp"))
(p :class "text-stone-600"
"This is satisfiable by construction: define " (code "inc(n) = n + 1") " and the assertion holds. " (code "prove.sx") " verifies this mechanically for every primitive — it parses the SMT-LIB, extracts the definition, builds a model, and evaluates it with test values.")
@@ -57,7 +57,7 @@
(p :class "text-xs text-emerald-700 mt-1"
"Computed live in " (str phase1-ms) "ms"))
(~doc-subsection :title "Results"
(~docs/subsection :title "Results"
(div :class "overflow-x-auto rounded border border-stone-200 max-h-64 overflow-y-auto"
(table :class "w-full text-left"
(thead
@@ -66,13 +66,13 @@
(th :class "py-2 px-3 text-xs text-stone-500 font-medium" "Status")))
(tbody
(map (fn (r)
(~prove-phase1-row
(~plans/theorem-prover/prove-phase1-row
:name (get r "name")
:status (get r "status")))
phase1-results))))))
;; --- Phase 2 Results ---
(~doc-section :title "Phase 2: Algebraic properties" :id "phase2"
(~docs/section :title "Phase 2: Algebraic properties" :id "phase2"
(p :class "text-stone-600"
"Phase 1 proves internal consistency. Phase 2 proves " (em "external properties") " — mathematical laws that should hold across all inputs. Each property is defined as a test function evaluated over a bounded integer domain.")
(p :class "text-stone-600"
@@ -86,7 +86,7 @@
(p :class "text-xs text-emerald-700 mt-1"
(str phase2-total-tested " constraint evaluations in " phase2-ms "ms")))
(~doc-subsection :title "Results"
(~docs/subsection :title "Results"
(div :class "overflow-x-auto rounded border border-stone-200 max-h-96 overflow-y-auto"
(table :class "w-full text-left"
(thead
@@ -98,7 +98,7 @@
(th :class "py-2 px-3 text-xs text-stone-500 font-medium" "Counterexample")))
(tbody
(map (fn (r)
(~prove-phase2-row
(~plans/theorem-prover/prove-phase2-row
:name (get r "name")
:status (get r "status")
:tested (get r "tested")
@@ -107,7 +107,7 @@
phase2-results))))))
;; --- What the properties prove ---
(~doc-section :title "What the properties prove" :id "properties"
(~docs/section :title "What the properties prove" :id "properties"
(p :class "text-stone-600"
"34 properties across seven categories. Each encodes a mathematical law that the SX primitives must obey.")
@@ -163,23 +163,23 @@
(code "(!= a b) = (not (= a b))") "."))))
;; --- SMT-LIB output ---
(~doc-section :title "SMT-LIB verification conditions" :id "smtlib"
(~docs/section :title "SMT-LIB verification conditions" :id "smtlib"
(p :class "text-stone-600"
"Each property also generates SMT-LIB for unbounded verification by an external solver. The strategy: assert the " (em "negation") " of the universal property. If Z3 returns " (code "unsat") ", the property holds for " (em "all") " integers — not just the bounded domain.")
(p :class "text-stone-600"
(code "prove.sx") " reuses " (code "z3-expr") " from " (code "z3.sx") " to translate the property AST to SMT-LIB. Properties with preconditions use " (code "=>") " (implication). The same SX expression is both the bounded test and the formal verification condition.")
(~doc-code :code (highlight smtlib-sample "lisp")))
(~docs/code :code (highlight smtlib-sample "lisp")))
;; --- What it tells us ---
(~doc-section :title "What this tells us about SX" :id "implications"
(~docs/section :title "What this tells us about SX" :id "implications"
(p :class "text-stone-600"
"Three things, at increasing depth.")
(~doc-subsection :title "1. The spec is internally consistent"
(~docs/subsection :title "1. The spec is internally consistent"
(p :class "text-stone-600"
"Phase 1 proves every " (code "define-primitive") " with a " (code ":body") " is satisfiable. The definition doesn't contradict itself. This is necessary but weak — it's true by construction. The value is mechanical verification: no typo, no copy-paste error, no accidental negation in any of the 91 definitions."))
(~doc-subsection :title "2. The primitives obey algebraic laws"
(~docs/subsection :title "2. The primitives obey algebraic laws"
(p :class "text-stone-600"
"Phase 2 proves real mathematical properties hold across bounded domains. These aren't tautologies — they're constraints that " (em "could") " fail. "
(code "(+ a b) = (+ b a)") " could fail if " (code "+") " had a subtle bug. "
@@ -188,7 +188,7 @@
(p :class "text-stone-600"
"Bounded model checking is not a mathematical proof — it verifies over a finite domain. The SMT-LIB output bridges the gap: feed it to Z3 for a universal proof over all integers."))
(~doc-subsection :title "3. SX can reason about itself"
(~docs/subsection :title "3. SX can reason about itself"
(p :class "text-stone-600"
"The deep result. The SX evaluator executes " (code "z3.sx") ", which reads SX spec files and emits formal logic. Then the SX evaluator executes " (code "prove.sx") ", which parses that logic and proves properties about it. The specification, the translator, and the prover are all written in the same language, operating on the same data structures.")
(p :class "text-stone-600"
@@ -199,7 +199,7 @@
"The SX spec defines primitives. " (code "z3.sx") " (written in SX, using those primitives) translates the spec to formal logic. " (code "prove.sx") " (written in SX, using those same primitives) proves properties about the logic. The primitives being verified are the same primitives doing the verifying. This is not circular — it's a fixed point. If the primitives were wrong, the proofs would fail."))))
;; --- The pipeline ---
(~doc-section :title "The full pipeline" :id "pipeline"
(~docs/section :title "The full pipeline" :id "pipeline"
(div :class "rounded border border-stone-200 bg-stone-50 p-4"
(table :class "w-full text-sm"
(thead (tr :class "text-left text-stone-500"
@@ -237,7 +237,7 @@
"Steps 1-3 run on this page, live, in the SX evaluator. Step 4 requires an external Z3 installation — the SMT-LIB output above is ready to feed to it."))
;; --- Source ---
(~doc-section :title "The source: prove.sx" :id "source"
(~docs/section :title "The source: prove.sx" :id "source"
(p :class "text-stone-600"
"The entire constraint solver is a single SX file. Key sections: "
(code "smt-eval") " evaluates SMT-LIB expressions. "
@@ -245,9 +245,9 @@
(code "prove-search") " walks tuples looking for counterexamples. "
(code "sx-properties") " declares 34 algebraic laws as test functions with quoted ASTs. "
(code "prove-property-smtlib") " translates properties to SMT-LIB verification conditions via " (code "z3-expr") ".")
(~doc-code :code (highlight prove-source "lisp")))
(~docs/code :code (highlight prove-source "lisp")))
(~doc-section :title "The translator: z3.sx" :id "z3-source"
(~docs/section :title "The translator: z3.sx" :id "z3-source"
(p :class "text-stone-600"
"The translator that " (code "prove.sx") " depends on. SX expressions that walk other SX expressions and emit SMT-LIB strings. Both files together: ~760 lines of s-expressions, no host language logic.")
(~doc-code :code (highlight z3-source "lisp")))))
(~docs/code :code (highlight z3-source "lisp")))))

View File

@@ -2,10 +2,10 @@
;; Typed SX — Gradual Type System
;; ---------------------------------------------------------------------------
(defcomp ~plan-typed-sx-content ()
(~doc-page :title "Typed SX"
(defcomp ~plans/typed-sx/plan-typed-sx-content ()
(~docs/page :title "Typed SX"
(~doc-section :title "The Opportunity" :id "opportunity"
(~docs/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 "/sx/(etc.(plan.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."))
@@ -14,7 +14,7 @@
;; What already exists
;; -----------------------------------------------------------------------
(~doc-section :title "What Already Exists" :id "existing"
(~docs/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"
@@ -57,42 +57,42 @@
;; Type language
;; -----------------------------------------------------------------------
(~doc-section :title "Type Language" :id "type-language"
(~docs/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")))
(~docs/subsection :title "Base types"
(~docs/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")))
(~docs/subsection :title "Compound types"
(~docs/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")))
(~docs/subsection :title "Component types"
(~docs/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 the core. No higher-kinded types, no dependent types, no type classes. Just: what goes in, what comes out, can it be nil.")
(~doc-subsection :title "User-defined types"
(~doc-code :code (highlight ";; Type alias — a name for an existing type\n(deftype price number)\n(deftype html-string string)\n\n;; Union — one of several types\n(deftype renderable (union string number nil))\n(deftype key-type (union string keyword))\n\n;; Record — typed dict shape\n(deftype card-props\n {:title string\n :subtitle string?\n :price number})\n\n;; Parameterized — generic over a type variable\n(deftype (maybe a) (union nil a))\n(deftype (list-of-pairs a b) (list-of (dict-of a b)))\n(deftype (result a e) (union (ok a) (err e)))" "lisp"))
(~docs/subsection :title "User-defined types"
(~docs/code :code (highlight ";; Type alias — a name for an existing type\n(deftype price number)\n(deftype html-string string)\n\n;; Union — one of several types\n(deftype renderable (union string number nil))\n(deftype key-type (union string keyword))\n\n;; Record — typed dict shape\n(deftype card-props\n {:title string\n :subtitle string?\n :price number})\n\n;; Parameterized — generic over a type variable\n(deftype (maybe a) (union nil a))\n(deftype (list-of-pairs a b) (list-of (dict-of a b)))\n(deftype (result a e) (union (ok a) (err e)))" "lisp"))
(p (code "deftype") " is a declaration form — zero runtime cost, purely for the checker. The type registry resolves user-defined type names during " (code "subtype?") " and " (code "infer-type") ". Records enable typed keyword args for components:")
(~doc-code :code (highlight ";; Define a prop shape\n(deftype card-props\n {:title string\n :subtitle string?\n :price number\n :on-click (-> any nil)?})\n\n;; Use it in a component\n(defcomp ~product-card (&key (props :as card-props) &rest children)\n (div :class \"card\"\n (h2 (get props :title))\n (span (format-decimal (get props :price) 2))\n children))\n\n;; Checker validates dict literals against record shape:\n(~product-card :props {:title \"Widget\" :price \"oops\"})\n;; ^^^^^^\n;; ERROR: :price expects number, got string" "lisp"))))
(~docs/code :code (highlight ";; Define a prop shape\n(deftype card-props\n {:title string\n :subtitle string?\n :price number\n :on-click (-> any nil)?})\n\n;; Use it in a component\n(defcomp ~plans/typed-sx/product-card (&key (props :as card-props) &rest children)\n (div :class \"card\"\n (h2 (get props :title))\n (span (format-decimal (get props :price) 2))\n children))\n\n;; Checker validates dict literals against record shape:\n(~plans/typed-sx/product-card :props {:title \"Widget\" :price \"oops\"})\n;; ^^^^^^\n;; ERROR: :price expects number, got string" "lisp"))))
;; -----------------------------------------------------------------------
;; Annotation syntax
;; -----------------------------------------------------------------------
(~doc-section :title "Annotation Syntax" :id "syntax"
(~docs/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"))
(~docs/subsection :title "1. Component params"
(~docs/code :code (highlight ";; Current (unchanged, still works)\n(defcomp ~plans/typed-sx/product-card (&key title price image-url &rest children)\n (div ...))\n\n;; Annotated — colon after param name\n(defcomp ~plans/typed-sx/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"))
(~docs/subsection :title "2. Define/lambda return types"
(~docs/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"))
(~docs/subsection :title "3. Let bindings"
(~docs/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."))
@@ -100,19 +100,19 @@
;; Type checking
;; -----------------------------------------------------------------------
(~doc-section :title "Type Checking" :id "checking"
(~docs/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"
(~docs/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 "Component calls:") " " (code "(~plans/typed-sx/product-card :title 42)") " — " (code ":title") " declared as " (code "string") ", got " (code "number") ". Error.")
(li (strong "Missing required params:") " " (code "(~plans/typed-sx/product-card :price 29.99)") " — " (code ":title") " not provided, no default. Error.")
(li (strong "Unknown keyword args:") " " (code "(~plans/typed-sx/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"
(~docs/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 (yet).") " " (code "(get user \"name\")") " — the checker knows " (code "get") " returns " (code "any") " but doesn't track which keys a dict has. Phase 6 (" (code "deftype") " records) will enable this.")
@@ -120,7 +120,7 @@
(li (strong "Full algebraic effects.") " The effect system (Phase 7) checks static effect annotations — it does not provide algebraic effect handlers, effect polymorphism, or continuation-based effect dispatch. That door remains open for the future.")))
(~doc-subsection :title "Inference"
(~docs/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"))
@@ -135,7 +135,7 @@
;; Gradual semantics
;; -----------------------------------------------------------------------
(~doc-section :title "Gradual Semantics" :id "gradual"
(~docs/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"
@@ -145,16 +145,16 @@
(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")))
(~docs/code :code (highlight ";; Sweet spot: annotate the interface, infer the rest\n(defcomp ~plans/typed-sx/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"
(~docs/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"))
(~docs/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 ~reactive-islands/event-bridge/product-page (products.sx:12)\n;;\n;; (~plans/typed-sx/product-card :title product-name :price \"29.99\")\n;; ^^^^^^\n;; Keyword :price of ~plans/typed-sx/product-card expects: number\n;; Got: string (literal \"29.99\")\n;;\n;; Fix: (~plans/typed-sx/product-card :title product-name :price 29.99)" "lisp"))
(p "Severity levels:")
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
@@ -183,10 +183,10 @@
;; Nil narrowing
;; -----------------------------------------------------------------------
(~doc-section :title "Nil Narrowing" :id "nil"
(~docs/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"))
(~docs/code :code (highlight ";; Before: runtime error if user is nil\n(defcomp ~plans/typed-sx/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 ~plans/typed-sx/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"
@@ -200,10 +200,10 @@
;; Component signature verification
;; -----------------------------------------------------------------------
(~doc-section :title "Component Signature Verification" :id "signatures"
(~docs/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"))
(~docs/code :code (highlight ";; Definition\n(defcomp ~plans/typed-sx/product-card (&key (title : string)\n (price : number)\n (image-url : string?)\n &rest children)\n ...)\n\n;; Call site checks:\n(~plans/typed-sx/product-card :title \"Widget\" :price 29.99) ;; OK\n(~plans/typed-sx/product-card :title \"Widget\") ;; ERROR: :price required\n(~plans/typed-sx/product-card :title 42 :price 29.99) ;; ERROR: :title expects string\n(~plans/typed-sx/product-card :title \"Widget\" :price 29.99\n (p \"Description\") (p \"Details\")) ;; OK: children\n(~plans/typed-sx/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"
@@ -219,10 +219,10 @@
;; Thread-first type flow
;; -----------------------------------------------------------------------
(~doc-section :title "Thread-First Type Flow" :id "thread-first"
(~docs/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"))
(~docs/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."))
@@ -230,7 +230,7 @@
;; Relationship to prove.sx
;; -----------------------------------------------------------------------
(~doc-section :title "Types vs Proofs" :id "types-vs-proofs"
(~docs/section :title "Types vs Proofs" :id "types-vs-proofs"
(p (a :href "/sx/(etc.(plan.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"
@@ -267,43 +267,43 @@
;; User-defined types
;; -----------------------------------------------------------------------
(~doc-section :title "User-Defined Types" :id "deftype"
(~docs/section :title "User-Defined Types" :id "deftype"
(p (code "deftype") " introduces named types — aliases, unions, records, and parameterized types. All are declaration-only, zero runtime cost, resolved at check time.")
(~doc-subsection :title "Type aliases"
(~doc-code :code (highlight ";; Simple name for an existing type\n(deftype price number)\n(deftype html-string string)\n(deftype user-id (or string number))\n\n;; Use anywhere a type is expected\n(defcomp ~price-tag (&key (amount :as price) (label :as string))\n (span :class \"price\" (str label \": $\" (format-decimal amount 2))))" "lisp"))
(~docs/subsection :title "Type aliases"
(~docs/code :code (highlight ";; Simple name for an existing type\n(deftype price number)\n(deftype html-string string)\n(deftype user-id (or string number))\n\n;; Use anywhere a type is expected\n(defcomp ~plans/typed-sx/price-tag (&key (amount :as price) (label :as string))\n (span :class \"price\" (str label \": $\" (format-decimal amount 2))))" "lisp"))
(p "Aliases are transparent — " (code "price") " IS " (code "number") " for all checking purposes. They exist for documentation and domain semantics."))
(~doc-subsection :title "Union types"
(~doc-code :code (highlight ";; Named unions\n(deftype renderable (union string number nil component))\n(deftype key-type (union string keyword))\n(deftype falsy (union nil false))\n\n;; The checker narrows unions in branches:\n(define handle-input\n (fn ((val :as (union string number)))\n (if (string? val)\n (upper val) ;; narrowed to string — upper is valid\n (+ val 1)))) ;; narrowed to number — + is valid" "lisp"))
(~docs/subsection :title "Union types"
(~docs/code :code (highlight ";; Named unions\n(deftype renderable (union string number nil component))\n(deftype key-type (union string keyword))\n(deftype falsy (union nil false))\n\n;; The checker narrows unions in branches:\n(define handle-input\n (fn ((val :as (union string number)))\n (if (string? val)\n (upper val) ;; narrowed to string — upper is valid\n (+ val 1)))) ;; narrowed to number — + is valid" "lisp"))
(p "Union types compose with narrowing — " (code "if (string? x)") " in the then-branch narrows " (code "(union string number)") " to " (code "string") ". Same flow typing that already works for nullable."))
(~doc-subsection :title "Record types (typed dicts)"
(~doc-code :code (highlight ";; Typed dict shape\n(deftype card-props\n {:title string\n :subtitle string?\n :price number\n :tags (list-of string)})\n\n;; Checker validates dict literals against shape:\n{:title \"Widget\" :price \"oops\"}\n;; ERROR: :price expects number, got string\n\n{:title \"Widget\" :price 29.99}\n;; WARNING: missing :tags (required field)\n\n;; Record types enable typed component props:\n(defcomp ~product-card (&key (props :as card-props) &rest children)\n (div :class \"card\"\n (h2 (get props :title))\n children))" "lisp"))
(~docs/subsection :title "Record types (typed dicts)"
(~docs/code :code (highlight ";; Typed dict shape\n(deftype card-props\n {:title string\n :subtitle string?\n :price number\n :tags (list-of string)})\n\n;; Checker validates dict literals against shape:\n{:title \"Widget\" :price \"oops\"}\n;; ERROR: :price expects number, got string\n\n{:title \"Widget\" :price 29.99}\n;; WARNING: missing :tags (required field)\n\n;; Record types enable typed component props:\n(defcomp ~plans/typed-sx/product-card (&key (props :as card-props) &rest children)\n (div :class \"card\"\n (h2 (get props :title))\n children))" "lisp"))
(p "Records are the big win. Components pass dicts everywhere — config, props, context. A record type makes " (code "get") " on a known-shape dict return the field's type instead of " (code "any") ". This is where " (code "deftype") " pays for itself."))
(~doc-subsection :title "Parameterized types"
(~doc-code :code (highlight ";; Generic over type variables\n(deftype (maybe a) (union nil a))\n(deftype (result a e) (union {:ok a} {:err e}))\n(deftype (pair a b) {:fst a :snd b})\n\n;; Used in signatures:\n(define find-user : (-> number (maybe user-record))\n (fn (id) ...))\n\n;; Checker instantiates: (maybe user-record) = (union nil user-record)\n;; So the caller must handle nil." "lisp"))
(~docs/subsection :title "Parameterized types"
(~docs/code :code (highlight ";; Generic over type variables\n(deftype (maybe a) (union nil a))\n(deftype (result a e) (union {:ok a} {:err e}))\n(deftype (pair a b) {:fst a :snd b})\n\n;; Used in signatures:\n(define find-user : (-> number (maybe user-record))\n (fn (id) ...))\n\n;; Checker instantiates: (maybe user-record) = (union nil user-record)\n;; So the caller must handle nil." "lisp"))
(p "Parameterized types are substitution-based — " (code "(maybe string)") " expands to " (code "(union nil string)") " at check time. No inference of type parameters (that would require Hindley-Milner). You write " (code "(maybe string)") " explicitly, the checker substitutes and verifies.")))
;; -----------------------------------------------------------------------
;; Effect system
;; -----------------------------------------------------------------------
(~doc-section :title "Effect System" :id "effects"
(~docs/section :title "Effect System" :id "effects"
(p "The pragmatic middle: static effect " (em "checking") " without algebraic effect " (em "handlers") ". Functions declare what side effects they use. The checker enforces that effects don't leak across boundaries. No continuations, no runtime cost.")
(~doc-subsection :title "Effect declarations"
(~doc-code :code (highlight ";; Declare named effects\n(defeffect io) ;; Database, HTTP, file system\n(defeffect dom) ;; Browser DOM manipulation\n(defeffect async) ;; Asynchronous operations\n(defeffect state) ;; Mutable state (set!, dict-set!, append!)\n\n;; Functions declare their effects in brackets\n(define fetch-user : (-> number user) [io async]\n (fn (id) (query \"SELECT * FROM users WHERE id = $1\" id)))\n\n(define toggle-class : (-> element string nil) [dom]\n (fn (el cls) (set-attr! el :class cls)))\n\n;; Pure by default — no annotation means no effects\n(define add-prices : (-> (list-of number) number)\n (fn (prices) (reduce + 0 prices)))" "lisp")))
(~docs/subsection :title "Effect declarations"
(~docs/code :code (highlight ";; Declare named effects\n(defeffect io) ;; Database, HTTP, file system\n(defeffect dom) ;; Browser DOM manipulation\n(defeffect async) ;; Asynchronous operations\n(defeffect state) ;; Mutable state (set!, dict-set!, append!)\n\n;; Functions declare their effects in brackets\n(define fetch-user : (-> number user) [io async]\n (fn (id) (query \"SELECT * FROM users WHERE id = $1\" id)))\n\n(define toggle-class : (-> element string nil) [dom]\n (fn (el cls) (set-attr! el :class cls)))\n\n;; Pure by default — no annotation means no effects\n(define add-prices : (-> (list-of number) number)\n (fn (prices) (reduce + 0 prices)))" "lisp")))
(~doc-subsection :title "What it checks"
(~docs/subsection :title "What it checks"
(ol :class "list-decimal pl-5 text-stone-700 space-y-2"
(li (strong "Pure functions can't call effectful ones.") " A function with no effect annotation calling " (code "fetch-user") " (which has " (code "[io async]") ") is an error. The IO leaked into pure code.")
(li (strong "Components declare their effect ceiling.") " A " (code "[pure]") " component can only call pure functions. A " (code "[io]") " component can call IO but not DOM. This is the render-mode safety guarantee.")
(li (strong "Render modes enforce effect sets.") " " (code "render-to-html") " (server) allows " (code "[io]") " but not " (code "[dom]") ". " (code "render-to-dom") " (browser) allows " (code "[dom]") " but not " (code "[io]") ". " (code "aser") " (wire format) allows " (code "[io]") " for evaluation but serializes the result.")
(li (strong "Islands are the effect boundary.") " Server effects (" (code "io") ") can't cross into client island code. Client effects (" (code "dom") ") can't leak into server rendering. Currently this is convention — effects make it a proof.")))
(~doc-subsection :title "Three effect sets match three render modes"
(~docs/subsection :title "Three effect sets match three render modes"
(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"
@@ -326,17 +326,17 @@
(p "This is exactly the information " (code "deps.sx") " already computes — which components have IO refs. Effects promote it from a runtime classification to a static type-level property. Pure components get an ironclad guarantee: memoize, cache, SSR anywhere, serialize for client — provably safe."))
(~doc-subsection :title "Effect propagation"
(~doc-code :code (highlight ";; Effects propagate through calls:\n(define fetch-prices : (-> (list-of number)) [io async]\n (fn () (query \"SELECT price FROM products\")))\n\n(define render-total : (-> element) [io async] ;; must declare, calls fetch-prices\n (fn ()\n (let ((prices (fetch-prices)))\n (span (str \"$\" (reduce + 0 prices))))))\n\n;; ERROR if you forget:\n(define render-total : (-> element) ;; no effects declared\n (fn ()\n (let ((prices (fetch-prices))) ;; ERROR: calls [io async] from pure context\n (span (str \"$\" (reduce + 0 prices))))))" "lisp"))
(~docs/subsection :title "Effect propagation"
(~docs/code :code (highlight ";; Effects propagate through calls:\n(define fetch-prices : (-> (list-of number)) [io async]\n (fn () (query \"SELECT price FROM products\")))\n\n(define render-total : (-> element) [io async] ;; must declare, calls fetch-prices\n (fn ()\n (let ((prices (fetch-prices)))\n (span (str \"$\" (reduce + 0 prices))))))\n\n;; ERROR if you forget:\n(define render-total : (-> element) ;; no effects declared\n (fn ()\n (let ((prices (fetch-prices))) ;; ERROR: calls [io async] from pure context\n (span (str \"$\" (reduce + 0 prices))))))" "lisp"))
(p "The checker walks call graphs and verifies that every function's declared effects are a superset of its callees' effects. This is transitive — if A calls B calls C, and C has " (code "[io]") ", then A must also declare " (code "[io]") "."))
(~doc-subsection :title "Gradual effects"
(~docs/subsection :title "Gradual effects"
(p "Like gradual types, effects are opt-in. Unannotated functions are assumed to have " (em "all") " effects — they can call anything, and anything can call them. This is safe (no false positives) but provides no guarantees. As you annotate more functions, the checker catches more violations.")
(p "The practical sweet spot: annotate " (code "defcomp") " declarations (they're the public API) and let the checker verify that pure components don't accidentally depend on IO. Internal helpers can stay unannotated until they matter.")
(~doc-code :code (highlight ";; Annotated component — checker enforces purity\n(defcomp ~price-display [pure] (&key (price :as number))\n (span :class \"price\" (str \"$\" (format-decimal price 2))))\n\n;; ERROR: pure component calls IO\n(defcomp ~price-display [pure] (&key (product-id :as number))\n (let ((product (fetch-product product-id))) ;; ERROR: [io] in [pure] context\n (span :class \"price\" (str \"$\" (get product \"price\")))))" "lisp")))
(~docs/code :code (highlight ";; Annotated component — checker enforces purity\n(defcomp ~plans/typed-sx/price-display [pure] (&key (price :as number))\n (span :class \"price\" (str \"$\" (format-decimal price 2))))\n\n;; ERROR: pure component calls IO\n(defcomp ~plans/typed-sx/price-display [pure] (&key (product-id :as number))\n (let ((product (fetch-product product-id))) ;; ERROR: [io] in [pure] context\n (span :class \"price\" (str \"$\" (get product \"price\")))))" "lisp")))
(~doc-subsection :title "Relationship to deps.sx and boundary.sx"
(~docs/subsection :title "Relationship to deps.sx and boundary.sx"
(p "Effects don't replace the existing systems — they formalize them:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code "boundary.sx") " declares which primitives are IO. Effects declare which " (em "functions") " use IO.")
@@ -344,7 +344,7 @@
(li "The boundary is still the source of truth for " (em "what is IO") ". Effects are the enforcement mechanism for " (em "who can use it") "."))
(p "Long term, " (code "deps.sx") "'s IO classification can be derived from effect annotations. In the short term, both coexist — effects are checked, deps are computed, both must agree."))
(~doc-subsection :title "What it does NOT do"
(~docs/subsection :title "What it does NOT do"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "No algebraic effect handlers.") " You can't intercept and resume effects. This would require delimited continuations in every bootstrapper target — massive complexity for marginal UI benefit.")
(li (strong "No effect polymorphism.") " You can't write a function generic over effects (" (code "forall e. (-> a [e] b)") "). This needs higher-kinded effect types — the same complexity as type classes.")
@@ -359,9 +359,9 @@
;; Implementation
;; -----------------------------------------------------------------------
(~doc-section :title "Implementation" :id "implementation"
(~docs/section :title "Implementation" :id "implementation"
(~doc-subsection :title "Phase 1: Type Registry (done)"
(~docs/subsection :title "Phase 1: Type Registry (done)"
(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"))
@@ -369,7 +369,7 @@
(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 (done)"
(~docs/subsection :title "Phase 2: Type Inference Engine (done)"
(p "Walk AST, infer types bottom-up.")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Literals → concrete types")
@@ -380,7 +380,7 @@
(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 (done)"
(~docs/subsection :title "Phase 3: Type Checker (done)"
(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?"))
@@ -388,19 +388,19 @@
(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 (done)"
(~docs/subsection :title "Phase 4: Annotation Parsing (done)"
(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 (done)"
(~docs/subsection :title "Phase 5: Typed Primitives (done)"
(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"))
(~docs/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."))
(~doc-subsection :title "Phase 6: User-Defined Types (deftype)"
(~docs/subsection :title "Phase 6: User-Defined Types (deftype)"
(p "Extend the type system with named user-defined types.")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code "deftype") " special form — parsed by evaluator, stored in type registry")
@@ -412,7 +412,7 @@
(li "Extend " (code "subtype?") " — resolve named types through the registry before comparing")
(li "Test: dict literal against record shape, parameterized type instantiation, field-typed " (code "get"))))
(~doc-subsection :title "Phase 7: Static Effect System"
(~docs/subsection :title "Phase 7: Static Effect System"
(p "Add effect annotations and static checking. No handlers, no runtime cost.")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code "defeffect") " declaration form — registers named effects: " (code "io") ", " (code "dom") ", " (code "async") ", " (code "state"))
@@ -429,7 +429,7 @@
;; Spec module
;; -----------------------------------------------------------------------
(~doc-section :title "Spec Module" :id "spec-module"
(~docs/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"
@@ -481,7 +481,7 @@
;; Relationships
;; -----------------------------------------------------------------------
(~doc-section :title "Relationships" :id "relationships"
(~docs/section :title "Relationships" :id "relationships"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (a :href "/sx/(etc.(plan.theorem-prover))" :class "text-violet-700 underline" "Theorem Prover") " — prove.sx verifies primitive properties; types.sx verifies composition. Complementary.")
(li (a :href "/sx/(etc.(plan.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.")

View File

@@ -2,10 +2,10 @@
;; WASM Bytecode VM — Compile SX to bytecode, run in Rust/WASM
;; ---------------------------------------------------------------------------
(defcomp ~plan-wasm-bytecode-vm-content ()
(~doc-page :title "WASM Bytecode VM"
(defcomp ~plans/wasm-bytecode-vm/plan-wasm-bytecode-vm-content ()
(~docs/page :title "WASM Bytecode VM"
(~doc-section :title "The Idea" :id "idea"
(~docs/section :title "The Idea" :id "idea"
(p "Currently the client-side SX runtime is a tree-walking interpreter bootstrapped to JavaScript. The server sends " (strong "SX source text") " — component definitions, page content — and the browser parses and evaluates it.")
(p "The alternative: compile SX to a " (strong "compact bytecode format") ", ship bytecode to the browser, and execute it in a " (strong "WebAssembly VM written in Rust") ". The VM calls out to JavaScript for DOM operations and I/O via standard WASM↔JS bindings.")
(p "This fits naturally into the SX host architecture. Rust becomes another bootstrapper target. The spec compiles to Rust the same way it compiles to Python and JavaScript. The WASM module is the client-side expression of that Rust target."))
@@ -14,7 +14,7 @@
;; Why
;; -----------------------------------------------------------------------
(~doc-section :title "Why" :id "why"
(~docs/section :title "Why" :id "why"
(ul :class "list-disc list-inside space-y-2"
(li (strong "Wire size") " — bytecode is far more compact than source text. No redundant whitespace, no comments, no repeated symbol names. A component bundle that's 40KB of SX source might be 8KB of bytecode.")
(li (strong "No parse overhead") " — the browser currently parses every SX source string (tokenize → AST → eval). Bytecode skips parsing entirely.")
@@ -26,12 +26,12 @@
;; Architecture
;; -----------------------------------------------------------------------
(~doc-section :title "Architecture" :id "architecture"
(~docs/section :title "Architecture" :id "architecture"
(p "Three new layers, all specced in " (code ".sx") " and bootstrapped:")
(h4 :class "font-semibold mt-4 mb-2" "1. Bytecode format — bytecode.sx")
(p "A spec for the bytecode instruction set. Stack-based VM (simpler than register-based, natural fit for s-expressions). Instructions:")
(~doc-code :code (highlight ";; Core instructions\nPUSH_CONST idx ;; push constant from pool\nPUSH_NIL ;; push nil\nPUSH_TRUE / PUSH_FALSE\nLOOKUP idx ;; look up symbol by index\nSET idx ;; define/set symbol\nCALL n ;; call top-of-stack with n args\nTAIL_CALL n ;; tail call (TCO)\nRETURN\nJUMP offset ;; unconditional jump\nJUMP_IF_FALSE offset ;; conditional jump\nMAKE_LAMBDA idx n_params ;; create closure\nMAKE_LIST n ;; collect n stack values into list\nMAKE_DICT n ;; collect 2n stack values into dict\nPOP ;; discard top\nDUP ;; duplicate top" "lisp"))
(~docs/code :code (highlight ";; Core instructions\nPUSH_CONST idx ;; push constant from pool\nPUSH_NIL ;; push nil\nPUSH_TRUE / PUSH_FALSE\nLOOKUP idx ;; look up symbol by index\nSET idx ;; define/set symbol\nCALL n ;; call top-of-stack with n args\nTAIL_CALL n ;; tail call (TCO)\nRETURN\nJUMP offset ;; unconditional jump\nJUMP_IF_FALSE offset ;; conditional jump\nMAKE_LAMBDA idx n_params ;; create closure\nMAKE_LIST n ;; collect n stack values into list\nMAKE_DICT n ;; collect 2n stack values into dict\nPOP ;; discard top\nDUP ;; duplicate top" "lisp"))
(p "Bytecode modules contain: a " (strong "constant pool") " (strings, numbers, symbols), a " (strong "code section") " (instruction bytes), and a " (strong "metadata section") " (source maps, component/island declarations for the host to register).")
(h4 :class "font-semibold mt-4 mb-2" "2. Compiler — compile.sx")
@@ -54,23 +54,23 @@
;; DOM interop
;; -----------------------------------------------------------------------
(~doc-section :title "DOM Interop" :id "dom-interop"
(~docs/section :title "DOM Interop" :id "dom-interop"
(p "The main engineering challenge. Every DOM operation crosses the WASM↔JS boundary. Two strategies:")
(h4 :class "font-semibold mt-4 mb-2" "Strategy A: Direct calls")
(p "Each DOM operation (" (code "createElement") ", " (code "setAttribute") ", " (code "appendChild") ") is a separate WASM→JS call. Simple, works, but ~50ns overhead per call. For a page with 1,000 DOM operations, that's ~50μs — negligible.")
(~doc-code :code (highlight "// JS side — imported by WASM\nfunction domCreateElement(tag_ptr, tag_len) {\n const tag = readString(tag_ptr, tag_len);\n return storeHandle(document.createElement(tag));\n}\n\n// Rust side\nextern \"C\" { fn dom_create_element(tag: *const u8, len: u32) -> u32; }" "javascript"))
(~docs/code :code (highlight "// JS side — imported by WASM\nfunction domCreateElement(tag_ptr, tag_len) {\n const tag = readString(tag_ptr, tag_len);\n return storeHandle(document.createElement(tag));\n}\n\n// Rust side\nextern \"C\" { fn dom_create_element(tag: *const u8, len: u32) -> u32; }" "javascript"))
(h4 :class "font-semibold mt-4 mb-2" "Strategy B: Command buffer")
(p "Batch DOM operations in WASM memory as a command buffer. Flush to JS in one call. JS walks the buffer and applies all operations. Fewer boundary crossings, but more complex.")
(~doc-code :code (highlight ";; Command buffer format (in shared WASM memory)\n;; [CREATE_ELEMENT, tag_idx, handle_out]\n;; [SET_ATTR, handle, key_idx, val_idx]\n;; [APPEND_CHILD, parent_handle, child_handle]\n;; [SET_TEXT, handle, text_idx]\n;; Then: (flush-dom-commands)" "lisp"))
(~docs/code :code (highlight ";; Command buffer format (in shared WASM memory)\n;; [CREATE_ELEMENT, tag_idx, handle_out]\n;; [SET_ATTR, handle, key_idx, val_idx]\n;; [APPEND_CHILD, parent_handle, child_handle]\n;; [SET_TEXT, handle, text_idx]\n;; Then: (flush-dom-commands)" "lisp"))
(p "Strategy A is simpler and sufficient for SX workloads. Strategy B is an optimisation if profiling shows the boundary crossing matters. " (strong "Start with A, measure, switch to B only if needed.")))
;; -----------------------------------------------------------------------
;; String handling
;; -----------------------------------------------------------------------
(~doc-section :title "String Handling" :id "strings"
(~docs/section :title "String Handling" :id "strings"
(p "WASM has no native string type. Strings must cross the boundary via shared " (code "ArrayBuffer") " memory. Options:")
(ul :class "list-disc list-inside space-y-2 mt-2"
(li (strong "Copy on crossing") " — encode to UTF-8 in WASM linear memory, JS reads via " (code "TextDecoder") ". Simple, safe, ~1μs per string.")
@@ -82,7 +82,7 @@
;; Memory management
;; -----------------------------------------------------------------------
(~doc-section :title "Memory & Closures" :id "memory"
(~docs/section :title "Memory & Closures" :id "memory"
(p "SX values that the VM must manage:")
(ul :class "list-disc list-inside space-y-2 mt-2"
(li (strong "Closures") " — lambda captures free variables. Rust: " (code "Rc<Closure>") " with captured env as " (code "Vec<Value>") ".")
@@ -95,7 +95,7 @@
;; What gets compiled
;; -----------------------------------------------------------------------
(~doc-section :title "What Gets Compiled" :id "compilation"
(~docs/section :title "What Gets Compiled" :id "compilation"
(p "Not everything needs bytecode. The compilation boundary follows the existing server/client split:")
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
(table :class "w-full text-left text-sm"
@@ -129,7 +129,7 @@
;; Bytecode vs direct WASM compilation
;; -----------------------------------------------------------------------
(~doc-section :title "Bytecode VM vs Direct WASM Compilation" :id "vm-vs-direct"
(~docs/section :title "Bytecode VM vs Direct WASM Compilation" :id "vm-vs-direct"
(p "Two paths to WASM. The choice matters:")
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
(table :class "w-full text-left text-sm"
@@ -168,9 +168,9 @@
;; Dual target — same spec, runtime choice
;; -----------------------------------------------------------------------
(~doc-section :title "Dual Target: JS or WASM from the Same Spec" :id "dual-target"
(~docs/section :title "Dual Target: JS or WASM from the Same Spec" :id "dual-target"
(p "The key insight: this is " (strong "not a replacement") " for the JS evaluator. It's " (strong "another compilation target from the same spec") ". The existing bootstrapper pipeline already proves this pattern:")
(~doc-code :code (highlight "eval.sx ──→ bootstrap_js.py ──→ sx-ref.js (browser, JS eval)\n ──→ bootstrap_py.py ──→ sx_ref.py (server, Python eval)\n ──→ bootstrap_rs.py ──→ sx-vm.wasm (browser, WASM eval) ← new" "text"))
(~docs/code :code (highlight "eval.sx ──→ bootstrap_js.py ──→ sx-ref.js (browser, JS eval)\n ──→ bootstrap_py.py ──→ sx_ref.py (server, Python eval)\n ──→ bootstrap_rs.py ──→ sx-vm.wasm (browser, WASM eval) ← new" "text"))
(p "All three outputs have identical semantics because they're compiled from the same source. The choice of which to use is a " (strong "deployment decision") ", not an architectural one:")
(ul :class "list-disc list-inside space-y-2 mt-2"
(li (strong "JS-only") " — current default. Works everywhere. Zero WASM dependency. Ship sx-browser.js + SX source text.")
@@ -184,7 +184,7 @@
;; Implementation phases
;; -----------------------------------------------------------------------
(~doc-section :title "Implementation Phases" :id "phases"
(~docs/section :title "Implementation Phases" :id "phases"
(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"
@@ -225,7 +225,7 @@
;; Interaction with existing plans
;; -----------------------------------------------------------------------
(~doc-section :title "Interaction with Other Plans" :id "interactions"
(~docs/section :title "Interaction with Other Plans" :id "interactions"
(ul :class "list-disc list-inside space-y-2"
(li (strong "Async Eval Convergence") " — must complete first. The spec must be the single evaluator before we add another target. Otherwise we'd be bootstrapping a fork.")
(li (strong "Runtime Slicing") " — the WASM module can be tiered just like the JS runtime. L0 hypermedia needs no VM at all (pure HTML). L1 DOM ops needs a minimal VM. L2 islands needs signals. The WASM module should be tree-shakeable.")
@@ -237,7 +237,7 @@
;; Principles
;; -----------------------------------------------------------------------
(~doc-section :title "Principles" :id "principles"
(~docs/section :title "Principles" :id "principles"
(ul :class "list-disc list-inside space-y-2"
(li (strong "The spec remains the single source of truth.") " The bytecode format, compiler, and VM semantics are all specced in .sx. The Rust VM is just another host, like Python and JavaScript.")
(li (strong "Bytecode is an optimisation, not a requirement.") " SX source text remains a valid wire format. The system degrades gracefully — if WASM isn't available, fall back to the JS evaluator. Progressive enhancement.")
@@ -249,7 +249,7 @@
;; Outcome
;; -----------------------------------------------------------------------
(~doc-section :title "Outcome" :id "outcome"
(~docs/section :title "Outcome" :id "outcome"
(p "After completion:")
(ul :class "list-disc list-inside space-y-2 mt-2"
(li "SX compiles to four targets: JavaScript, Python, Rust (native), Rust (WASM)")

View File

@@ -1,12 +1,12 @@
;; Protocol documentation pages — fully self-contained
(defcomp ~protocol-wire-format-content ()
(~doc-page :title "Wire Format"
(~doc-section :title "The text/sx content type" :id "content-type"
(defcomp ~protocols/wire-format-content ()
(~docs/page :title "Wire Format"
(~docs/section :title "The text/sx content type" :id "content-type"
(p :class "text-stone-600"
"sx responses use content type text/sx. The body is s-expression source code. The client parses and evaluates it, then renders the result into the DOM.")
(~doc-code :code (highlight "HTTP/1.1 200 OK\nContent-Type: text/sx\nSX-Css-Hash: a1b2c3d4\n\n(div :class \"p-4\"\n (~card :title \"Hello\"))" "bash")))
(~doc-section :title "Request lifecycle" :id "lifecycle"
(~docs/code :code (highlight "HTTP/1.1 200 OK\nContent-Type: text/sx\nSX-Css-Hash: a1b2c3d4\n\n(div :class \"p-4\"\n (~card :title \"Hello\"))" "bash")))
(~docs/section :title "Request lifecycle" :id "lifecycle"
(p :class "text-stone-600"
"1. User interacts with an element that has sx-get/sx-post/etc.")
(p :class "text-stone-600"
@@ -19,28 +19,28 @@
"5. sx.js fires sx:afterSwap and sx:afterSettle.")
(p :class "text-stone-600"
"6. Any sx-swap-oob elements are swapped into their targets elsewhere in the DOM."))
(~doc-section :title "Component definitions" :id "components"
(~docs/section :title "Component definitions" :id "components"
(p :class "text-stone-600"
"On full page loads, component definitions are in <script type=\"text/sx\" data-components>. On subsequent sx requests, missing definitions are prepended to the response body. The client caches definitions in localStorage keyed by a content hash."))))
(defcomp ~protocol-fragments-content ()
(~doc-page :title "Cross-Service Fragments"
(~doc-section :title "Fragment protocol" :id "protocol"
(defcomp ~protocols/fragments-content ()
(~docs/page :title "Cross-Service Fragments"
(~docs/section :title "Fragment protocol" :id "protocol"
(p :class "text-stone-600"
"Rose Ash runs as independent microservices. Each service can expose HTML or sx fragments that other services compose into their pages. Fragment endpoints return text/sx or text/html.")
(p :class "text-stone-600"
"The frag resolver is an I/O primitive in the render tree:")
(~doc-code :code (highlight "(frag \"blog\" \"link-card\" :slug \"hello\")" "lisp")))
(~doc-section :title "SxExpr wrapping" :id "wrapping"
(~docs/code :code (highlight "(frag \"blog\" \"link-card\" :slug \"hello\")" "lisp")))
(~docs/section :title "SxExpr wrapping" :id "wrapping"
(p :class "text-stone-600"
"When a fragment returns text/sx, the response is wrapped in an SxExpr and embedded directly in the render tree. When it returns text/html, it's wrapped in a ~rich-text component that inserts the HTML via raw!. This allows transparent composition across service boundaries."))
(~doc-section :title "fetch_fragments()" :id "fetch"
(~docs/section :title "fetch_fragments()" :id "fetch"
(p :class "text-stone-600"
"The Python helper fetch_fragments() fetches multiple fragments in parallel via asyncio.gather(). Fragments are cached in Redis with short TTLs. Each fragment request is HMAC-signed for authentication."))))
(defcomp ~protocol-resolver-io-content ()
(~doc-page :title "Resolver I/O"
(~doc-section :title "Async I/O primitives" :id "primitives"
(defcomp ~protocols/resolver-io-content ()
(~docs/page :title "Resolver I/O"
(~docs/section :title "Async I/O primitives" :id "primitives"
(p :class "text-stone-600"
"The sx resolver identifies I/O nodes in the render tree, groups them, executes them in parallel via asyncio.gather(), and substitutes results back in.")
(p :class "text-stone-600"
@@ -50,49 +50,49 @@
(li (span :class "font-mono text-violet-700" "query") " — read data from another service")
(li (span :class "font-mono text-violet-700" "action") " — execute a write on another service")
(li (span :class "font-mono text-violet-700" "current-user") " — resolve the current authenticated user")))
(~doc-section :title "Execution model" :id "execution"
(~docs/section :title "Execution model" :id "execution"
(p :class "text-stone-600"
"The render tree is walked to find I/O nodes. All nodes at the same depth are gathered and executed in parallel. Results replace the I/O nodes in the tree. The walk continues until no more I/O nodes are found. This typically completes in 1-2 passes."))))
(defcomp ~protocol-internal-services-content ()
(~doc-page :title "Internal Services"
(~doc-note
(defcomp ~protocols/internal-services-content ()
(~docs/page :title "Internal Services"
(~docs/note
(p "Honest note: the internal service protocol is JSON, not sx. Sx is the composition layer on top. The protocols below are the plumbing underneath."))
(~doc-section :title "HMAC-signed HTTP" :id "hmac"
(~docs/section :title "HMAC-signed HTTP" :id "hmac"
(p :class "text-stone-600"
"Services communicate via HMAC-signed HTTP requests with short timeouts:")
(ul :class "space-y-2 text-stone-600 font-mono text-sm"
(li "GET /internal/data/{query} — read data (3s timeout)")
(li "POST /internal/actions/{action} — execute write (5s timeout)")
(li "POST /internal/inbox — ActivityPub-shaped event delivery")))
(~doc-section :title "fetch_data / call_action" :id "fetch"
(~docs/section :title "fetch_data / call_action" :id "fetch"
(p :class "text-stone-600"
"Python helpers fetch_data() and call_action() handle HMAC signing, serialization, and error handling. They resolve service URLs from environment variables (INTERNAL_URL_BLOG, etc) and fall back to public URLs in development."))))
(defcomp ~protocol-activitypub-content ()
(~doc-page :title "ActivityPub"
(~doc-note
(defcomp ~protocols/activitypub-content ()
(~docs/page :title "ActivityPub"
(~docs/note
(p "Honest note: ActivityPub wire format is JSON-LD, not sx. This documents how AP integrates with the sx rendering layer."))
(~doc-section :title "AP activities" :id "activities"
(~docs/section :title "AP activities" :id "activities"
(p :class "text-stone-600"
"Rose Ash services communicate cross-domain writes via ActivityPub-shaped activities. Each service has a virtual actor. Activities are JSON-LD objects sent to /internal/inbox endpoints. RSA signatures authenticate the sender."))
(~doc-section :title "Event bus" :id "bus"
(~docs/section :title "Event bus" :id "bus"
(p :class "text-stone-600"
"The event bus dispatches activities to registered handlers. Handlers are async functions that process the activity and may trigger side effects. The bus runs as a background processor in each service."))))
(defcomp ~protocol-future-content ()
(~doc-page :title "Future Possibilities"
(~doc-note
(defcomp ~protocols/future-content ()
(~docs/page :title "Future Possibilities"
(~docs/note
(p "This page is speculative. Nothing here is implemented. It documents ideas that may or may not happen."))
(~doc-section :title "Custom protocol schemes" :id "schemes"
(~docs/section :title "Custom protocol schemes" :id "schemes"
(p :class "text-stone-600"
"sx:// and sxs:// as custom URI schemes for content addressing or deep linking. An sx:// URI could resolve to an sx expression from a federated registry. This is technically feasible but practically unnecessary for a single-site deployment."))
(~doc-section :title "Sx as AP serialization" :id "ap-sx"
(~docs/section :title "Sx as AP serialization" :id "ap-sx"
(p :class "text-stone-600"
"ActivityPub objects could be serialized as s-expressions instead of JSON-LD. S-expressions are more compact and easier to parse. The practical barrier: the entire AP ecosystem expects JSON-LD."))
(~doc-section :title "Sx-native federation" :id "federation"
(~docs/section :title "Sx-native federation" :id "federation"
(p :class "text-stone-600"
"Federated services could exchange sx fragments directly — render a remote user's profile card by fetching its sx source from their server. This requires trust and standardization that doesn't exist yet."))
(~doc-section :title "Realistic assessment" :id "realistic"
(~docs/section :title "Realistic assessment" :id "realistic"
(p :class "text-stone-600"
"The most likely near-term improvement is sx:// deep linking for client-side component resolution. Everything else requires ecosystem adoption that one project can't drive alone."))))

View File

@@ -2,103 +2,103 @@
;; Demo page — shows what's been implemented
;; ---------------------------------------------------------------------------
(defcomp ~reactive-islands-demo-content ()
(~doc-page :title "Reactive Islands Demo"
(defcomp ~reactive-islands/demo/reactive-islands-demo-content ()
(~docs/page :title "Reactive Islands Demo"
(~doc-section :title "What this demonstrates" :id "what"
(~docs/section :title "What this demonstrates" :id "what"
(p (strong "These are live interactive islands") " — not static code snippets. Click the buttons. The signal runtime is defined in " (code "signals.sx") " (374 lines of s-expressions), then bootstrapped to JavaScript by " (code "bootstrap_js.py") ". No hand-written signal logic in JavaScript.")
(p "The transpiled " (code "sx-browser.js") " registers " (code "signal") ", " (code "deref") ", " (code "reset!") ", " (code "swap!") ", " (code "computed") ", " (code "effect") ", and " (code "batch") " as SX primitives — callable from " (code "defisland") " bodies defined in " (code ".sx") " files."))
(~doc-section :title "1. Signal + Computed + Effect" :id "demo-counter"
(~docs/section :title "1. Signal + Computed + Effect" :id "demo-counter"
(p "A signal holds a value. A computed derives from it. Click the buttons — the counter and doubled value update instantly, no server round-trip.")
(~demo-counter :initial 0)
(~doc-code :code (highlight "(defisland ~demo-counter (&key initial)\n (let ((count (signal (or initial 0)))\n (doubled (computed (fn () (* 2 (deref count))))))\n (div :class \"...\"\n (button :on-click (fn (e) (swap! count dec)) \"\")\n (span (deref count))\n (button :on-click (fn (e) (swap! count inc)) \"+\")\n (p \"doubled: \" (deref doubled)))))" "lisp"))
(~reactive-islands/demo/counter :initial 0)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/counter (&key initial)\n (let ((count (signal (or initial 0)))\n (doubled (computed (fn () (* 2 (deref count))))))\n (div :class \"...\"\n (button :on-click (fn (e) (swap! count dec)) \"\")\n (span (deref count))\n (button :on-click (fn (e) (swap! count inc)) \"+\")\n (p \"doubled: \" (deref doubled)))))" "lisp"))
(p (code "(deref count)") " in a text position creates a reactive text node. When " (code "count") " changes, " (em "only that text node") " updates. " (code "doubled") " recomputes automatically. No diffing."))
(~doc-section :title "2. Temperature Converter" :id "demo-temperature"
(~docs/section :title "2. Temperature Converter" :id "demo-temperature"
(p "Two derived values from one signal. Click to change Celsius — Fahrenheit updates reactively.")
(~demo-temperature)
(~doc-code :code (highlight "(defisland ~demo-temperature ()\n (let ((celsius (signal 20)))\n (div :class \"...\"\n (button :on-click (fn (e) (swap! celsius (fn (c) (- c 5)))) \"5\")\n (span (deref celsius))\n (button :on-click (fn (e) (swap! celsius (fn (c) (+ c 5)))) \"+5\")\n (span \"°C = \")\n (span (+ (* (deref celsius) 1.8) 32))\n (span \"°F\"))))" "lisp"))
(~reactive-islands/demo/temperature)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/temperature ()\n (let ((celsius (signal 20)))\n (div :class \"...\"\n (button :on-click (fn (e) (swap! celsius (fn (c) (- c 5)))) \"5\")\n (span (deref celsius))\n (button :on-click (fn (e) (swap! celsius (fn (c) (+ c 5)))) \"+5\")\n (span \"°C = \")\n (span (+ (* (deref celsius) 1.8) 32))\n (span \"°F\"))))" "lisp"))
(p "The actual implementation uses " (code "computed") " for Fahrenheit: " (code "(computed (fn () (+ (* (deref celsius) 1.8) 32)))") ". The " (code "(deref fahrenheit)") " in the span creates a reactive text node that updates when celsius changes."))
(~doc-section :title "3. Effect + Cleanup: Stopwatch" :id "demo-stopwatch"
(~docs/section :title "3. Effect + Cleanup: Stopwatch" :id "demo-stopwatch"
(p "Effects can return cleanup functions. This stopwatch starts a " (code "set-interval") " — the cleanup clears it when the running signal toggles off.")
(~demo-stopwatch)
(~doc-code :code (highlight "(defisland ~demo-stopwatch ()\n (let ((running (signal false))\n (elapsed (signal 0))\n (time-text (create-text-node \"0.0s\"))\n (btn-text (create-text-node \"Start\")))\n ;; Timer: effect creates interval, cleanup clears it\n (effect (fn ()\n (when (deref running)\n (let ((id (set-interval (fn () (swap! elapsed inc)) 100)))\n (fn () (clear-interval id))))))\n ;; Display: updates text node when elapsed changes\n (effect (fn ()\n (let ((e (deref elapsed)))\n (dom-set-text-content time-text\n (str (floor (/ e 10)) \".\" (mod e 10) \"s\")))))\n ;; Button label\n (effect (fn ()\n (dom-set-text-content btn-text\n (if (deref running) \"Stop\" \"Start\"))))\n (div :class \"...\"\n (span time-text)\n (button :on-click (fn (e) (swap! running not)) btn-text)\n (button :on-click (fn (e)\n (reset! running false) (reset! elapsed 0)) \"Reset\"))))" "lisp"))
(~reactive-islands/demo/stopwatch)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/stopwatch ()\n (let ((running (signal false))\n (elapsed (signal 0))\n (time-text (create-text-node \"0.0s\"))\n (btn-text (create-text-node \"Start\")))\n ;; Timer: effect creates interval, cleanup clears it\n (effect (fn ()\n (when (deref running)\n (let ((id (set-interval (fn () (swap! elapsed inc)) 100)))\n (fn () (clear-interval id))))))\n ;; Display: updates text node when elapsed changes\n (effect (fn ()\n (let ((e (deref elapsed)))\n (dom-set-text-content time-text\n (str (floor (/ e 10)) \".\" (mod e 10) \"s\")))))\n ;; Button label\n (effect (fn ()\n (dom-set-text-content btn-text\n (if (deref running) \"Stop\" \"Start\"))))\n (div :class \"...\"\n (span time-text)\n (button :on-click (fn (e) (swap! running not)) btn-text)\n (button :on-click (fn (e)\n (reset! running false) (reset! elapsed 0)) \"Reset\"))))" "lisp"))
(p "Three effects, each tracking different signals. The timer effect's cleanup fires before each re-run — toggling " (code "running") " off clears the interval. No hook rules: effects can appear anywhere, in any order."))
(~doc-section :title "4. Imperative Pattern" :id "demo-imperative"
(~docs/section :title "4. Imperative Pattern" :id "demo-imperative"
(p "For complex reactivity (dynamic classes, conditional text), use the imperative pattern: " (code "create-text-node") " + " (code "effect") " + " (code "dom-set-text-content") ".")
(~demo-imperative)
(~doc-code :code (highlight "(defisland ~demo-imperative ()\n (let ((count (signal 0))\n (text-node (create-text-node \"0\")))\n ;; Explicit effect: re-runs when count changes\n (effect (fn ()\n (dom-set-text-content text-node (str (deref count)))))\n (div :class \"...\"\n (span text-node)\n (button :on-click (fn (e) (swap! count inc)) \"+\"))))" "lisp"))
(~reactive-islands/demo/imperative)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/imperative ()\n (let ((count (signal 0))\n (text-node (create-text-node \"0\")))\n ;; Explicit effect: re-runs when count changes\n (effect (fn ()\n (dom-set-text-content text-node (str (deref count)))))\n (div :class \"...\"\n (span text-node)\n (button :on-click (fn (e) (swap! count inc)) \"+\"))))" "lisp"))
(p "Two patterns exist: " (strong "declarative") " (" (code "(span (deref sig))") " — auto-reactive via " (code "reactive-text") ") and " (strong "imperative") " (" (code "create-text-node") " + " (code "effect") " — explicit, full control). Use declarative for simple text, imperative for dynamic classes, conditional DOM, or complex updates."))
(~doc-section :title "5. Reactive List" :id "demo-reactive-list"
(~docs/section :title "5. Reactive List" :id "demo-reactive-list"
(p "When " (code "map") " is used with " (code "(deref signal)") " inside an island, it auto-upgrades to a reactive list. With " (code ":key") " attributes, existing DOM nodes are reused across updates — only additions, removals, and reorderings touch the DOM.")
(~demo-reactive-list)
(~doc-code :code (highlight "(defisland ~demo-reactive-list ()\n (let ((next-id (signal 1))\n (items (signal (list)))\n (add-item (fn (e)\n (batch (fn ()\n (swap! items (fn (old)\n (append old (dict \"id\" (deref next-id)\n \"text\" (str \"Item \" (deref next-id))))))\n (swap! next-id inc)))))\n (remove-item (fn (id)\n (swap! items (fn (old)\n (filter (fn (item) (not (= (get item \"id\") id))) old))))))\n (div\n (button :on-click add-item \"Add Item\")\n (span (deref (computed (fn () (len (deref items))))) \" items\")\n (ul\n (map (fn (item)\n (li :key (str (get item \"id\"))\n (span (get item \"text\"))\n (button :on-click (fn (e) (remove-item (get item \"id\"))) \"✕\")))\n (deref items))))))" "lisp"))
(~reactive-islands/demo/reactive-list)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/reactive-list ()\n (let ((next-id (signal 1))\n (items (signal (list)))\n (add-item (fn (e)\n (batch (fn ()\n (swap! items (fn (old)\n (append old (dict \"id\" (deref next-id)\n \"text\" (str \"Item \" (deref next-id))))))\n (swap! next-id inc)))))\n (remove-item (fn (id)\n (swap! items (fn (old)\n (filter (fn (item) (not (= (get item \"id\") id))) old))))))\n (div\n (button :on-click add-item \"Add Item\")\n (span (deref (computed (fn () (len (deref items))))) \" items\")\n (ul\n (map (fn (item)\n (li :key (str (get item \"id\"))\n (span (get item \"text\"))\n (button :on-click (fn (e) (remove-item (get item \"id\"))) \"✕\")))\n (deref items))))))" "lisp"))
(p (code ":key") " identifies each list item. When items change, the reconciler matches old and new keys — reusing existing DOM nodes, inserting new ones, and removing stale ones. Without keys, the list falls back to clear-and-rerender. " (code "batch") " groups the two signal writes into one update pass."))
(~doc-section :title "6. Input Binding" :id "demo-input-binding"
(~docs/section :title "6. Input Binding" :id "demo-input-binding"
(p "The " (code ":bind") " attribute creates a two-way link between a signal and a form element. Type in the input — the signal updates. Change the signal — the input updates. Works with text inputs, checkboxes, radios, textareas, and selects.")
(~demo-input-binding)
(~doc-code :code (highlight "(defisland ~demo-input-binding ()\n (let ((name (signal \"\"))\n (agreed (signal false)))\n (div\n (input :type \"text\" :bind name\n :placeholder \"Type your name...\")\n (span \"Hello, \" (strong (deref name)) \"!\")\n (input :type \"checkbox\" :bind agreed)\n (when (deref agreed)\n (p \"Thanks for agreeing!\")))))" "lisp"))
(~reactive-islands/demo/input-binding)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/input-binding ()\n (let ((name (signal \"\"))\n (agreed (signal false)))\n (div\n (input :type \"text\" :bind name\n :placeholder \"Type your name...\")\n (span \"Hello, \" (strong (deref name)) \"!\")\n (input :type \"checkbox\" :bind agreed)\n (when (deref agreed)\n (p \"Thanks for agreeing!\")))))" "lisp"))
(p (code ":bind") " detects the element type automatically — text inputs use " (code "value") " + " (code "input") " event, checkboxes use " (code "checked") " + " (code "change") " event. The effect only updates the DOM when the value actually changed, preventing cursor jump."))
(~doc-section :title "7. Portals" :id "demo-portal"
(~docs/section :title "7. Portals" :id "demo-portal"
(p "A " (code "portal") " renders children into a DOM node " (em "outside") " the island's subtree. Essential for modals, tooltips, and toasts — anything that must escape " (code "overflow:hidden") " or z-index stacking.")
(~demo-portal)
(~doc-code :code (highlight "(defisland ~demo-portal ()\n (let ((open? (signal false)))\n (div\n (button :on-click (fn (e) (swap! open? not))\n (if (deref open?) \"Close Modal\" \"Open Modal\"))\n (portal \"#portal-root\"\n (when (deref open?)\n (div :class \"fixed inset-0 bg-black/50 ...\"\n :on-click (fn (e) (reset! open? false))\n (div :class \"bg-white rounded-lg p-6 ...\"\n :on-click (fn (e) (stop-propagation e))\n (h2 \"Portal Modal\")\n (p \"Rendered outside the island's DOM.\")\n (button :on-click (fn (e) (reset! open? false))\n \"Close\"))))))))" "lisp"))
(~reactive-islands/demo/portal)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/portal ()\n (let ((open? (signal false)))\n (div\n (button :on-click (fn (e) (swap! open? not))\n (if (deref open?) \"Close Modal\" \"Open Modal\"))\n (portal \"#portal-root\"\n (when (deref open?)\n (div :class \"fixed inset-0 bg-black/50 ...\"\n :on-click (fn (e) (reset! open? false))\n (div :class \"bg-white rounded-lg p-6 ...\"\n :on-click (fn (e) (stop-propagation e))\n (h2 \"Portal Modal\")\n (p \"Rendered outside the island's DOM.\")\n (button :on-click (fn (e) (reset! open? false))\n \"Close\"))))))))" "lisp"))
(p "The portal content lives in " (code "#portal-root") " (typically at the page body level), not inside the island. On island disposal, portal content is automatically removed from its target — the " (code "register-in-scope") " mechanism handles cleanup."))
(~doc-section :title "8. Error Boundaries" :id "demo-error-boundary"
(~docs/section :title "8. Error Boundaries" :id "demo-error-boundary"
(p "When an island's rendering or effect throws, " (code "error-boundary") " catches the error and renders a fallback. The fallback receives the error and a retry function. Partial effects created before the error are disposed automatically.")
(~demo-error-boundary)
(~doc-code :code (highlight "(defisland ~demo-error-boundary ()\n (let ((throw? (signal false)))\n (error-boundary\n ;; Fallback: receives (err retry-fn)\n (fn (err retry-fn)\n (div :class \"p-3 bg-red-50 border border-red-200 rounded\"\n (p :class \"text-red-700\" (error-message err))\n (button :on-click (fn (e)\n (reset! throw? false) (invoke retry-fn))\n \"Retry\")))\n ;; Children: the happy path\n (do\n (when (deref throw?) (error \"Intentional explosion!\"))\n (p \"Everything is fine.\")))))" "lisp"))
(~reactive-islands/demo/error-boundary)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/error-boundary ()\n (let ((throw? (signal false)))\n (error-boundary\n ;; Fallback: receives (err retry-fn)\n (fn (err retry-fn)\n (div :class \"p-3 bg-red-50 border border-red-200 rounded\"\n (p :class \"text-red-700\" (error-message err))\n (button :on-click (fn (e)\n (reset! throw? false) (invoke retry-fn))\n \"Retry\")))\n ;; Children: the happy path\n (do\n (when (deref throw?) (error \"Intentional explosion!\"))\n (p \"Everything is fine.\")))))" "lisp"))
(p "React equivalent: " (code "componentDidCatch") " / " (code "ErrorBoundary") ". SX's version is simpler — one form, not a class. The " (code "error-boundary") " form is a render-dom special form in " (code "adapter-dom.sx") "."))
(~doc-section :title "9. Refs — Imperative DOM Access" :id "demo-refs"
(~docs/section :title "9. Refs — Imperative DOM Access" :id "demo-refs"
(p "The " (code ":ref") " attribute captures a DOM element handle into a dict. Use it for imperative operations: focusing, measuring, reading values.")
(~demo-refs)
(~doc-code :code (highlight "(defisland ~demo-refs ()\n (let ((my-ref (dict \"current\" nil))\n (msg (signal \"\")))\n (input :ref my-ref :type \"text\"\n :placeholder \"I can be focused programmatically\")\n (button :on-click (fn (e)\n (dom-focus (get my-ref \"current\")))\n \"Focus Input\")\n (button :on-click (fn (e)\n (let ((el (get my-ref \"current\")))\n (reset! msg (str \"value: \" (dom-get-prop el \"value\")))))\n \"Read Input\")\n (when (not (= (deref msg) \"\"))\n (p (deref msg)))))" "lisp"))
(~reactive-islands/demo/refs)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/refs ()\n (let ((my-ref (dict \"current\" nil))\n (msg (signal \"\")))\n (input :ref my-ref :type \"text\"\n :placeholder \"I can be focused programmatically\")\n (button :on-click (fn (e)\n (dom-focus (get my-ref \"current\")))\n \"Focus Input\")\n (button :on-click (fn (e)\n (let ((el (get my-ref \"current\")))\n (reset! msg (str \"value: \" (dom-get-prop el \"value\")))))\n \"Read Input\")\n (when (not (= (deref msg) \"\"))\n (p (deref msg)))))" "lisp"))
(p "React equivalent: " (code "useRef") ". In SX, a ref is just " (code "(dict \"current\" nil)") " — no special API. The " (code ":ref") " attribute sets " (code "(dict-set! ref \"current\" el)") " when the element is created. Read it with " (code "(get ref \"current\")") "."))
(~doc-section :title "10. Dynamic Class and Style" :id "demo-dynamic-class"
(~docs/section :title "10. Dynamic Class and Style" :id "demo-dynamic-class"
(p "React uses " (code "className") " and " (code "style") " props with state. SX does the same — " (code "(deref signal)") " inside a " (code ":class") " or " (code ":style") " attribute creates a reactive binding. The attribute updates when the signal changes.")
(~demo-dynamic-class)
(~doc-code :code (highlight "(defisland ~demo-dynamic-class ()\n (let ((danger (signal false))\n (size (signal 16)))\n (div\n (button :on-click (fn (e) (swap! danger not))\n (if (deref danger) \"Safe mode\" \"Danger mode\"))\n (button :on-click (fn (e) (swap! size (fn (s) (+ s 2))))\n \"Bigger\")\n ;; Reactive class — recomputed when danger changes\n (div :class (str \"p-3 rounded font-medium \"\n (if (deref danger)\n \"bg-red-100 text-red-800\"\n \"bg-green-100 text-green-800\"))\n ;; Reactive style — recomputed when size changes\n :style (str \"font-size:\" (deref size) \"px\")\n \"This element's class and style are reactive.\"))))" "lisp"))
(~reactive-islands/demo/dynamic-class)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/dynamic-class ()\n (let ((danger (signal false))\n (size (signal 16)))\n (div\n (button :on-click (fn (e) (swap! danger not))\n (if (deref danger) \"Safe mode\" \"Danger mode\"))\n (button :on-click (fn (e) (swap! size (fn (s) (+ s 2))))\n \"Bigger\")\n ;; Reactive class — recomputed when danger changes\n (div :class (str \"p-3 rounded font-medium \"\n (if (deref danger)\n \"bg-red-100 text-red-800\"\n \"bg-green-100 text-green-800\"))\n ;; Reactive style — recomputed when size changes\n :style (str \"font-size:\" (deref size) \"px\")\n \"This element's class and style are reactive.\"))))" "lisp"))
(p "React equivalent: " (code "className={danger ? 'red' : 'green'}") " and " (code "style={{fontSize: size}}") ". In SX the " (code "str") " + " (code "if") " + " (code "deref") " pattern handles it — no " (code "classnames") " library needed. For complex conditional classes, use a " (code "computed") " or a CSSX " (code "defcomp") " that returns a class string."))
(~doc-section :title "11. Resource + Suspense Pattern" :id "demo-resource"
(~docs/section :title "11. Resource + Suspense Pattern" :id "demo-resource"
(p (code "resource") " wraps an async operation into a signal with " (code "loading") "/" (code "data") "/" (code "error") " states. Combined with " (code "cond") " + " (code "deref") ", this is the suspense pattern — no special form needed.")
(~demo-resource)
(~doc-code :code (highlight "(defisland ~demo-resource ()\n (let ((data (resource (fn ()\n ;; Any promise-returning function\n (promise-delayed 1500\n (dict \"name\" \"Ada Lovelace\"\n \"role\" \"First Programmer\"))))))\n ;; This IS the suspense pattern:\n (let ((state (deref data)))\n (cond\n (get state \"loading\")\n (div \"Loading...\")\n (get state \"error\")\n (div \"Error: \" (get state \"error\"))\n :else\n (div (get (get state \"data\") \"name\"))))))" "lisp"))
(~reactive-islands/demo/resource)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/resource ()\n (let ((data (resource (fn ()\n ;; Any promise-returning function\n (promise-delayed 1500\n (dict \"name\" \"Ada Lovelace\"\n \"role\" \"First Programmer\"))))))\n ;; This IS the suspense pattern:\n (let ((state (deref data)))\n (cond\n (get state \"loading\")\n (div \"Loading...\")\n (get state \"error\")\n (div \"Error: \" (get state \"error\"))\n :else\n (div (get (get state \"data\") \"name\"))))))" "lisp"))
(p "React equivalent: " (code "Suspense") " + " (code "use()") " or " (code "useSWR") ". SX doesn't need a special " (code "suspense") " form because " (code "resource") " returns a signal and " (code "cond") " + " (code "deref") " creates reactive conditional rendering. When the promise resolves, the signal updates and the " (code "cond") " branch switches automatically."))
(~doc-section :title "12. Transition Pattern" :id "demo-transition"
(~docs/section :title "12. Transition Pattern" :id "demo-transition"
(p "React's " (code "startTransition") " defers non-urgent updates so typing stays responsive. In SX: " (code "schedule-idle") " + " (code "batch") ". The filter runs during idle time, not blocking the input event.")
(~demo-transition)
(~doc-code :code (highlight "(defisland ~demo-transition ()\n (let ((query (signal \"\"))\n (all-items (list \"Signals\" \"Effects\" ...))\n (filtered (signal (list)))\n (pending (signal false)))\n (reset! filtered all-items)\n ;; Filter effect — deferred via schedule-idle\n (effect (fn ()\n (let ((q (lower (deref query))))\n (if (= q \"\")\n (do (reset! pending false)\n (reset! filtered all-items))\n (do (reset! pending true)\n (schedule-idle (fn ()\n (batch (fn ()\n (reset! filtered\n (filter (fn (item)\n (contains? (lower item) q))\n all-items))\n (reset! pending false))))))))))\n (div\n (input :bind query :placeholder \"Filter...\")\n (when (deref pending) (span \"Filtering...\"))\n (ul (map (fn (item) (li :key item item))\n (deref filtered))))))" "lisp"))
(~reactive-islands/demo/transition)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/transition ()\n (let ((query (signal \"\"))\n (all-items (list \"Signals\" \"Effects\" ...))\n (filtered (signal (list)))\n (pending (signal false)))\n (reset! filtered all-items)\n ;; Filter effect — deferred via schedule-idle\n (effect (fn ()\n (let ((q (lower (deref query))))\n (if (= q \"\")\n (do (reset! pending false)\n (reset! filtered all-items))\n (do (reset! pending true)\n (schedule-idle (fn ()\n (batch (fn ()\n (reset! filtered\n (filter (fn (item)\n (contains? (lower item) q))\n all-items))\n (reset! pending false))))))))))\n (div\n (input :bind query :placeholder \"Filter...\")\n (when (deref pending) (span \"Filtering...\"))\n (ul (map (fn (item) (li :key item item))\n (deref filtered))))))" "lisp"))
(p "React equivalent: " (code "startTransition(() => setFiltered(...))") ". SX uses " (code "schedule-idle") " (" (code "requestIdleCallback") " under the hood) to defer the expensive " (code "filter") " operation, and " (code "batch") " to group the result into one update. Fine-grained signals already avoid the jank that makes transitions critical in React — this pattern is for truly expensive computations."))
(~doc-section :title "13. Shared Stores" :id "demo-stores"
(~docs/section :title "13. Shared Stores" :id "demo-stores"
(p "React uses " (code "Context") " or state management libraries for cross-component state. SX uses " (code "def-store") " / " (code "use-store") " — named signal containers that persist across island creation/destruction.")
(~demo-store-writer)
(~demo-store-reader)
(~doc-code :code (highlight ";; Island A — creates/writes the store\n(defisland ~store-writer ()\n (let ((store (def-store \"theme\" (fn ()\n (dict \"color\" (signal \"violet\")\n \"dark\" (signal false))))))\n (select :bind (get store \"color\")\n (option :value \"violet\" \"Violet\")\n (option :value \"blue\" \"Blue\"))\n (input :type \"checkbox\" :bind (get store \"dark\"))))\n\n;; Island B — reads the same store, different island\n(defisland ~store-reader ()\n (let ((store (use-store \"theme\")))\n (div :class (str \"bg-\" (deref (get store \"color\")) \"-100\")\n \"Styled by signals from Island A\")))" "lisp"))
(~reactive-islands/index/demo-store-writer)
(~reactive-islands/index/demo-store-reader)
(~docs/code :code (highlight ";; Island A — creates/writes the store\n(defisland ~reactive-islands/demo/store-writer ()\n (let ((store (def-store \"theme\" (fn ()\n (dict \"color\" (signal \"violet\")\n \"dark\" (signal false))))))\n (select :bind (get store \"color\")\n (option :value \"violet\" \"Violet\")\n (option :value \"blue\" \"Blue\"))\n (input :type \"checkbox\" :bind (get store \"dark\"))))\n\n;; Island B — reads the same store, different island\n(defisland ~reactive-islands/demo/store-reader ()\n (let ((store (use-store \"theme\")))\n (div :class (str \"bg-\" (deref (get store \"color\")) \"-100\")\n \"Styled by signals from Island A\")))" "lisp"))
(p "React equivalent: " (code "createContext") " + " (code "useContext") " or Redux/Zustand. Stores are simpler — just named dicts of signals at page scope. " (code "def-store") " creates once, " (code "use-store") " retrieves. Stores survive island disposal but clear on full page navigation."))
(~doc-section :title "14. How defisland Works" :id "how-defisland"
(~docs/section :title "14. How defisland Works" :id "how-defisland"
(p (code "defisland") " creates a reactive component. Same calling convention as " (code "defcomp") " — keyword args, rest children — but with a reactive boundary. Inside an island, " (code "deref") " subscribes DOM nodes to signals.")
(~doc-code :code (highlight ";; Definition — same syntax as defcomp\n(defisland ~counter (&key initial)\n (let ((count (signal (or initial 0))))\n (div\n (span (deref count)) ;; reactive text node\n (button :on-click (fn (e) (swap! count inc)) ;; event handler\n \"+\"))))\n\n;; Usage — same as any component\n(~counter :initial 42)\n\n;; Server-side rendering:\n;; <div data-sx-island=\"counter\" data-sx-state='{\"initial\":42}'>\n;; <span>42</span><button>+</button>\n;; </div>\n;;\n;; Client hydrates: signals + effects + event handlers attach" "lisp"))
(~docs/code :code (highlight ";; Definition — same syntax as defcomp\n(defisland ~reactive-islands/demo/counter (&key initial)\n (let ((count (signal (or initial 0))))\n (div\n (span (deref count)) ;; reactive text node\n (button :on-click (fn (e) (swap! count inc)) ;; event handler\n \"+\"))))\n\n;; Usage — same as any component\n(~reactive-islands/demo/counter :initial 42)\n\n;; Server-side rendering:\n;; <div data-sx-island=\"counter\" data-sx-state='{\"initial\":42}'>\n;; <span>42</span><button>+</button>\n;; </div>\n;;\n;; Client hydrates: signals + effects + event handlers attach" "lisp"))
(p "Each " (code "deref") " call registers the enclosing DOM node as a subscriber. Signal changes update " (em "only") " the subscribed nodes — no virtual DOM, no diffing, no component re-renders."))
(~doc-section :title "15. Test suite" :id "demo-tests"
(~docs/section :title "15. Test suite" :id "demo-tests"
(p "17 tests verify the signal runtime against the spec. All pass in the Python test runner (which uses the hand-written evaluator with native platform primitives).")
(~doc-code :code (highlight ";; Signal basics (6 tests)\n(assert-true (signal? (signal 42)))\n(assert-equal 42 (deref (signal 42)))\n(assert-equal 5 (deref 5)) ;; non-signal passthrough\n\n;; reset! changes value\n(let ((s (signal 0)))\n (reset! s 10)\n (assert-equal 10 (deref s)))\n\n;; reset! does NOT notify when value unchanged (identical? check)\n\n;; Computed (3 tests)\n(let ((a (signal 3)) (b (signal 4))\n (sum (computed (fn () (+ (deref a) (deref b))))))\n (assert-equal 7 (deref sum))\n (reset! a 10)\n (assert-equal 14 (deref sum)))\n\n;; Effects (4 tests) — immediate run, re-run on change, dispose, cleanup\n;; Batch (1 test) — defers notifications, deduplicates subscribers\n;; defisland (3 tests) — creates island, callable, accepts children" "lisp"))
(~docs/code :code (highlight ";; Signal basics (6 tests)\n(assert-true (signal? (signal 42)))\n(assert-equal 42 (deref (signal 42)))\n(assert-equal 5 (deref 5)) ;; non-signal passthrough\n\n;; reset! changes value\n(let ((s (signal 0)))\n (reset! s 10)\n (assert-equal 10 (deref s)))\n\n;; reset! does NOT notify when value unchanged (identical? check)\n\n;; Computed (3 tests)\n(let ((a (signal 3)) (b (signal 4))\n (sum (computed (fn () (+ (deref a) (deref b))))))\n (assert-equal 7 (deref sum))\n (reset! a 10)\n (assert-equal 14 (deref sum)))\n\n;; Effects (4 tests) — immediate run, re-run on change, dispose, cleanup\n;; Batch (1 test) — defers notifications, deduplicates subscribers\n;; defisland (3 tests) — creates island, callable, accepts children" "lisp"))
(p :class "mt-2 text-sm text-stone-500" "Run: " (code "python3 shared/sx/tests/run.py signals")))
(~doc-section :title "React Feature Coverage" :id "coverage"
(~docs/section :title "React Feature Coverage" :id "coverage"
(p "Every React feature has an SX equivalent — most are simpler because signals are fine-grained.")
(div :class "overflow-x-auto rounded border border-stone-200"
(table :class "w-full text-left text-sm"

View File

@@ -2,38 +2,38 @@
;; Event Bridge — DOM events for lake→island communication
;; ---------------------------------------------------------------------------
(defcomp ~reactive-islands-event-bridge-content ()
(~doc-page :title "Event Bridge"
(defcomp ~reactive-islands/event-bridge/reactive-islands-event-bridge-content ()
(~docs/page :title "Event Bridge"
(~doc-section :title "The Problem" :id "problem"
(~docs/section :title "The Problem" :id "problem"
(p "A reactive island can contain server-rendered content — an htmx \"lake\" that swaps via " (code "sx-get") "/" (code "sx-post") ". The lake content is pure HTML from the server. It has no access to island signals.")
(p "But sometimes the lake needs to " (em "tell") " the island something happened. A server-rendered \"Add to Cart\" button needs to update the island's cart signal. A server-rendered search form needs to feed results into the island's result signal.")
(p "The event bridge solves this: DOM custom events bubble from the lake up to the island, where an effect listens and updates signals."))
(~doc-section :title "How it works" :id "how"
(~docs/section :title "How it works" :id "how"
(p "Three components:")
(ol :class "space-y-2 text-stone-600 list-decimal list-inside"
(li (strong "Server emits: ") "Server-rendered elements carry " (code "data-sx-emit") " attributes. When the user interacts, the client dispatches a CustomEvent.")
(li (strong "Event bubbles: ") "The event bubbles up through the DOM tree until it reaches the island container.")
(li (strong "Effect catches: ") "An effect inside the island listens for the event name and updates a signal."))
(~doc-code :code (highlight ";; Island with an event bridge\n(defisland ~product-page (&key product)\n (let ((cart-items (signal (list))))\n\n ;; Bridge: listen for \"cart:add\" events from server content\n (bridge-event container \"cart:add\" cart-items\n (fn (detail)\n (append (deref cart-items)\n (dict :id (get detail \"id\")\n :name (get detail \"name\")\n :price (get detail \"price\")))))\n\n (div\n ;; Island header with reactive cart count\n (div :class \"flex justify-between\"\n (h1 (get product \"name\"))\n (span :class \"badge\" (length (deref cart-items)) \" items\"))\n\n ;; htmx lake — server-rendered product details\n ;; This content is swapped by sx-get, not rendered by the island\n (div :id \"product-details\"\n :sx-get (str \"/products/\" (get product \"id\") \"/details\")\n :sx-swap \"innerHTML\"\n :sx-trigger \"load\"))))" "lisp"))
(~docs/code :code (highlight ";; Island with an event bridge\n(defisland ~reactive-islands/event-bridge/product-page (&key product)\n (let ((cart-items (signal (list))))\n\n ;; Bridge: listen for \"cart:add\" events from server content\n (bridge-event container \"cart:add\" cart-items\n (fn (detail)\n (append (deref cart-items)\n (dict :id (get detail \"id\")\n :name (get detail \"name\")\n :price (get detail \"price\")))))\n\n (div\n ;; Island header with reactive cart count\n (div :class \"flex justify-between\"\n (h1 (get product \"name\"))\n (span :class \"badge\" (length (deref cart-items)) \" items\"))\n\n ;; htmx lake — server-rendered product details\n ;; This content is swapped by sx-get, not rendered by the island\n (div :id \"product-details\"\n :sx-get (str \"/products/\" (get product \"id\") \"/details\")\n :sx-swap \"innerHTML\"\n :sx-trigger \"load\"))))" "lisp"))
(p "The server handler for " (code "/products/:id/details") " returns HTML with emit attributes:")
(~doc-code :code (highlight ";; Server-rendered response (pure HTML, no signals)\n(div\n (p (get product \"description\"))\n (div :class \"flex gap-2 mt-4\"\n (button\n :data-sx-emit \"cart:add\"\n :data-sx-emit-detail (json-serialize\n (dict :id (get product \"id\")\n :name (get product \"name\")\n :price (get product \"price\")))\n :class \"bg-violet-600 text-white px-4 py-2 rounded\"\n \"Add to Cart\")))" "lisp"))
(~docs/code :code (highlight ";; Server-rendered response (pure HTML, no signals)\n(div\n (p (get product \"description\"))\n (div :class \"flex gap-2 mt-4\"\n (button\n :data-sx-emit \"cart:add\"\n :data-sx-emit-detail (json-serialize\n (dict :id (get product \"id\")\n :name (get product \"name\")\n :price (get product \"price\")))\n :class \"bg-violet-600 text-white px-4 py-2 rounded\"\n \"Add to Cart\")))" "lisp"))
(p "The button is plain server HTML. When clicked, the client's event bridge dispatches " (code "cart:add") " with the JSON detail. The island effect catches it and appends to " (code "cart-items") ". The badge updates reactively."))
(~doc-section :title "Why signals survive swaps" :id "survival"
(~docs/section :title "Why signals survive swaps" :id "survival"
(p "Signals live in JavaScript memory (closures), not in the DOM. When htmx swaps content inside an island:")
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
(li (strong "Swap inside island: ") "Signals survive. The lake content is replaced but the island's signal closures are untouched. Effects re-bind to new DOM nodes if needed.")
(li (strong "Swap outside island: ") "Signals survive. The island is not affected by swaps to other parts of the page.")
(li (strong "Swap replaces island: ") "Signals are " (em "lost") ". The island is disposed. This is where " (a :href "/sx/(geography.(reactive.named-stores))" :sx-get "/sx/(geography.(reactive.named-stores))" :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" :class "text-violet-700 underline" "named stores") " come in — they persist at page level, surviving island destruction.")))
(~doc-section :title "Spec" :id "spec"
(~docs/section :title "Spec" :id "spec"
(p "The event bridge is spec'd in " (code "signals.sx") " (sections 12-13). Three functions:")
(~doc-code :code (highlight ";; Low-level: dispatch a custom event\n(emit-event el \"cart:add\" {:id 42 :name \"Widget\"})\n\n;; Low-level: listen for a custom event\n(on-event container \"cart:add\" (fn (e)\n (swap! items (fn (old) (append old (event-detail e))))))\n\n;; High-level: bridge an event directly to a signal\n;; Creates an effect with automatic cleanup on dispose\n(bridge-event container \"cart:add\" items\n (fn (detail) (append (deref items) detail)))" "lisp"))
(~docs/code :code (highlight ";; Low-level: dispatch a custom event\n(emit-event el \"cart:add\" {:id 42 :name \"Widget\"})\n\n;; Low-level: listen for a custom event\n(on-event container \"cart:add\" (fn (e)\n (swap! items (fn (old) (append old (event-detail e))))))\n\n;; High-level: bridge an event directly to a signal\n;; Creates an effect with automatic cleanup on dispose\n(bridge-event container \"cart:add\" items\n (fn (detail) (append (deref items) detail)))" "lisp"))
(p "Platform interface required:")
(div :class "overflow-x-auto rounded border border-stone-200 mt-2"

View File

@@ -4,10 +4,10 @@
;; Index / Overview
;; ---------------------------------------------------------------------------
(defcomp ~reactive-islands-index-content ()
(~doc-page :title "Reactive Islands"
(defcomp ~reactive-islands/index/reactive-islands-index-content ()
(~docs/page :title "Reactive Islands"
(~doc-section :title "Architecture" :id "architecture"
(~docs/section :title "Architecture" :id "architecture"
(p "Two orthogonal bars control how an SX page works:")
(ul :class "space-y-1 text-stone-600 list-disc pl-5"
(li (strong "Render boundary") " — where rendering happens (server HTML vs client DOM)")
@@ -32,7 +32,7 @@
(p "Most content stays pure hypermedia. Interactive regions opt into reactivity. The author controls where each component sits on both bars."))
(~doc-section :title "Four Levels" :id "levels"
(~docs/section :title "Four Levels" :id "levels"
(div :class "space-y-4"
(div :class "rounded border border-stone-200 p-4"
(div :class "font-semibold text-stone-800" "Level 0: Pure Hypermedia")
@@ -51,11 +51,11 @@
(p :class "text-sm text-stone-600 mt-1"
"Islands that share state via signal props or named stores (" (code "def-store") " / " (code "use-store") ")."))))
(~doc-section :title "Signal Primitives" :id "signals"
(~doc-code :code (highlight "(signal v) ;; create a reactive container\n(deref s) ;; read value — subscribes in reactive context\n(reset! s v) ;; write new value — notifies subscribers\n(swap! s f) ;; update via function: (f old-value)\n(computed fn) ;; derived signal — auto-tracks dependencies\n(effect fn) ;; side effect — re-runs when deps change\n(batch fn) ;; group writes — one notification pass" "lisp"))
(~docs/section :title "Signal Primitives" :id "signals"
(~docs/code :code (highlight "(signal v) ;; create a reactive container\n(deref s) ;; read value — subscribes in reactive context\n(reset! s v) ;; write new value — notifies subscribers\n(swap! s f) ;; update via function: (f old-value)\n(computed fn) ;; derived signal — auto-tracks dependencies\n(effect fn) ;; side effect — re-runs when deps change\n(batch fn) ;; group writes — one notification pass" "lisp"))
(p "Signals are values, not hooks. Create them anywhere — conditionals, loops, closures. No rules of hooks. Pass them as arguments, store them in dicts, share between islands."))
(~doc-section :title "Island Lifecycle" :id "lifecycle"
(~docs/section :title "Island Lifecycle" :id "lifecycle"
(ol :class "space-y-2 text-stone-600 list-decimal list-inside"
(li (strong "Definition: ") (code "defisland") " registers a reactive component (like " (code "defcomp") " + island flag)")
(li (strong "Server render: ") "Body evaluated with initial values. " (code "deref") " returns plain value. Output wrapped in " (code "data-sx-island") " / " (code "data-sx-state"))
@@ -63,7 +63,7 @@
(li (strong "Updates: ") "Signal changes update only subscribed DOM nodes. No full island re-render")
(li (strong "Disposal: ") "Island removed from DOM — all signals and effects cleaned up via " (code "with-island-scope"))))
(~doc-section :title "Implementation Status" :id "status"
(~docs/section :title "Implementation Status" :id "status"
(p :class "text-stone-600 mb-3" "All signal logic lives in " (code ".sx") " spec files and is bootstrapped to JavaScript and Python. No SX-specific logic in host languages.")
(div :class "overflow-x-auto rounded border border-stone-200"
@@ -168,7 +168,7 @@
;; ---------------------------------------------------------------------------
;; 1. Counter — basic signal + effect
(defisland ~demo-counter (&key initial)
(defisland ~reactive-islands/index/demo-counter (&key initial)
(let ((count (signal (or initial 0)))
(doubled (computed (fn () (* 2 (deref count))))))
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4"
@@ -185,7 +185,7 @@
"doubled: " (span :class "font-mono text-violet-700" (deref doubled))))))
;; 2. Temperature converter — computed derived signal
(defisland ~demo-temperature ()
(defisland ~reactive-islands/index/demo-temperature ()
(let ((celsius (signal 20))
(fahrenheit (computed (fn () (+ (* (deref celsius) 1.8) 32)))))
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4"
@@ -206,7 +206,7 @@
(span :class "text-stone-500" "°F")))))
;; 3. Imperative counter — shows create-text-node + effect pattern
(defisland ~demo-imperative ()
(defisland ~reactive-islands/index/demo-imperative ()
(let ((count (signal 0))
(text-node (create-text-node "0"))
(_eff (effect (fn ()
@@ -224,7 +224,7 @@
"+")))))
;; 4. Stopwatch — effect with cleanup (interval), fully imperative
(defisland ~demo-stopwatch ()
(defisland ~reactive-islands/index/demo-stopwatch ()
(let ((running (signal false))
(elapsed (signal 0))
(time-text (create-text-node "0.0s"))
@@ -258,7 +258,7 @@
;; 5. Reactive list — map over a signal, auto-updates when signal changes
(defisland ~demo-reactive-list ()
(defisland ~reactive-islands/index/demo-reactive-list ()
(let ((next-id (signal 1))
(items (signal (list)))
(add-item (fn (e)
@@ -288,7 +288,7 @@
(deref items))))))
;; 6. Input binding — two-way signal binding for form elements
(defisland ~demo-input-binding ()
(defisland ~reactive-islands/index/demo-input-binding ()
(let ((name (signal ""))
(agreed (signal false)))
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4 space-y-3"
@@ -309,7 +309,7 @@
;; 7. Portal — render into a remote DOM target
(defisland ~demo-portal ()
(defisland ~reactive-islands/index/demo-portal ()
(let ((open? (signal false)))
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4"
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700"
@@ -329,7 +329,7 @@
"Close"))))))))
;; 8. Error boundary — catch errors, render fallback with retry
(defisland ~demo-error-boundary ()
(defisland ~reactive-islands/index/demo-error-boundary ()
(let ((throw? (signal false)))
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4"
(div :class "flex items-center gap-3 mb-3"
@@ -355,7 +355,7 @@
"Everything is fine. Click \"Trigger Error\" to throw."))))))
;; 9. Refs — imperative DOM access via :ref attribute
(defisland ~demo-refs ()
(defisland ~reactive-islands/index/demo-refs ()
(let ((my-ref (dict "current" nil))
(msg (signal "")))
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4 space-y-3"
@@ -376,7 +376,7 @@
(p :class "text-sm text-stone-600 font-mono" (deref msg))))))
;; 10. Dynamic class/style — computed signals drive class and style reactively
(defisland ~demo-dynamic-class ()
(defisland ~reactive-islands/index/demo-dynamic-class ()
(let ((danger (signal false))
(size (signal 16)))
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4 space-y-3"
@@ -396,7 +396,7 @@
"This element's class and style are reactive."))))
;; 11. Resource + suspense pattern — async data with loading/error states
(defisland ~demo-resource ()
(defisland ~reactive-islands/index/demo-resource ()
(let ((data (resource (fn ()
;; Simulate async fetch with a delayed promise
(promise-delayed 1500 (dict "name" "Ada Lovelace"
@@ -418,7 +418,7 @@
(p :class "text-sm text-stone-600" (get d "role") " (" (get d "year") ")")))))))
;; 12. Transition pattern — deferred updates for expensive operations
(defisland ~demo-transition ()
(defisland ~reactive-islands/index/demo-transition ()
(let ((query (signal ""))
(all-items (list "Signals" "Effects" "Computed" "Batch" "Stores"
"Islands" "Portals" "Error Boundaries" "Resources"
@@ -454,7 +454,7 @@
(deref filtered))))))
;; 13. Shared stores — cross-island state via def-store / use-store
(defisland ~demo-store-writer ()
(defisland ~reactive-islands/index/demo-store-writer ()
(let ((store (def-store "demo-theme" (fn ()
(dict "color" (signal "violet")
"dark" (signal false))))))
@@ -472,7 +472,7 @@
:class "rounded border-stone-300")
"Dark mode")))))
(defisland ~demo-store-reader ()
(defisland ~reactive-islands/index/demo-store-reader ()
(let ((store (use-store "demo-theme")))
(div :class "rounded border border-stone-200 bg-stone-50 p-4 my-2"
(p :class "text-xs font-semibold text-stone-500 mb-2" "Island B — Store Reader")

View File

@@ -2,8 +2,8 @@
;; Marshes — where reactivity and hypermedia interpenetrate
;; ---------------------------------------------------------------------------
(defcomp ~reactive-islands-marshes-content ()
(~doc-page :title "Marshes"
(defcomp ~reactive-islands/marshes/reactive-islands-marshes-content ()
(~docs/page :title "Marshes"
(p :class "text-stone-500 text-sm italic mb-8"
"Islands are dry land. Lakes are open water. Marshes are the saturated ground between — where you can't tell whether you're standing on reactivity or wading through hypermedia.")
@@ -11,7 +11,7 @@
;; I. The problem
;; =====================================================================
(~doc-section :title "The boundary dissolves" :id "problem"
(~docs/section :title "The boundary dissolves" :id "problem"
(p "Islands and lakes establish a clear territorial agreement. Islands own reactive state — signals, computed, effects. Lakes own server content — morphed during navigation, updated by " (code "sx-get") "/" (code "sx-post") ". The morph algorithm enforces the border: it enters islands, finds lakes, updates them, and leaves everything else untouched.")
(p "But real applications need more than peaceful coexistence. They need " (em "interpenetration") ":")
(ul :class "list-disc pl-5 space-y-2 text-stone-600"
@@ -25,23 +25,23 @@
;; II. Three marsh patterns
;; =====================================================================
(~doc-section :title "Three marsh patterns" :id "patterns"
(~docs/section :title "Three marsh patterns" :id "patterns"
(p "Marshes manifest in three directions, each reversing the flow between the reactive and hypermedia worlds.")
;; -----------------------------------------------------------------
;; Pattern 1: Server → Signal
;; -----------------------------------------------------------------
(~doc-subsection :title "Pattern 1: Hypermedia writes to reactive state"
(~docs/subsection :title "Pattern 1: Hypermedia writes to reactive state"
(p "The server response carries data that should update a signal. The lake doesn't just display content — it " (em "feeds") " the island's reactive graph.")
(h4 :class "font-semibold mt-4 mb-2" "Mechanism: data-sx-signal")
(p "A server-rendered element carries a " (code "data-sx-signal") " attribute naming a store signal and its new value. When the morph processes this element, it writes to the signal instead of (or in addition to) updating the DOM.")
(~doc-code :code (highlight ";; Server response includes:\n(div :data-sx-signal \"cart-count:7\"\n (span \"7 items\"))\n\n;; The morph sees data-sx-signal, parses it:\n;; store name = \"cart-count\"\n;; value = 7\n;; Then: (reset! (use-store \"cart-count\") 7)\n;;\n;; Any island anywhere on the page that reads cart-count\n;; updates immediately — fine-grained, no re-render." "lisp"))
(~docs/code :code (highlight ";; Server response includes:\n(div :data-sx-signal \"cart-count:7\"\n (span \"7 items\"))\n\n;; The morph sees data-sx-signal, parses it:\n;; store name = \"cart-count\"\n;; value = 7\n;; Then: (reset! (use-store \"cart-count\") 7)\n;;\n;; Any island anywhere on the page that reads cart-count\n;; updates immediately — fine-grained, no re-render." "lisp"))
(h4 :class "font-semibold mt-4 mb-2" "Mechanism: sx-on-settle")
(p "An " (code "sx-on-settle") " attribute on a hypermedia trigger element. After the swap completes and the DOM settles, the SX expression is evaluated. This gives the response a chance to run arbitrary reactive logic.")
(~doc-code :code (highlight ";; A search form that updates a signal after results arrive:\n(form :sx-post \"/search\" :sx-target \"#results\"\n :sx-on-settle (reset! (use-store \"result-count\") result-count)\n (input :name \"q\" :placeholder \"Search...\"))" "lisp"))
(~docs/code :code (highlight ";; A search form that updates a signal after results arrive:\n(form :sx-post \"/search\" :sx-target \"#results\"\n :sx-on-settle (reset! (use-store \"result-count\") result-count)\n (input :name \"q\" :placeholder \"Search...\"))" "lisp"))
(h4 :class "font-semibold mt-4 mb-2" "Mechanism: event bridge (already exists)")
(p "The event bridge (" (code "data-sx-emit") ") already provides server → island communication via custom DOM events. Marshes generalise this: " (code "data-sx-signal") " is a declarative shorthand for the common case of \"server says update this value.\"")
@@ -54,12 +54,12 @@
;; Pattern 2: Server modifies reactive structure
;; -----------------------------------------------------------------
(~doc-subsection :title "Pattern 2: Hypermedia modifies reactive components"
(~docs/subsection :title "Pattern 2: Hypermedia modifies reactive components"
(p "Lake morphing lets the server update " (em "content") " inside an island. Marsh morphing goes further: the server can send new SX that the island evaluates reactively.")
(h4 :class "font-semibold mt-4 mb-2" "Mechanism: marsh tag")
(p "A " (code "marsh") " is a zone inside an island where server content is " (em "re-evaluated") " by the island's reactive evaluator, not just inserted as static DOM. When the morph updates a marsh, the new content is parsed as SX and rendered in the island's signal context.")
(~doc-code :code (highlight ";; Inside an island — a marsh re-evaluates on morph:\n(defisland ~product-card (&key product-id)\n (let ((quantity (signal 1))\n (variant (signal nil)))\n (div :class \"card\"\n ;; Lake: server content, inserted as static HTML\n (lake :id \"description\"\n (p \"Loading...\"))\n ;; Marsh: server content, evaluated with access to island signals\n (marsh :id \"controls\"\n ;; Initial content from server — has signal references:\n (div\n (select :bind variant\n (option :value \"red\" \"Red\")\n (option :value \"blue\" \"Blue\"))\n (input :type \"number\" :bind quantity))))))" "lisp"))
(~docs/code :code (highlight ";; Inside an island — a marsh re-evaluates on morph:\n(defisland ~reactive-islands/marshes/product-card (&key product-id)\n (let ((quantity (signal 1))\n (variant (signal nil)))\n (div :class \"card\"\n ;; Lake: server content, inserted as static HTML\n (lake :id \"description\"\n (p \"Loading...\"))\n ;; Marsh: server content, evaluated with access to island signals\n (marsh :id \"controls\"\n ;; Initial content from server — has signal references:\n (div\n (select :bind variant\n (option :value \"red\" \"Red\")\n (option :value \"blue\" \"Blue\"))\n (input :type \"number\" :bind quantity))))))" "lisp"))
(p "When the server sends updated marsh content (e.g., new variant options fetched from a database), the island re-evaluates it in its signal scope. The new " (code "select") " options bind to the existing " (code "variant") " signal. The reactive graph reconnects seamlessly.")
(h4 :class "font-semibold mt-4 mb-2" "Lake vs. Marsh")
@@ -95,17 +95,17 @@
;; Pattern 3: Reactive state modifies hypermedia
;; -----------------------------------------------------------------
(~doc-subsection :title "Pattern 3: Reactive state directs and transforms hypermedia"
(~docs/subsection :title "Pattern 3: Reactive state directs and transforms hypermedia"
(p "The deepest marsh pattern. Client signals don't just maintain local UI state — they control the hypermedia system itself: what to fetch, where to put it, and " (strong "how to interpret it") ".")
(h4 :class "font-semibold mt-4 mb-2" "3a: Signal-bound hypermedia attributes")
(p "Hypermedia trigger attributes (" (code "sx-get") ", " (code "sx-post") ", " (code "sx-target") ", " (code "sx-swap") ") can reference signals. The URL, target, and swap strategy become reactive.")
(~doc-code :code (highlight ";; A search input whose endpoint depends on reactive state:\n(defisland ~smart-search ()\n (let ((mode (signal \"products\"))\n (query (signal \"\")))\n (div\n ;; Mode selector — changes what we're searching\n (select :bind mode\n (option :value \"products\" \"Products\")\n (option :value \"events\" \"Events\")\n (option :value \"posts\" \"Posts\"))\n ;; Search input — endpoint changes reactively\n (input :type \"text\" :bind query\n :sx-get (computed (fn () (str \"/search/\" (deref mode) \"?q=\" (deref query))))\n :sx-trigger \"input changed delay:300ms\"\n :sx-target \"#results\")\n (div :id \"results\"))))" "lisp"))
(~docs/code :code (highlight ";; A search input whose endpoint depends on reactive state:\n(defisland ~reactive-islands/marshes/smart-search ()\n (let ((mode (signal \"products\"))\n (query (signal \"\")))\n (div\n ;; Mode selector — changes what we're searching\n (select :bind mode\n (option :value \"products\" \"Products\")\n (option :value \"events\" \"Events\")\n (option :value \"posts\" \"Posts\"))\n ;; Search input — endpoint changes reactively\n (input :type \"text\" :bind query\n :sx-get (computed (fn () (str \"/search/\" (deref mode) \"?q=\" (deref query))))\n :sx-trigger \"input changed delay:300ms\"\n :sx-target \"#results\")\n (div :id \"results\"))))" "lisp"))
(p "The " (code "sx-get") " URL isn't a static string — it's a computed signal. When the mode changes, the next search hits a different endpoint. The hypermedia trigger system reads the signal's current value at trigger time.")
(h4 :class "font-semibold mt-4 mb-2" "3b: Reactive swap transforms")
(p "A " (code "marsh-transform") " function that processes server content " (em "before") " it enters the DOM. The transform has access to island signals, so it can reshape the same server response differently based on client state.")
(~doc-code :code (highlight ";; View mode transforms how server results are displayed:\n(defisland ~result-list ()\n (let ((view (signal \"list\"))\n (sort-key (signal \"date\")))\n (div\n (div :class \"flex gap-2\"\n (button :on-click (fn (e) (reset! view \"list\")) \"List\")\n (button :on-click (fn (e) (reset! view \"grid\")) \"Grid\")\n (button :on-click (fn (e) (reset! view \"compact\")) \"Compact\"))\n ;; Marsh: server sends a result list; client transforms its rendering\n (marsh :id \"results\"\n :transform (fn (sx-content)\n (case (deref view)\n \"grid\" (wrap-grid sx-content)\n \"compact\" (compact-view sx-content)\n :else sx-content))\n ;; Initial server content\n (div :class \"space-y-2\" \"Loading...\")))))" "lisp"))
(~docs/code :code (highlight ";; View mode transforms how server results are displayed:\n(defisland ~reactive-islands/marshes/result-list ()\n (let ((view (signal \"list\"))\n (sort-key (signal \"date\")))\n (div\n (div :class \"flex gap-2\"\n (button :on-click (fn (e) (reset! view \"list\")) \"List\")\n (button :on-click (fn (e) (reset! view \"grid\")) \"Grid\")\n (button :on-click (fn (e) (reset! view \"compact\")) \"Compact\"))\n ;; Marsh: server sends a result list; client transforms its rendering\n (marsh :id \"results\"\n :transform (fn (sx-content)\n (case (deref view)\n \"grid\" (wrap-grid sx-content)\n \"compact\" (compact-view sx-content)\n :else sx-content))\n ;; Initial server content\n (div :class \"space-y-2\" \"Loading...\")))))" "lisp"))
(p "The server sends the same canonical result list every time. The " (code ":transform") " function — a reactive closure over the " (code "view") " signal — reshapes it into a grid, compact list, or default list. Change the view signal → existing content is re-transformed without a server round-trip. Fetch new results → they arrive pre-sorted, then the transform applies the current view.")
(h4 :class "font-semibold mt-4 mb-2" "3c: Reactive interpretation")
@@ -120,29 +120,29 @@
;; III. Spec primitives
;; =====================================================================
(~doc-section :title "Spec primitives" :id "primitives"
(~docs/section :title "Spec primitives" :id "primitives"
(p "Five new constructs, all specced in " (code ".sx") " files, bootstrapped to every host.")
(h4 :class "font-semibold mt-4 mb-2" "1. marsh tag")
(~doc-code :code (highlight ";; In adapter-dom.sx / adapter-html.sx / adapter-sx.sx:\n;;\n;; (marsh :id \"controls\" :transform transform-fn children...)\n;;\n;; Server: renders as <div data-sx-marsh=\"controls\">children HTML</div>\n;; Client: wraps children in reactive evaluation scope\n;; Morph: re-parses incoming SX, evaluates in island scope, replaces DOM\n;;\n;; The :transform is optional. If present, it's called on the parsed SX\n;; before evaluation. The transform has full signal access." "lisp"))
(~docs/code :code (highlight ";; In adapter-dom.sx / adapter-html.sx / adapter-sx.sx:\n;;\n;; (marsh :id \"controls\" :transform transform-fn children...)\n;;\n;; Server: renders as <div data-sx-marsh=\"controls\">children HTML</div>\n;; Client: wraps children in reactive evaluation scope\n;; Morph: re-parses incoming SX, evaluates in island scope, replaces DOM\n;;\n;; The :transform is optional. If present, it's called on the parsed SX\n;; before evaluation. The transform has full signal access." "lisp"))
(h4 :class "font-semibold mt-4 mb-2" "2. data-sx-signal (morph integration)")
(~doc-code :code (highlight ";; In engine.sx, morph-children:\n;;\n;; When processing a new element with data-sx-signal=\"name:value\":\n;; 1. Parse the attribute: store-name, signal-value\n;; 2. Look up (use-store store-name) — finds or creates the signal\n;; 3. (reset! signal parsed-value)\n;; 4. Remove the data-sx-signal attribute from DOM (consumed)\n;;\n;; Values are JSON-parsed: \"7\" → 7, '\"hello\"' → \"hello\",\n;; 'true' → true, '{...}' → dict" "lisp"))
(~docs/code :code (highlight ";; In engine.sx, morph-children:\n;;\n;; When processing a new element with data-sx-signal=\"name:value\":\n;; 1. Parse the attribute: store-name, signal-value\n;; 2. Look up (use-store store-name) — finds or creates the signal\n;; 3. (reset! signal parsed-value)\n;; 4. Remove the data-sx-signal attribute from DOM (consumed)\n;;\n;; Values are JSON-parsed: \"7\" → 7, '\"hello\"' → \"hello\",\n;; 'true' → true, '{...}' → dict" "lisp"))
(h4 :class "font-semibold mt-4 mb-2" "3. Signal-bound hypermedia attributes")
(~doc-code :code (highlight ";; In orchestration.sx, resolve-trigger-attrs:\n;;\n;; Before issuing a fetch, read sx-get/sx-post/sx-target/sx-swap.\n;; If the value is a signal or computed, deref it at trigger time.\n;;\n;; (define resolve-trigger-url\n;; (fn (el attr)\n;; (let ((val (dom-get-attr el attr)))\n;; (if (signal? val) (deref val) val))))\n;;\n;; This means the URL is evaluated lazily — it reflects the current\n;; signal state at the moment the user acts, not when the DOM was built." "lisp"))
(~docs/code :code (highlight ";; In orchestration.sx, resolve-trigger-attrs:\n;;\n;; Before issuing a fetch, read sx-get/sx-post/sx-target/sx-swap.\n;; If the value is a signal or computed, deref it at trigger time.\n;;\n;; (define resolve-trigger-url\n;; (fn (el attr)\n;; (let ((val (dom-get-attr el attr)))\n;; (if (signal? val) (deref val) val))))\n;;\n;; This means the URL is evaluated lazily — it reflects the current\n;; signal state at the moment the user acts, not when the DOM was built." "lisp"))
(h4 :class "font-semibold mt-4 mb-2" "4. marsh-transform (swap pipeline)")
(~doc-code :code (highlight ";; In orchestration.sx, process-swap:\n;;\n;; After receiving server HTML and before inserting into target:\n;; 1. Find the target element\n;; 2. If target has data-sx-marsh, find its transform function\n;; 3. Parse server content as SX\n;; 4. Call transform(sx-content) — transform is a reactive closure\n;; 5. Evaluate the transformed SX in the island's signal scope\n;; 6. Replace the marsh's DOM children\n;;\n;; The transform runs inside the island's tracking context,\n;; so computed/effect dependencies are captured automatically.\n;; When a signal the transform reads changes, the marsh re-transforms." "lisp"))
(~docs/code :code (highlight ";; In orchestration.sx, process-swap:\n;;\n;; After receiving server HTML and before inserting into target:\n;; 1. Find the target element\n;; 2. If target has data-sx-marsh, find its transform function\n;; 3. Parse server content as SX\n;; 4. Call transform(sx-content) — transform is a reactive closure\n;; 5. Evaluate the transformed SX in the island's signal scope\n;; 6. Replace the marsh's DOM children\n;;\n;; The transform runs inside the island's tracking context,\n;; so computed/effect dependencies are captured automatically.\n;; When a signal the transform reads changes, the marsh re-transforms." "lisp"))
(h4 :class "font-semibold mt-4 mb-2" "5. sx-on-settle (post-swap hook)")
(~doc-code :code (highlight ";; In orchestration.sx, after swap completes:\n;;\n;; (define process-settle-hooks\n;; (fn (trigger-el)\n;; (let ((hook (dom-get-attr trigger-el \"sx-on-settle\")))\n;; (when hook\n;; (eval-expr (parse hook) (island-env trigger-el))))))\n;;\n;; The expression is evaluated in the nearest island's environment,\n;; giving it access to signals, stores, and island-local functions." "lisp")))
(~docs/code :code (highlight ";; In orchestration.sx, after swap completes:\n;;\n;; (define process-settle-hooks\n;; (fn (trigger-el)\n;; (let ((hook (dom-get-attr trigger-el \"sx-on-settle\")))\n;; (when hook\n;; (eval-expr (parse hook) (island-env trigger-el))))))\n;;\n;; The expression is evaluated in the nearest island's environment,\n;; giving it access to signals, stores, and island-local functions." "lisp")))
;; =====================================================================
;; IV. The morph enters the marsh
;; =====================================================================
(~doc-section :title "The morph enters the marsh" :id "morph"
(~docs/section :title "The morph enters the marsh" :id "morph"
(p "The morph algorithm already handles three zones: static DOM (full reconciliation), islands (preserve reactive nodes), and lakes (update static content within islands). Marshes add a fourth:")
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
@@ -169,22 +169,22 @@
(td :class "px-3 py-2 font-mono text-sm text-stone-600" "data-sx-marsh")
(td :class "px-3 py-2 text-stone-600" "Parse new content as SX, apply transform, evaluate in island scope, replace DOM")))))
(~doc-code :code (highlight ";; Updated morph-island-children in engine.sx:\n\n(define morph-island-children\n (fn (old-island new-island)\n (let ((old-lakes (dom-query-all old-island \"[data-sx-lake]\"))\n (new-lakes (dom-query-all new-island \"[data-sx-lake]\"))\n (old-marshes (dom-query-all old-island \"[data-sx-marsh]\"))\n (new-marshes (dom-query-all new-island \"[data-sx-marsh]\")))\n ;; Build lookup maps\n (let ((new-lake-map (index-by-attr new-lakes \"data-sx-lake\"))\n (new-marsh-map (index-by-attr new-marshes \"data-sx-marsh\")))\n ;; Lakes: static DOM swap\n (for-each\n (fn (old-lake)\n (let ((id (dom-get-attr old-lake \"data-sx-lake\"))\n (new-lake (dict-get new-lake-map id)))\n (when new-lake\n (sync-attrs old-lake new-lake)\n (morph-children old-lake new-lake))))\n old-lakes)\n ;; Marshes: parse + evaluate + replace\n (for-each\n (fn (old-marsh)\n (let ((id (dom-get-attr old-marsh \"data-sx-marsh\"))\n (new-marsh (dict-get new-marsh-map id)))\n (when new-marsh\n (morph-marsh old-marsh new-marsh old-island))))\n old-marshes)\n ;; Signal updates from data-sx-signal\n (process-signal-updates new-island)))))" "lisp"))
(~docs/code :code (highlight ";; Updated morph-island-children in engine.sx:\n\n(define morph-island-children\n (fn (old-island new-island)\n (let ((old-lakes (dom-query-all old-island \"[data-sx-lake]\"))\n (new-lakes (dom-query-all new-island \"[data-sx-lake]\"))\n (old-marshes (dom-query-all old-island \"[data-sx-marsh]\"))\n (new-marshes (dom-query-all new-island \"[data-sx-marsh]\")))\n ;; Build lookup maps\n (let ((new-lake-map (index-by-attr new-lakes \"data-sx-lake\"))\n (new-marsh-map (index-by-attr new-marshes \"data-sx-marsh\")))\n ;; Lakes: static DOM swap\n (for-each\n (fn (old-lake)\n (let ((id (dom-get-attr old-lake \"data-sx-lake\"))\n (new-lake (dict-get new-lake-map id)))\n (when new-lake\n (sync-attrs old-lake new-lake)\n (morph-children old-lake new-lake))))\n old-lakes)\n ;; Marshes: parse + evaluate + replace\n (for-each\n (fn (old-marsh)\n (let ((id (dom-get-attr old-marsh \"data-sx-marsh\"))\n (new-marsh (dict-get new-marsh-map id)))\n (when new-marsh\n (morph-marsh old-marsh new-marsh old-island))))\n old-marshes)\n ;; Signal updates from data-sx-signal\n (process-signal-updates new-island)))))" "lisp"))
(~doc-code :code (highlight ";; morph-marsh: re-evaluate server content in island scope\n\n(define morph-marsh\n (fn (old-marsh new-marsh island-el)\n (let ((transform (get-marsh-transform old-marsh))\n (new-sx (dom-inner-sx new-marsh))\n (island-env (get-island-env island-el)))\n ;; Apply transform if present\n (let ((transformed (if transform (transform new-sx) new-sx)))\n ;; Dispose old reactive bindings in this marsh\n (dispose-marsh-scope old-marsh)\n ;; Evaluate the SX in island scope — creates new reactive bindings\n (with-marsh-scope old-marsh\n (let ((new-dom (render-to-dom transformed island-env)))\n (dom-replace-children old-marsh new-dom)))))))" "lisp")))
(~docs/code :code (highlight ";; morph-marsh: re-evaluate server content in island scope\n\n(define morph-marsh\n (fn (old-marsh new-marsh island-el)\n (let ((transform (get-marsh-transform old-marsh))\n (new-sx (dom-inner-sx new-marsh))\n (island-env (get-island-env island-el)))\n ;; Apply transform if present\n (let ((transformed (if transform (transform new-sx) new-sx)))\n ;; Dispose old reactive bindings in this marsh\n (dispose-marsh-scope old-marsh)\n ;; Evaluate the SX in island scope — creates new reactive bindings\n (with-marsh-scope old-marsh\n (let ((new-dom (render-to-dom transformed island-env)))\n (dom-replace-children old-marsh new-dom)))))))" "lisp")))
;; =====================================================================
;; V. Signal lifecycle in marshes
;; =====================================================================
(~doc-section :title "Signal lifecycle" :id "lifecycle"
(~docs/section :title "Signal lifecycle" :id "lifecycle"
(p "Marshes introduce a sub-scope within the island's reactive context. When a marsh is re-evaluated (morph or transform change), its old effects and computeds must be disposed without disturbing the island's own reactive graph.")
(~doc-subsection :title "Scoping"
(~doc-code :code (highlight ";; In signals.sx:\n\n(define with-marsh-scope\n (fn (marsh-el body-fn)\n ;; Create a child scope under the current island scope\n ;; All effects/computeds created during body-fn register here\n (let ((parent-scope (current-island-scope))\n (marsh-scope (create-child-scope parent-scope (dom-get-attr marsh-el \"data-sx-marsh\"))))\n (with-scope marsh-scope\n (body-fn)))))\n\n(define dispose-marsh-scope\n (fn (marsh-el)\n ;; Dispose all effects/computeds registered in this marsh's scope\n ;; Parent island scope and sibling marshes are unaffected\n (let ((scope (get-marsh-scope marsh-el)))\n (when scope (dispose-scope scope)))))" "lisp"))
(~docs/subsection :title "Scoping"
(~docs/code :code (highlight ";; In signals.sx:\n\n(define with-marsh-scope\n (fn (marsh-el body-fn)\n ;; Create a child scope under the current island scope\n ;; All effects/computeds created during body-fn register here\n (let ((parent-scope (current-island-scope))\n (marsh-scope (create-child-scope parent-scope (dom-get-attr marsh-el \"data-sx-marsh\"))))\n (with-scope marsh-scope\n (body-fn)))))\n\n(define dispose-marsh-scope\n (fn (marsh-el)\n ;; Dispose all effects/computeds registered in this marsh's scope\n ;; Parent island scope and sibling marshes are unaffected\n (let ((scope (get-marsh-scope marsh-el)))\n (when scope (dispose-scope scope)))))" "lisp"))
(p "The scoping hierarchy: " (strong "island") " → " (strong "marsh") " → " (strong "effects/computeds") ". Disposing a marsh disposes its subscope. Disposing an island disposes all its marshes. The signal graph is a tree, not a flat list."))
(~doc-subsection :title "Reactive transforms"
(~docs/subsection :title "Reactive transforms"
(p "When a marsh has a " (code ":transform") " function, the transform itself is an effect. It reads signals (via " (code "deref") " inside the transform body) and produces transformed SX. When those signals change, the transform re-runs, the marsh re-evaluates, and the DOM updates — all without a server round-trip.")
(p "The transform effect belongs to the marsh scope, so it's automatically disposed when the marsh is morphed with new content.")))
@@ -192,22 +192,22 @@
;; VI. Reactive interpretation — the deep end
;; =====================================================================
(~doc-section :title "Reactive interpretation" :id "interpretation"
(~docs/section :title "Reactive interpretation" :id "interpretation"
(p "The deepest marsh pattern isn't about transforming content — it's about transforming the " (em "rules") ". Reactive state modifies how the hypermedia system itself operates.")
(~doc-subsection :title "Swap strategy as signal"
(~docs/subsection :title "Swap strategy as signal"
(p "The same server response inserted differently based on client state:")
(~doc-code :code (highlight ";; Chat app: append messages normally, morph when switching threads\n(defisland ~chat ()\n (let ((mode (signal \"live\")))\n (div\n (div :sx-get \"/messages/latest\"\n :sx-trigger \"every 2s\"\n :sx-target \"#messages\"\n :sx-swap (computed (fn ()\n (if (= (deref mode) \"live\") \"beforeend\" \"innerHTML\")))\n (div :id \"messages\")))))" "lisp"))
(~docs/code :code (highlight ";; Chat app: append messages normally, morph when switching threads\n(defisland ~reactive-islands/marshes/chat ()\n (let ((mode (signal \"live\")))\n (div\n (div :sx-get \"/messages/latest\"\n :sx-trigger \"every 2s\"\n :sx-target \"#messages\"\n :sx-swap (computed (fn ()\n (if (= (deref mode) \"live\") \"beforeend\" \"innerHTML\")))\n (div :id \"messages\")))))" "lisp"))
(p "In " (code "\"live\"") " mode, new messages append. Switch to thread view — the same polling endpoint now replaces the whole list. The server doesn't change. The client's reactive state changes the " (em "semantics") " of the swap."))
(~doc-subsection :title "URL rewriting as signal"
(~docs/subsection :title "URL rewriting as signal"
(p "Reactive state transparently modifies request URLs:")
(~doc-code :code (highlight ";; Locale prefix — the server sees /fr/products, /en/products, etc.\n;; The author writes /products — the marsh layer prepends the locale.\n(def-store \"locale\" \"en\")\n\n;; In orchestration.sx, resolve-trigger-url:\n(define resolve-trigger-url\n (fn (el attr)\n (let ((raw (dom-get-attr el attr))\n (locale (deref (use-store \"locale\"))))\n (if (and locale (not (starts-with? raw (str \"/\" locale))))\n (str \"/\" locale raw)\n raw))))" "lisp"))
(~docs/code :code (highlight ";; Locale prefix — the server sees /fr/products, /en/products, etc.\n;; The author writes /products — the marsh layer prepends the locale.\n(def-store \"locale\" \"en\")\n\n;; In orchestration.sx, resolve-trigger-url:\n(define resolve-trigger-url\n (fn (el attr)\n (let ((raw (dom-get-attr el attr))\n (locale (deref (use-store \"locale\"))))\n (if (and locale (not (starts-with? raw (str \"/\" locale))))\n (str \"/\" locale raw)\n raw))))" "lisp"))
(p "Every " (code "sx-get") " and " (code "sx-post") " URL passes through the resolver. A locale signal, a preview-mode signal, an A/B-test signal — any reactive state can transparently rewrite the request the server sees."))
(~doc-subsection :title "Content rewriting as signal"
(~docs/subsection :title "Content rewriting as signal"
(p "Incoming server HTML passes through a reactive filter before insertion:")
(~doc-code :code (highlight ";; Dark mode — rewrites server classes before insertion\n(def-store \"theme\" \"light\")\n\n;; In orchestration.sx, after receiving server HTML:\n(define apply-theme-transform\n (fn (html-str)\n (if (= (deref (use-store \"theme\")) \"dark\")\n (-> html-str\n (replace-all \"bg-white\" \"bg-stone-900\")\n (replace-all \"text-stone-800\" \"text-stone-100\")\n (replace-all \"border-stone-200\" \"border-stone-700\"))\n html-str)))" "lisp"))
(~docs/code :code (highlight ";; Dark mode — rewrites server classes before insertion\n(def-store \"theme\" \"light\")\n\n;; In orchestration.sx, after receiving server HTML:\n(define apply-theme-transform\n (fn (html-str)\n (if (= (deref (use-store \"theme\")) \"dark\")\n (-> html-str\n (replace-all \"bg-white\" \"bg-stone-900\")\n (replace-all \"text-stone-800\" \"text-stone-100\")\n (replace-all \"border-stone-200\" \"border-stone-700\"))\n html-str)))" "lisp"))
(p "The server renders canonical light-mode HTML. The client's theme signal rewrites it at the edge. No server-side theme support needed. No separate dark-mode templates. The same document, different interpretation.")
(p "This is the Hegelian deepening: the reactive state isn't just " (em "alongside") " the hypermedia content. It " (em "constitutes the lens through which the content is perceived") ". The marsh isn't a zone in the DOM — it's a layer in the interpretation pipeline.")))
@@ -215,7 +215,7 @@
;; VII. Implementation order
;; =====================================================================
(~doc-section :title "Implementation order" :id "implementation"
(~docs/section :title "Implementation order" :id "implementation"
(p "Spec-first, bootstrap-second, like everything else.")
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
@@ -256,7 +256,7 @@
;; VIII. Design principles
;; =====================================================================
(~doc-section :title "Design principles" :id "principles"
(~docs/section :title "Design principles" :id "principles"
(ol :class "space-y-3 text-stone-600 list-decimal list-inside"
(li (strong "Marshes are opt-in per zone.") " " (code "lake") " remains the default for server content inside islands. " (code "marsh") " is for the zones that need reactive re-evaluation. Don't use a marsh where a lake suffices.")
(li (strong "The server doesn't need to know.") " Marsh transforms, signal-bound URLs, reactive interpretation — these are client-side concerns. The server sends canonical content. The client's reactive state shapes how it arrives. The server remains simple.")
@@ -268,7 +268,7 @@
;; IX. The dialectic continues
;; =====================================================================
(~doc-section :title "The dialectic continues" :id "dialectic"
(~docs/section :title "The dialectic continues" :id "dialectic"
(p "Islands separated client state from server content. Lakes let server content flow through islands. Marshes dissolve the boundary entirely — the same zone is simultaneously server-authored and reactively interpreted.")
(p "This is the next turn of the Hegelian spiral. The thesis (pure hypermedia) posited the server as sole authority. The antithesis (reactive islands) gave the client its own inner life. The first synthesis (islands + lakes) maintained the boundary between them. The second synthesis (marshes) " (em "sublates the boundary itself") ".")
(p "In a marsh, you can't point to a piece of DOM and say \"this is server territory\" or \"this is client territory.\" It's both. The server sent it. The client transformed it. The server can update it. The client will re-transform it. The signal reads the server data. The server data feeds the signal. Subject and substance are one.")
@@ -278,22 +278,22 @@
;; X. Live demos
;; =====================================================================
(~doc-section :title "Live demos" :id "demos"
(~docs/section :title "Live demos" :id "demos"
(p (strong "These are live interactive islands") " — not static code snippets. Click the buttons. Inspect the DOM.")
;; -----------------------------------------------------------------
;; Demo 1: Server content feeds reactive state
;; -----------------------------------------------------------------
(~doc-subsection :title "Demo 1: Hypermedia feeds reactive state"
(~docs/subsection :title "Demo 1: Hypermedia feeds reactive state"
(p "Click \"Fetch Price\" to hit a real server endpoint. The response is " (em "hypermedia") " — SX content swapped into the page. But a " (code "data-init") " script in the response also writes to the " (code "\"demo-price\"") " store signal. The island's reactive UI — total, savings, price display — updates instantly from the signal change.")
(p "This is the marsh pattern: " (strong "the server response is both content and a signal write") ". Hypermedia and reactivity aren't separate — the same response does both.")
(~demo-marsh-product)
(~reactive-islands/marshes/demo-marsh-product)
(~doc-code :code (highlight ";; Island with a store-backed price signal\n(defisland ~demo-marsh-product ()\n (let ((price (def-store \"demo-price\" (fn () (signal 19.99))))\n (qty (signal 1))\n (total (computed (fn () (* (deref price) (deref qty))))))\n (div\n ;; Reactive price display — updates when store changes\n (span \"$\" (deref price))\n (span \"Qty:\") (button \"-\") (span (deref qty)) (button \"+\")\n (span \"Total: $\" (deref total))\n\n ;; Fetch from server — response arrives as hypermedia\n (button :sx-get \"/sx/(geography.(reactive.(api.flash-sale)))\"\n :sx-target \"#marsh-server-msg\"\n :sx-swap \"innerHTML\"\n \"Fetch Price\")\n ;; Server response lands here:\n (div :id \"marsh-server-msg\"))))" "lisp"))
(~docs/code :code (highlight ";; Island with a store-backed price signal\n(defisland ~reactive-islands/marshes/demo-marsh-product ()\n (let ((price (def-store \"demo-price\" (fn () (signal 19.99))))\n (qty (signal 1))\n (total (computed (fn () (* (deref price) (deref qty))))))\n (div\n ;; Reactive price display — updates when store changes\n (span \"$\" (deref price))\n (span \"Qty:\") (button \"-\") (span (deref qty)) (button \"+\")\n (span \"Total: $\" (deref total))\n\n ;; Fetch from server — response arrives as hypermedia\n (button :sx-get \"/sx/(geography.(reactive.(api.flash-sale)))\"\n :sx-target \"#marsh-server-msg\"\n :sx-swap \"innerHTML\"\n \"Fetch Price\")\n ;; Server response lands here:\n (div :id \"marsh-server-msg\"))))" "lisp"))
(~doc-code :code (highlight ";; Server returns SX content + a data-init script:\n;;\n;; (<>\n;; (p \"Flash sale! Price: $14.99\")\n;; (script :type \"text/sx\" :data-init\n;; \"(reset! (use-store \\\"demo-price\\\") 14.99)\"))\n;;\n;; The <p> is swapped in as normal hypermedia content.\n;; The script writes to the store signal.\n;; The island's (deref price), total, and savings\n;; all update reactively — no re-render, no diffing." "lisp"))
(~docs/code :code (highlight ";; Server returns SX content + a data-init script:\n;;\n;; (<>\n;; (p \"Flash sale! Price: $14.99\")\n;; (script :type \"text/sx\" :data-init\n;; \"(reset! (use-store \\\"demo-price\\\") 14.99)\"))\n;;\n;; The <p> is swapped in as normal hypermedia content.\n;; The script writes to the store signal.\n;; The island's (deref price), total, and savings\n;; all update reactively — no re-render, no diffing." "lisp"))
(p "Two things happen from one server response: content appears in the swap target (hypermedia) and the price signal updates (reactivity). The island didn't fetch the price. The server didn't call a signal API. The response " (em "is") " both."))
@@ -301,12 +301,12 @@
;; Demo 2: Server → Signal (simulated + live)
;; -----------------------------------------------------------------
(~doc-subsection :title "Demo 2: Server writes to signals"
(~docs/subsection :title "Demo 2: Server writes to signals"
(p "Two separate islands share a named store " (code "\"demo-price\"") ". Island A creates the store and has control buttons. Island B reads it. Signal changes propagate instantly across island boundaries.")
(div :class "space-y-3"
(~demo-marsh-store-writer)
(~demo-marsh-store-reader))
(~reactive-islands/marshes/demo-marsh-store-writer)
(~reactive-islands/marshes/demo-marsh-store-reader))
(p :class "mt-3 text-sm text-stone-500" "The \"Flash Sale\" buttons call " (code "(reset! price 14.99)") " — exactly what " (code "data-sx-signal=\"demo-price:14.99\"") " does during morph.")
@@ -320,7 +320,7 @@
:sx-swap "innerHTML"
"Fetch from server"))
(~doc-code :code (highlight ";; Island A — creates the store, has control buttons\n(defisland ~demo-marsh-store-writer ()\n (let ((price (def-store \"demo-price\" (fn () (signal 19.99)))))\n ;; (reset! price 14.99) is what data-sx-signal does during morph\n (button :on-click (fn (e) (reset! price 14.99))\n \"Flash Sale $14.99\")))\n\n;; Island B — reads the same store, different island\n(defisland ~demo-marsh-store-reader ()\n (let ((price (def-store \"demo-price\" (fn () (signal 19.99)))))\n (span \"$\" (deref price))))\n\n;; Server returns: data-sx-signal writes to the store during morph\n;; (div :data-sx-signal \"demo-price:14.99\"\n;; (p \"Flash sale! Price updated.\"))" "lisp"))
(~docs/code :code (highlight ";; Island A — creates the store, has control buttons\n(defisland ~reactive-islands/marshes/demo-marsh-store-writer ()\n (let ((price (def-store \"demo-price\" (fn () (signal 19.99)))))\n ;; (reset! price 14.99) is what data-sx-signal does during morph\n (button :on-click (fn (e) (reset! price 14.99))\n \"Flash Sale $14.99\")))\n\n;; Island B — reads the same store, different island\n(defisland ~reactive-islands/marshes/demo-marsh-store-reader ()\n (let ((price (def-store \"demo-price\" (fn () (signal 19.99)))))\n (span \"$\" (deref price))))\n\n;; Server returns: data-sx-signal writes to the store during morph\n;; (div :data-sx-signal \"demo-price:14.99\"\n;; (p \"Flash sale! Price updated.\"))" "lisp"))
(p "In production, the server response includes " (code "data-sx-signal=\"demo-price:14.99\"") ". The morph algorithm processes this attribute, calls " (code "(reset! (use-store \"demo-price\") 14.99)") ", and removes the attribute from the DOM. Every island reading that store updates instantly — fine-grained, no re-render."))
@@ -328,13 +328,13 @@
;; Demo 3: sx-on-settle — post-swap SX evaluation
;; -----------------------------------------------------------------
(~doc-subsection :title "Demo 3: sx-on-settle"
(~docs/subsection :title "Demo 3: sx-on-settle"
(p "After a swap settles, the trigger element's " (code "sx-on-settle") " attribute is parsed and evaluated as SX. This runs " (em "after") " the content is in the DOM — so you can update reactive state based on what the server returned.")
(p "Click \"Fetch Item\" to load server content. The response is pure hypermedia. But " (code "sx-on-settle") " on the button increments a fetch counter signal " (em "after") " the swap. The counter updates reactively.")
(~demo-marsh-settle)
(~reactive-islands/marshes/demo-marsh-settle)
(~doc-code :code (highlight ";; sx-on-settle runs SX after the swap settles\n(defisland ~demo-marsh-settle ()\n (let ((count (def-store \"settle-count\" (fn () (signal 0)))))\n (div\n ;; Reactive counter — updates from sx-on-settle\n (span \"Fetched: \" (deref count) \" times\")\n\n ;; Button with sx-on-settle hook\n (button :sx-get \"/sx/(geography.(reactive.(api.settle-data)))\"\n :sx-target \"#settle-result\"\n :sx-swap \"innerHTML\"\n :sx-on-settle \"(swap! (use-store \\\"settle-count\\\") inc)\"\n \"Fetch Item\")\n\n ;; Server content lands here (pure hypermedia)\n (div :id \"settle-result\"))))" "lisp"))
(~docs/code :code (highlight ";; sx-on-settle runs SX after the swap settles\n(defisland ~reactive-islands/marshes/demo-marsh-settle ()\n (let ((count (def-store \"settle-count\" (fn () (signal 0)))))\n (div\n ;; Reactive counter — updates from sx-on-settle\n (span \"Fetched: \" (deref count) \" times\")\n\n ;; Button with sx-on-settle hook\n (button :sx-get \"/sx/(geography.(reactive.(api.settle-data)))\"\n :sx-target \"#settle-result\"\n :sx-swap \"innerHTML\"\n :sx-on-settle \"(swap! (use-store \\\"settle-count\\\") inc)\"\n \"Fetch Item\")\n\n ;; Server content lands here (pure hypermedia)\n (div :id \"settle-result\"))))" "lisp"))
(p "The server knows nothing about signals or counters. It returns plain content. The " (code "sx-on-settle") " hook is a client-side concern — it runs in the global SX environment with access to all primitives."))
@@ -342,13 +342,13 @@
;; Demo 4: Signal-bound triggers
;; -----------------------------------------------------------------
(~doc-subsection :title "Demo 4: Signal-bound triggers"
(~docs/subsection :title "Demo 4: Signal-bound triggers"
(p "Inside an island, " (em "all") " attributes are reactive — including " (code "sx-get") ". When an attribute value contains " (code "deref") ", the DOM adapter wraps it in an effect that re-sets the attribute when signals change.")
(p "Select a search category. The " (code "sx-get") " URL on the search button changes reactively. Click \"Search\" to fetch from the current endpoint. The URL was computed from the " (code "mode") " signal at render time and updates whenever the mode changes.")
(~demo-marsh-signal-url)
(~reactive-islands/marshes/demo-marsh-signal-url)
(~doc-code :code (highlight ";; sx-get URL computed from a signal\n(defisland ~demo-marsh-signal-url ()\n (let ((mode (signal \"products\"))\n (query (signal \"\")))\n (div\n ;; Mode selector — changes what we're searching\n (div :class \"flex gap-2\"\n (button :on-click (fn (e) (reset! mode \"products\"))\n :class (computed (fn () ...active-class...))\n \"Products\")\n (button :on-click (fn (e) (reset! mode \"events\")) \"Events\")\n (button :on-click (fn (e) (reset! mode \"posts\")) \"Posts\"))\n\n ;; Search button — URL is a computed expression\n (button :sx-get (computed (fn ()\n (str \"/sx/(geography.(reactive.(api.search-\"\n (deref mode) \")))\" \"?q=\" (deref query))))\n :sx-target \"#signal-results\"\n :sx-swap \"innerHTML\"\n \"Search\")\n\n (div :id \"signal-results\"))))" "lisp"))
(~docs/code :code (highlight ";; sx-get URL computed from a signal\n(defisland ~reactive-islands/marshes/demo-marsh-signal-url ()\n (let ((mode (signal \"products\"))\n (query (signal \"\")))\n (div\n ;; Mode selector — changes what we're searching\n (div :class \"flex gap-2\"\n (button :on-click (fn (e) (reset! mode \"products\"))\n :class (computed (fn () ...active-class...))\n \"Products\")\n (button :on-click (fn (e) (reset! mode \"events\")) \"Events\")\n (button :on-click (fn (e) (reset! mode \"posts\")) \"Posts\"))\n\n ;; Search button — URL is a computed expression\n (button :sx-get (computed (fn ()\n (str \"/sx/(geography.(reactive.(api.search-\"\n (deref mode) \")))\" \"?q=\" (deref query))))\n :sx-target \"#signal-results\"\n :sx-swap \"innerHTML\"\n \"Search\")\n\n (div :id \"signal-results\"))))" "lisp"))
(p "No custom plumbing. The same " (code "reactive-attr") " mechanism that makes " (code ":class") " reactive also makes " (code ":sx-get") " reactive. " (code "get-verb-info") " reads " (code "dom-get-attr") " at trigger time — it sees the current URL because the effect already updated the DOM attribute."))
@@ -356,12 +356,12 @@
;; Demo 5: Reactive view transform
;; -----------------------------------------------------------------
(~doc-subsection :title "Demo 5: Reactive view transform"
(~docs/subsection :title "Demo 5: Reactive view transform"
(p "A view-mode signal controls how items are displayed. Click \"Fetch Catalog\" to load items from the server, then toggle the view mode. The " (em "same") " data re-renders differently based on client state — no server round-trip for view changes.")
(~demo-marsh-view-transform)
(~reactive-islands/marshes/demo-marsh-view-transform)
(~doc-code :code (highlight ";; View mode transforms display without refetch\n(defisland ~demo-marsh-view-transform ()\n (let ((view (signal \"list\"))\n (items (signal nil)))\n (div\n ;; View toggle\n (div :class \"flex gap-2\"\n (button :on-click (fn (e) (reset! view \"list\")) \"List\")\n (button :on-click (fn (e) (reset! view \"grid\")) \"Grid\")\n (button :on-click (fn (e) (reset! view \"compact\")) \"Compact\"))\n\n ;; Fetch from server — stores raw data in signal\n (button :sx-get \"/sx/(geography.(reactive.(api.catalog)))\"\n :sx-target \"#catalog-raw\"\n :sx-swap \"innerHTML\"\n \"Fetch Catalog\")\n\n ;; Raw server content (hidden, used as data source)\n (div :id \"catalog-raw\" :class \"hidden\")\n\n ;; Reactive display — re-renders when view changes\n (div (computed (fn () (render-view (deref view) (deref items))))))))" "lisp"))
(~docs/code :code (highlight ";; View mode transforms display without refetch\n(defisland ~reactive-islands/marshes/demo-marsh-view-transform ()\n (let ((view (signal \"list\"))\n (items (signal nil)))\n (div\n ;; View toggle\n (div :class \"flex gap-2\"\n (button :on-click (fn (e) (reset! view \"list\")) \"List\")\n (button :on-click (fn (e) (reset! view \"grid\")) \"Grid\")\n (button :on-click (fn (e) (reset! view \"compact\")) \"Compact\"))\n\n ;; Fetch from server — stores raw data in signal\n (button :sx-get \"/sx/(geography.(reactive.(api.catalog)))\"\n :sx-target \"#catalog-raw\"\n :sx-swap \"innerHTML\"\n \"Fetch Catalog\")\n\n ;; Raw server content (hidden, used as data source)\n (div :id \"catalog-raw\" :class \"hidden\")\n\n ;; Reactive display — re-renders when view changes\n (div (computed (fn () (render-view (deref view) (deref items))))))))" "lisp"))
(p "The view signal doesn't just toggle CSS classes — it fundamentally reshapes the DOM. List view shows description. Grid view arranges in columns. Compact view shows names only. All from the same server data, transformed by client state."))
@@ -373,7 +373,7 @@
;; ===========================================================================
;; Demo 1: Hypermedia feeds reactive state
(defisland ~demo-marsh-product ()
(defisland ~reactive-islands/marshes/demo-marsh-product ()
(let ((price (def-store "demo-price" (fn () (signal 19.99))))
(qty (signal 1))
(total (computed (fn () (* (deref price) (deref qty)))))
@@ -418,7 +418,7 @@
;; Demo 2: Shared store — simulates data-sx-signal
(defisland ~demo-marsh-store-writer ()
(defisland ~reactive-islands/marshes/demo-marsh-store-writer ()
(let ((price (def-store "demo-price" (fn () (signal 19.99)))))
(div :class "rounded-lg border border-amber-200 bg-amber-50 p-4"
(div :class "flex items-center justify-between mb-3"
@@ -443,7 +443,7 @@
"Each button calls " (code "(reset! price ...)") " — simulating " (code "data-sx-signal") " during morph."))))
(defisland ~demo-marsh-store-reader ()
(defisland ~reactive-islands/marshes/demo-marsh-store-reader ()
(let ((price (def-store "demo-price" (fn () (signal 19.99)))))
(div :class "rounded-lg border border-emerald-200 bg-emerald-50 p-4"
(div :class "flex items-center justify-between"
@@ -458,7 +458,7 @@
;; Demo 3: sx-on-settle — post-swap hook
(defisland ~demo-marsh-settle ()
(defisland ~reactive-islands/marshes/demo-marsh-settle ()
(let ((count (def-store "settle-count" (fn () (signal 0)))))
(div :class "rounded-lg border border-stone-200 bg-white p-4 my-4 space-y-3"
(div :class "flex items-center justify-between"
@@ -484,7 +484,7 @@
;; Demo 4: Signal-bound URL
(defisland ~demo-marsh-signal-url ()
(defisland ~reactive-islands/marshes/demo-marsh-signal-url ()
(let ((mode (signal "products"))
(query (signal "")))
(div :class "rounded-lg border border-stone-200 bg-white p-4 my-4 space-y-3"
@@ -533,7 +533,7 @@
;; Demo 5: Reactive view transform
;; The server sends structured data via data-init that writes to a store signal.
;; The view mode signal controls how the data is rendered — no refetch needed.
(defisland ~demo-marsh-view-transform ()
(defisland ~reactive-islands/marshes/demo-marsh-view-transform ()
(let ((view (signal "list"))
(items (def-store "catalog-items" (fn () (signal (list))))))
(div :class "rounded-lg border border-stone-200 bg-white p-4 my-4 space-y-3"

View File

@@ -2,10 +2,10 @@
;; Named Stores — page-level signal containers
;; ---------------------------------------------------------------------------
(defcomp ~reactive-islands-named-stores-content ()
(~doc-page :title "Named Stores"
(defcomp ~reactive-islands/named-stores/reactive-islands-named-stores-content ()
(~docs/page :title "Named Stores"
(~doc-section :title "The Problem" :id "problem"
(~docs/section :title "The Problem" :id "problem"
(p "Islands are isolated by default. Signal props work when islands are adjacent, but not when they are:")
(ul :class "space-y-1 text-stone-600 list-disc pl-5"
(li "Distant in the DOM tree (header badge + drawer island)")
@@ -13,12 +13,12 @@
(li "Destroyed and recreated by htmx swaps"))
(p "Named stores solve all three. A store is a named collection of signals that lives at " (em "page") " scope, not island scope."))
(~doc-section :title "def-store / use-store" :id "api"
(~doc-code :code (highlight ";; Create a named store — called once at page level\n;; The init function creates signals and computeds\n(def-store \"cart\" (fn ()\n (let ((items (signal (list))))\n (dict\n :items items\n :count (computed (fn () (length (deref items))))\n :total (computed (fn () (reduce + 0\n (map (fn (i) (get i \"price\")) (deref items)))))))))\n\n;; Use the store from any island — returns the signal dict\n(defisland ~cart-badge ()\n (let ((store (use-store \"cart\")))\n (span :class \"badge bg-violet-100 text-violet-800 px-2 py-1 rounded-full\"\n (deref (get store \"count\")))))\n\n(defisland ~cart-drawer ()\n (let ((store (use-store \"cart\")))\n (div :class \"p-4\"\n (h2 \"Cart\")\n (ul (map (fn (item)\n (li :class \"flex justify-between py-1\"\n (span (get item \"name\"))\n (span :class \"text-stone-500\" \"\\u00A3\" (get item \"price\"))))\n (deref (get store \"items\"))))\n (div :class \"border-t pt-2 font-semibold\"\n \"Total: \\u00A3\" (deref (get store \"total\"))))))" "lisp"))
(~docs/section :title "def-store / use-store" :id "api"
(~docs/code :code (highlight ";; Create a named store — called once at page level\n;; The init function creates signals and computeds\n(def-store \"cart\" (fn ()\n (let ((items (signal (list))))\n (dict\n :items items\n :count (computed (fn () (length (deref items))))\n :total (computed (fn () (reduce + 0\n (map (fn (i) (get i \"price\")) (deref items)))))))))\n\n;; Use the store from any island — returns the signal dict\n(defisland ~reactive-islands/named-stores/cart-badge ()\n (let ((store (use-store \"cart\")))\n (span :class \"badge bg-violet-100 text-violet-800 px-2 py-1 rounded-full\"\n (deref (get store \"count\")))))\n\n(defisland ~reactive-islands/named-stores/cart-drawer ()\n (let ((store (use-store \"cart\")))\n (div :class \"p-4\"\n (h2 \"Cart\")\n (ul (map (fn (item)\n (li :class \"flex justify-between py-1\"\n (span (get item \"name\"))\n (span :class \"text-stone-500\" \"\\u00A3\" (get item \"price\"))))\n (deref (get store \"items\"))))\n (div :class \"border-t pt-2 font-semibold\"\n \"Total: \\u00A3\" (deref (get store \"total\"))))))" "lisp"))
(p (code "def-store") " is " (strong "idempotent") " — calling it again with the same name returns the existing store. This means multiple components can call " (code "def-store") " defensively without double-creating."))
(~doc-section :title "Lifecycle" :id "lifecycle"
(~docs/section :title "Lifecycle" :id "lifecycle"
(ol :class "space-y-2 text-stone-600 list-decimal list-inside"
(li (strong "Page load: ") (code "def-store") " creates the store in a global registry. Signals are initialized.")
(li (strong "Island hydration: ") "Each island calls " (code "use-store") " to get the shared signal dict. Derefs create subscriptions.")
@@ -26,14 +26,14 @@
(li (strong "Island recreation: ") "The new island calls " (code "use-store") " again. Gets the same signals. Reconnects reactively. User state is preserved.")
(li (strong "Full page navigation: ") (code "clear-stores") " wipes the registry. Clean slate.")))
(~doc-section :title "Combining with event bridge" :id "combined"
(~docs/section :title "Combining with event bridge" :id "combined"
(p "Named stores + event bridge = full lake→island→island communication:")
(~doc-code :code (highlight ";; Store persists across island lifecycle\n(def-store \"cart\" (fn () ...))\n\n;; Island 1: product page with htmx lake\n(defisland ~product-island ()\n (let ((store (use-store \"cart\")))\n ;; Bridge server-rendered \"Add\" buttons to store\n (bridge-event container \"cart:add\" (get store \"items\")\n (fn (detail) (append (deref (get store \"items\")) detail)))\n ;; Lake content swapped via sx-get\n (div :id \"product-content\" :sx-get \"/products/featured\")))\n\n;; Island 2: cart badge in header (distant in DOM)\n(defisland ~cart-badge ()\n (let ((store (use-store \"cart\")))\n (span (deref (get store \"count\")))))" "lisp"))
(~docs/code :code (highlight ";; Store persists across island lifecycle\n(def-store \"cart\" (fn () ...))\n\n;; Island 1: product page with htmx lake\n(defisland ~reactive-islands/named-stores/product-island ()\n (let ((store (use-store \"cart\")))\n ;; Bridge server-rendered \"Add\" buttons to store\n (bridge-event container \"cart:add\" (get store \"items\")\n (fn (detail) (append (deref (get store \"items\")) detail)))\n ;; Lake content swapped via sx-get\n (div :id \"product-content\" :sx-get \"/products/featured\")))\n\n;; Island 2: cart badge in header (distant in DOM)\n(defisland ~reactive-islands/named-stores/cart-badge ()\n (let ((store (use-store \"cart\")))\n (span (deref (get store \"count\")))))" "lisp"))
(p "User clicks \"Add to Cart\" in server-rendered product content. " (code "cart:add") " event fires. Product island catches it via bridge. Store's " (code "items") " signal updates. Cart badge — in a completely different island — updates reactively because it reads the same signal."))
(~doc-section :title "Spec" :id "spec"
(~docs/section :title "Spec" :id "spec"
(p "Named stores are spec'd in " (code "signals.sx") " (section 12). Three functions:")
(div :class "overflow-x-auto rounded border border-stone-200"

View File

@@ -2,10 +2,10 @@
;; Phase 2 Plan — remaining reactive features
;; ---------------------------------------------------------------------------
(defcomp ~reactive-islands-phase2-content ()
(~doc-page :title "Phase 2: Completing the Reactive Toolkit"
(defcomp ~reactive-islands/phase2/reactive-islands-phase2-content ()
(~docs/page :title "Phase 2: Completing the Reactive Toolkit"
(~doc-section :title "Where we are" :id "where"
(~docs/section :title "Where we are" :id "where"
(p "Phase 1 delivered the core reactive primitive: signals, effects, computed values, islands, disposal, stores, event bridges, and reactive DOM rendering. These are sufficient for any isolated interactive widget.")
(p "Phase 2 fills the gaps that appear when you try to build " (em "real application UI") " with islands — forms, modals, dynamic styling, efficient lists, error handling, and async loading. Each feature is independently valuable and independently shippable. None requires changes to the signal runtime.")
@@ -52,13 +52,13 @@
;; P0 — must have
;; -----------------------------------------------------------------------
(~doc-section :title "P0: Input Binding" :id "input-binding"
(~docs/section :title "P0: Input Binding" :id "input-binding"
(p "You cannot build a form without two-way input binding. React uses controlled components — value is always driven by state, onChange feeds back. SX needs the same pattern but with signals instead of setState.")
(~doc-subsection :title "Design"
(~docs/subsection :title "Design"
(p "A new " (code ":bind") " attribute on " (code "input") ", " (code "textarea") ", and " (code "select") " elements. It takes a signal and creates a bidirectional link: signal value flows into the element, user input flows back into the signal.")
(~doc-code :code (highlight ";; Bind a signal to an input\n(defisland ~login-form ()\n (let ((email (signal \"\"))\n (password (signal \"\")))\n (form :on-submit (fn (e)\n (dom-prevent-default e)\n (fetch-json \"POST\" \"/api/login\"\n (dict \"email\" (deref email)\n \"password\" (deref password))))\n (input :type \"email\" :bind email\n :placeholder \"Email\")\n (input :type \"password\" :bind password\n :placeholder \"Password\")\n (button :type \"submit\" \"Log in\"))))" "lisp"))
(~docs/code :code (highlight ";; Bind a signal to an input\n(defisland ~reactive-islands/phase2/login-form ()\n (let ((email (signal \"\"))\n (password (signal \"\")))\n (form :on-submit (fn (e)\n (dom-prevent-default e)\n (fetch-json \"POST\" \"/api/login\"\n (dict \"email\" (deref email)\n \"password\" (deref password))))\n (input :type \"email\" :bind email\n :placeholder \"Email\")\n (input :type \"password\" :bind password\n :placeholder \"Password\")\n (button :type \"submit\" \"Log in\"))))" "lisp"))
(p "The " (code ":bind") " attribute is handled in " (code "adapter-dom.sx") "'s element rendering. For a signal " (code "s") ":")
(ol :class "space-y-1 text-stone-600 list-decimal list-inside text-sm"
@@ -68,12 +68,12 @@
(li "For checkboxes/radios: bind to " (code "checked") " instead of " (code "value"))
(li "For select: bind to " (code "value") ", handle " (code "change") " event")))
(~doc-subsection :title "Spec changes"
(~doc-code :code (highlight ";; In adapter-dom.sx, inside render-dom-element:\n;; After processing :on-* event attrs, check for :bind\n(when (dict-has? kwargs \"bind\")\n (let ((sig (dict-get kwargs \"bind\")))\n (when (signal? sig)\n (bind-input el sig))))\n\n;; New function in adapter-dom.sx:\n(define bind-input\n (fn (el sig)\n (let ((tag (lower (dom-tag-name el)))\n (is-checkbox (or (= (dom-get-attr el \"type\") \"checkbox\")\n (= (dom-get-attr el \"type\") \"radio\"))))\n ;; Set initial value\n (if is-checkbox\n (dom-set-prop el \"checked\" (deref sig))\n (dom-set-prop el \"value\" (str (deref sig))))\n ;; Signal → element (effect, auto-tracked)\n (effect (fn ()\n (if is-checkbox\n (dom-set-prop el \"checked\" (deref sig))\n (let ((v (str (deref sig))))\n (when (!= (dom-get-prop el \"value\") v)\n (dom-set-prop el \"value\" v))))))\n ;; Element → signal (event listener)\n (dom-listen el (if is-checkbox \"change\" \"input\")\n (fn (e)\n (if is-checkbox\n (reset! sig (dom-get-prop el \"checked\"))\n (reset! sig (dom-get-prop el \"value\"))))))))" "lisp"))
(~docs/subsection :title "Spec changes"
(~docs/code :code (highlight ";; In adapter-dom.sx, inside render-dom-element:\n;; After processing :on-* event attrs, check for :bind\n(when (dict-has? kwargs \"bind\")\n (let ((sig (dict-get kwargs \"bind\")))\n (when (signal? sig)\n (bind-input el sig))))\n\n;; New function in adapter-dom.sx:\n(define bind-input\n (fn (el sig)\n (let ((tag (lower (dom-tag-name el)))\n (is-checkbox (or (= (dom-get-attr el \"type\") \"checkbox\")\n (= (dom-get-attr el \"type\") \"radio\"))))\n ;; Set initial value\n (if is-checkbox\n (dom-set-prop el \"checked\" (deref sig))\n (dom-set-prop el \"value\" (str (deref sig))))\n ;; Signal → element (effect, auto-tracked)\n (effect (fn ()\n (if is-checkbox\n (dom-set-prop el \"checked\" (deref sig))\n (let ((v (str (deref sig))))\n (when (!= (dom-get-prop el \"value\") v)\n (dom-set-prop el \"value\" v))))))\n ;; Element → signal (event listener)\n (dom-listen el (if is-checkbox \"change\" \"input\")\n (fn (e)\n (if is-checkbox\n (reset! sig (dom-get-prop el \"checked\"))\n (reset! sig (dom-get-prop el \"value\"))))))))" "lisp"))
(p "Platform additions: " (code "dom-set-prop") " and " (code "dom-get-prop") " (property access, not attribute — " (code ".value") " not " (code "getAttribute") "). These go in the boundary as IO primitives."))
(~doc-subsection :title "Derived patterns"
(~docs/subsection :title "Derived patterns"
(p "Input binding composes with everything already built:")
(ul :class "space-y-1 text-stone-600 list-disc pl-5 text-sm"
(li (strong "Validation: ") (code "(computed (fn () (>= (len (deref email)) 3)))") " — derived from the bound signal")
@@ -81,13 +81,13 @@
(li (strong "Form submission: ") (code "(deref email)") " in the submit handler gives the current value")
(li (strong "Stores: ") "Bind to a store signal — multiple islands share the same form state"))))
(~doc-section :title "P0: Keyed List Reconciliation" :id "keyed-list"
(~docs/section :title "P0: Keyed List Reconciliation" :id "keyed-list"
(p (code "reactive-list") " currently clears all DOM nodes and re-renders from scratch on every signal change. This works for small lists but breaks down for large ones — focus is lost, animations restart, scroll position resets.")
(~doc-subsection :title "Design"
(~docs/subsection :title "Design"
(p "When items have a " (code ":key") " attribute (or a key function), " (code "reactive-list") " should reconcile by key instead of clearing.")
(~doc-code :code (highlight ";; Keyed list — items matched by :key, reused across updates\n(defisland ~todo-list ()\n (let ((items (signal (list\n (dict \"id\" 1 \"text\" \"Buy milk\")\n (dict \"id\" 2 \"text\" \"Write spec\")\n (dict \"id\" 3 \"text\" \"Ship it\")))))\n (ul\n (map (fn (item)\n (li :key (get item \"id\")\n (span (get item \"text\"))\n (button :on-click (fn (e) ...)\n \"Remove\")))\n (deref items)))))" "lisp"))
(~docs/code :code (highlight ";; Keyed list — items matched by :key, reused across updates\n(defisland ~reactive-islands/phase2/todo-list ()\n (let ((items (signal (list\n (dict \"id\" 1 \"text\" \"Buy milk\")\n (dict \"id\" 2 \"text\" \"Write spec\")\n (dict \"id\" 3 \"text\" \"Ship it\")))))\n (ul\n (map (fn (item)\n (li :key (get item \"id\")\n (span (get item \"text\"))\n (button :on-click (fn (e) ...)\n \"Remove\")))\n (deref items)))))" "lisp"))
(p "The reconciliation algorithm:")
(ol :class "space-y-1 text-stone-600 list-decimal list-inside text-sm"
@@ -97,8 +97,8 @@
(li "Remove DOM nodes whose keys are absent from the new list")
(li "Result: minimum DOM mutations. Focus, scroll, animations preserved.")))
(~doc-subsection :title "Spec changes"
(~doc-code :code (highlight ";; In adapter-dom.sx, replace reactive-list's effect body:\n(define reactive-list\n (fn (map-fn items-sig env ns)\n (let ((marker (create-comment \"island-list\"))\n (key-map (dict)) ;; key → DOM node\n (key-order (list))) ;; current key order\n (effect (fn ()\n (let ((parent (dom-parent marker))\n (items (deref items-sig)))\n (when parent\n (let ((new-map (dict))\n (new-keys (list)))\n ;; Render or reuse each item\n (for-each (fn (item)\n (let ((rendered (render-item map-fn item env ns))\n (key (or (dom-get-attr rendered \"key\")\n (dom-get-data rendered \"key\")\n (identity-key item))))\n (dom-remove-attr rendered \"key\")\n (if (dict-has? key-map key)\n ;; Reuse existing\n (dict-set! new-map key (dict-get key-map key))\n ;; New node\n (dict-set! new-map key rendered))\n (append! new-keys key)))\n items)\n ;; Remove stale nodes\n (for-each (fn (k)\n (when (not (dict-has? new-map k))\n (dom-remove (dict-get key-map k))))\n key-order)\n ;; Reorder to match new-keys\n (let ((cursor marker))\n (for-each (fn (k)\n (let ((node (dict-get new-map k)))\n (when (not (= node (dom-next-sibling cursor)))\n (dom-insert-after cursor node))\n (set! cursor node)))\n new-keys))\n ;; Update state\n (set! key-map new-map)\n (set! key-order new-keys))))))\n marker)))" "lisp"))
(~docs/subsection :title "Spec changes"
(~docs/code :code (highlight ";; In adapter-dom.sx, replace reactive-list's effect body:\n(define reactive-list\n (fn (map-fn items-sig env ns)\n (let ((marker (create-comment \"island-list\"))\n (key-map (dict)) ;; key → DOM node\n (key-order (list))) ;; current key order\n (effect (fn ()\n (let ((parent (dom-parent marker))\n (items (deref items-sig)))\n (when parent\n (let ((new-map (dict))\n (new-keys (list)))\n ;; Render or reuse each item\n (for-each (fn (item)\n (let ((rendered (render-item map-fn item env ns))\n (key (or (dom-get-attr rendered \"key\")\n (dom-get-data rendered \"key\")\n (identity-key item))))\n (dom-remove-attr rendered \"key\")\n (if (dict-has? key-map key)\n ;; Reuse existing\n (dict-set! new-map key (dict-get key-map key))\n ;; New node\n (dict-set! new-map key rendered))\n (append! new-keys key)))\n items)\n ;; Remove stale nodes\n (for-each (fn (k)\n (when (not (dict-has? new-map k))\n (dom-remove (dict-get key-map k))))\n key-order)\n ;; Reorder to match new-keys\n (let ((cursor marker))\n (for-each (fn (k)\n (let ((node (dict-get new-map k)))\n (when (not (= node (dom-next-sibling cursor)))\n (dom-insert-after cursor node))\n (set! cursor node)))\n new-keys))\n ;; Update state\n (set! key-map new-map)\n (set! key-order new-keys))))))\n marker)))" "lisp"))
(p "Falls back to current clear-and-rerender when no keys are present.")))
@@ -106,11 +106,11 @@
;; P1 — important
;; -----------------------------------------------------------------------
(~doc-section :title "P1: Portals" :id "portals"
(~docs/section :title "P1: Portals" :id "portals"
(p "A portal renders children into a DOM node " (em "outside") " the island's subtree. Essential for modals, tooltips, dropdown menus, and toast notifications — anything that must escape overflow:hidden, z-index stacking, or layout constraints.")
(~doc-subsection :title "Design"
(~doc-code :code (highlight ";; portal — render children into a target element\n(defisland ~modal-trigger ()\n (let ((open? (signal false)))\n (div\n (button :on-click (fn (e) (swap! open? not))\n \"Open Modal\")\n\n ;; Portal: children rendered into #modal-root,\n ;; not into this island's DOM\n (portal \"#modal-root\"\n (when (deref open?)\n (div :class \"fixed inset-0 bg-black/50 flex items-center justify-center\"\n (div :class \"bg-white rounded-lg p-6 max-w-md\"\n (h2 \"Modal Title\")\n (p \"This is rendered outside the island's DOM subtree.\")\n (button :on-click (fn (e) (reset! open? false))\n \"Close\"))))))))" "lisp"))
(~docs/subsection :title "Design"
(~docs/code :code (highlight ";; portal — render children into a target element\n(defisland ~reactive-islands/phase2/modal-trigger ()\n (let ((open? (signal false)))\n (div\n (button :on-click (fn (e) (swap! open? not))\n \"Open Modal\")\n\n ;; Portal: children rendered into #modal-root,\n ;; not into this island's DOM\n (portal \"#modal-root\"\n (when (deref open?)\n (div :class \"fixed inset-0 bg-black/50 flex items-center justify-center\"\n (div :class \"bg-white rounded-lg p-6 max-w-md\"\n (h2 \"Modal Title\")\n (p \"This is rendered outside the island's DOM subtree.\")\n (button :on-click (fn (e) (reset! open? false))\n \"Close\"))))))))" "lisp"))
(p "Implementation in " (code "adapter-dom.sx") ":")
(ol :class "space-y-1 text-stone-600 list-decimal list-inside text-sm"
@@ -121,18 +121,18 @@
(li "Return a comment marker in the original position (for disposal tracking)")
(li "On island disposal, portal content is removed from the target")))
(~doc-subsection :title "Disposal"
(~docs/subsection :title "Disposal"
(p "Portals must participate in island disposal. When the island is destroyed, portal content must be removed from its remote target. The " (code "with-island-scope") " mechanism handles this — the portal registers a disposer that removes its children from the target element.")))
;; -----------------------------------------------------------------------
;; P2 — nice to have
;; -----------------------------------------------------------------------
(~doc-section :title "P2: Error Boundaries" :id "error-boundaries"
(~docs/section :title "P2: Error Boundaries" :id "error-boundaries"
(p "When an island's rendering or effect throws, the error currently propagates to the top level and may crash other islands. An error boundary catches the error and renders a fallback UI.")
(~doc-subsection :title "Design"
(~doc-code :code (highlight ";; error-boundary — catch errors in island subtrees\n(defisland ~resilient-widget ()\n (error-boundary\n ;; Fallback: shown when children throw\n (fn (err)\n (div :class \"p-4 bg-red-50 border border-red-200 rounded\"\n (p :class \"text-red-700 font-medium\" \"Something went wrong\")\n (p :class \"text-red-500 text-sm\" (error-message err))))\n ;; Children: the happy path\n (do\n (~risky-component)\n (~another-component))))" "lisp"))
(~docs/subsection :title "Design"
(~docs/code :code (highlight ";; error-boundary — catch errors in island subtrees\n(defisland ~reactive-islands/phase2/resilient-widget ()\n (error-boundary\n ;; Fallback: shown when children throw\n (fn (err)\n (div :class \"p-4 bg-red-50 border border-red-200 rounded\"\n (p :class \"text-red-700 font-medium\" \"Something went wrong\")\n (p :class \"text-red-500 text-sm\" (error-message err))))\n ;; Children: the happy path\n (do\n (~risky-component)\n (~another-component))))" "lisp"))
(p "Implementation:")
(ol :class "space-y-1 text-stone-600 list-decimal list-inside text-sm"
@@ -143,23 +143,23 @@
(li "Effects within the boundary are disposed on error")
(li "A " (code "retry") " function is passed to the fallback for recovery"))))
(~doc-section :title "P2: Suspense" :id "suspense"
(~docs/section :title "P2: Suspense" :id "suspense"
(p "Suspense handles async operations in the render path — data fetching, lazy-loaded components, code splitting. Show a loading placeholder until the async work completes, then swap in the result.")
(~doc-subsection :title "Design"
(~doc-code :code (highlight ";; suspense — async-aware rendering boundary\n(defisland ~user-profile (&key user-id)\n (suspense\n ;; Fallback: shown during loading\n (div :class \"animate-pulse\"\n (div :class \"h-4 bg-stone-200 rounded w-3/4\")\n (div :class \"h-4 bg-stone-200 rounded w-1/2 mt-2\"))\n ;; Children: may contain async operations\n (let ((user (await (fetch-json (str \"/api/users/\" user-id)))))\n (div\n (h2 (get user \"name\"))\n (p (get user \"email\"))))))" "lisp"))
(~docs/subsection :title "Design"
(~docs/code :code (highlight ";; suspense — async-aware rendering boundary\n(defisland ~reactive-islands/phase2/user-profile (&key user-id)\n (suspense\n ;; Fallback: shown during loading\n (div :class \"animate-pulse\"\n (div :class \"h-4 bg-stone-200 rounded w-3/4\")\n (div :class \"h-4 bg-stone-200 rounded w-1/2 mt-2\"))\n ;; Children: may contain async operations\n (let ((user (await (fetch-json (str \"/api/users/\" user-id)))))\n (div\n (h2 (get user \"name\"))\n (p (get user \"email\"))))))" "lisp"))
(p "This requires a new primitive concept: a " (strong "resource") " — an async signal that transitions through loading → resolved → error states.")
(~doc-code :code (highlight ";; resource — async signal\n(define resource\n (fn (fetch-fn)\n ;; Returns a signal-like value:\n ;; {:loading true :data nil :error nil} initially\n ;; {:loading false :data result :error nil} on success\n ;; {:loading false :data nil :error err} on failure\n (let ((state (signal (dict \"loading\" true\n \"data\" nil\n \"error\" nil))))\n ;; Kick off the async operation\n (promise-then (fetch-fn)\n (fn (data) (reset! state (dict \"loading\" false\n \"data\" data\n \"error\" nil)))\n (fn (err) (reset! state (dict \"loading\" false\n \"data\" nil\n \"error\" err))))\n state)))" "lisp"))
(~docs/code :code (highlight ";; resource — async signal\n(define resource\n (fn (fetch-fn)\n ;; Returns a signal-like value:\n ;; {:loading true :data nil :error nil} initially\n ;; {:loading false :data result :error nil} on success\n ;; {:loading false :data nil :error err} on failure\n (let ((state (signal (dict \"loading\" true\n \"data\" nil\n \"error\" nil))))\n ;; Kick off the async operation\n (promise-then (fetch-fn)\n (fn (data) (reset! state (dict \"loading\" false\n \"data\" data\n \"error\" nil)))\n (fn (err) (reset! state (dict \"loading\" false\n \"data\" nil\n \"error\" err))))\n state)))" "lisp"))
(p "Suspense is the rendering boundary; resource is the data primitive. Together they give a clean async data story without effects-that-fetch (React's " (code "useEffect") " + " (code "useState") " anti-pattern).")))
(~doc-section :title "P2: Transitions" :id "transitions"
(~docs/section :title "P2: Transitions" :id "transitions"
(p "Transitions mark updates as non-urgent. The UI stays interactive during expensive re-renders. React's " (code "startTransition") " defers state updates so that urgent updates (typing, clicking) aren't blocked by slow ones (filtering a large list, rendering a complex subtree).")
(~doc-subsection :title "Design"
(~doc-code :code (highlight ";; transition — non-urgent signal update\n(defisland ~search-results (&key items)\n (let ((query (signal \"\"))\n (filtered (signal items))\n (is-pending (signal false)))\n ;; Typing is urgent — updates immediately\n ;; Filtering is deferred — doesn't block input\n (effect (fn ()\n (let ((q (deref query)))\n (transition is-pending\n (fn ()\n (reset! filtered\n (filter (fn (item)\n (contains? (lower (get item \"name\")) (lower q)))\n items)))))))\n (div\n (input :bind query :placeholder \"Search...\")\n (when (deref is-pending)\n (span :class \"text-stone-400\" \"Filtering...\"))\n (ul (map (fn (item) (li (get item \"name\")))\n (deref filtered))))))" "lisp"))
(~docs/subsection :title "Design"
(~docs/code :code (highlight ";; transition — non-urgent signal update\n(defisland ~reactive-islands/phase2/search-results (&key items)\n (let ((query (signal \"\"))\n (filtered (signal items))\n (is-pending (signal false)))\n ;; Typing is urgent — updates immediately\n ;; Filtering is deferred — doesn't block input\n (effect (fn ()\n (let ((q (deref query)))\n (transition is-pending\n (fn ()\n (reset! filtered\n (filter (fn (item)\n (contains? (lower (get item \"name\")) (lower q)))\n items)))))))\n (div\n (input :bind query :placeholder \"Search...\")\n (when (deref is-pending)\n (span :class \"text-stone-400\" \"Filtering...\"))\n (ul (map (fn (item) (li (get item \"name\")))\n (deref filtered))))))" "lisp"))
(p (code "transition") " takes a pending-signal and a thunk. It sets pending to true, schedules the thunk via " (code "requestIdleCallback") " (or " (code "setTimeout 0") " as fallback), then sets pending to false when complete. Signal writes inside the thunk are batched and applied asynchronously.")
(p "This is lower priority because SX's fine-grained updates already avoid the re-render-everything problem that makes transitions critical in React. But for truly large lists or expensive computations, deferral is still valuable.")))
@@ -168,7 +168,7 @@
;; Implementation order
;; -----------------------------------------------------------------------
(~doc-section :title "Implementation Order" :id "order"
(~docs/section :title "Implementation Order" :id "order"
(p "Each feature is independent. Suggested order based on dependency and value:")
(ol :class "space-y-3 text-stone-600 list-decimal list-inside"
(li (strong "Input binding") " (P0) — unlocks forms. Smallest change, biggest impact. One new function in adapter-dom.sx, two platform primitives (" (code "dom-set-prop") ", " (code "dom-get-prop") "). Add to demo page immediately.")
@@ -178,7 +178,7 @@
(p :class "mt-4 text-stone-600" "Every feature follows the same pattern: spec in " (code ".sx") " → bootstrap to JS/Python → add platform primitives → add demo island. No feature requires changes to the signal runtime, the evaluator, or the rendering pipeline. They are all additive."))
(~doc-section :title "What we are NOT building" :id "not-building"
(~docs/section :title "What we are NOT building" :id "not-building"
(p "Some React features are deliberately excluded:")
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
(li (strong "Virtual DOM / diffing") " — SX uses fine-grained signals. There is no component re-render to diff against. The " (code "reactive-text") ", " (code "reactive-attr") ", " (code "reactive-fragment") ", and " (code "reactive-list") " primitives update the exact DOM nodes that changed.")

View File

@@ -2,10 +2,10 @@
;; Plan — the full design document (moved from plans section)
;; ---------------------------------------------------------------------------
(defcomp ~reactive-islands-plan-content ()
(~doc-page :title "Reactive Islands Plan"
(defcomp ~reactive-islands/plan/reactive-islands-plan-content ()
(~docs/page :title "Reactive Islands Plan"
(~doc-section :title "Context" :id "context"
(~docs/section :title "Context" :id "context"
(p "SX already has a sliding bar for " (em "where") " rendering happens — server-side HTML, SX wire format for client rendering, or any point between. This is the isomorphism bar. It controls the render boundary.")
(p "There is a second bar, orthogonal to the first: " (em "how state flows.") " On one end, all state lives on the server — every user action is a round-trip, every UI update is a fresh render. This is the htmx model. On the other end, state lives on the client — signals, subscriptions, fine-grained DOM updates without server involvement. This is the React model.")
(p "These two bars are independent. You can have server-rendered HTML with client state (SSR + hydrated React). You can have client-rendered components with server state (current SX). The combination creates four quadrants:")
@@ -29,26 +29,26 @@
(p "Today SX occupies the bottom-left quadrant — client-rendered components with server state. This plan adds the bottom-right: " (strong "reactive islands") " with client-local signals. A page can mix all four quadrants. Most content stays hypermedia. Interactive regions opt into reactivity. The author controls where each component sits on both bars."))
(~doc-section :title "The Spectrum" :id "spectrum"
(~docs/section :title "The Spectrum" :id "spectrum"
(p "Four levels of client interactivity. Each is independently valuable. Each is opt-in per component.")
(~doc-subsection :title "Level 0: Pure Hypermedia"
(~docs/subsection :title "Level 0: Pure Hypermedia"
(p "The default. " (code "sx-get") ", " (code "sx-post") ", " (code "sx-swap") ". Server renders everything. Client swaps fragments. No client state. No JavaScript state management. This is where 90% of a typical application should live."))
(~doc-subsection :title "Level 1: Local DOM Operations"
(~docs/subsection :title "Level 1: Local DOM Operations"
(p "Imperative escape hatches for micro-interactions too small for a server round-trip: toggling a menu, switching a tab, showing a tooltip. " (code "toggle!") ", " (code "set-attr!") ", " (code "on-event") ". No reactive graph. Just do the thing directly."))
(~doc-subsection :title "Level 2: Reactive Islands"
(~docs/subsection :title "Level 2: Reactive Islands"
(p (code "defisland") " components with local signals. Fine-grained DOM updates — no virtual DOM, no diffing, no component re-renders. A signal change updates only the DOM nodes that read it. Islands are isolated by default. The server can render their initial state."))
(~doc-subsection :title "Level 3: Connected Islands"
(~docs/subsection :title "Level 3: Connected Islands"
(p "Islands that share state via signal props or named stores (" (code "def-store") " / " (code "use-store") "). Plus event bridges for htmx lake-to-island communication. This is where SX starts to feel like React — but only in the regions that need it. The surrounding page remains hypermedia.")))
(~doc-section :title "htmx Lakes" :id "lakes"
(~docs/section :title "htmx Lakes" :id "lakes"
(p "An htmx lake is server-driven content " (em "inside") " a reactive island. The island provides the reactive boundary; the lake content is swapped via " (code "sx-get") "/" (code "sx-post") " like normal hypermedia.")
(p "This works because signals live in JavaScript closures, not in the DOM. When a swap replaces lake content, the island's signals are unaffected. The lake can communicate back to the island via the " (a :href "/sx/(geography.(reactive.event-bridge))" :sx-get "/sx/(geography.(reactive.event-bridge))" :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" :class "text-violet-700 underline" "event bridge") ".")
(~doc-subsection :title "Navigation scenarios"
(~docs/subsection :title "Navigation scenarios"
(div :class "space-y-3"
(div :class "rounded border border-green-200 bg-green-50 p-3"
(div :class "font-semibold text-green-800" "Swap inside island")
@@ -63,25 +63,25 @@
(div :class "font-semibold text-stone-800" "Full page navigation")
(p :class "text-sm text-stone-600 mt-1" "Everything cleared. clean slate. clear-stores wipes the registry.")))))
(~doc-section :title "Reactive DOM Rendering" :id "reactive-rendering"
(~docs/section :title "Reactive DOM Rendering" :id "reactive-rendering"
(p "The existing " (code "renderDOM") " function walks the AST and creates DOM nodes. Inside an island, it becomes signal-aware:")
(~doc-subsection :title "Text bindings"
(~doc-code :code (highlight ";; (span (deref count)) creates:\n;; const text = document.createTextNode(sig.value)\n;; effect(() => text.nodeValue = sig.value)" "lisp"))
(~docs/subsection :title "Text bindings"
(~docs/code :code (highlight ";; (span (deref count)) creates:\n;; const text = document.createTextNode(sig.value)\n;; effect(() => text.nodeValue = sig.value)" "lisp"))
(p "Only the text node updates. The span is untouched."))
(~doc-subsection :title "Attribute bindings"
(~doc-code :code (highlight ";; (div :class (str \"panel \" (if (deref open?) \"visible\" \"hidden\")))\n;; effect(() => div.className = ...)" "lisp")))
(~docs/subsection :title "Attribute bindings"
(~docs/code :code (highlight ";; (div :class (str \"panel \" (if (deref open?) \"visible\" \"hidden\")))\n;; effect(() => div.className = ...)" "lisp")))
(~doc-subsection :title "Conditional fragments"
(~doc-code :code (highlight ";; (when (deref show?) (~details)) creates:\n;; A marker comment node, then:\n;; effect(() => show ? insert-after(marker, render(~details)) : remove)" "lisp"))
(~docs/subsection :title "Conditional fragments"
(~docs/code :code (highlight ";; (when (deref show?) (~details)) creates:\n;; A marker comment node, then:\n;; effect(() => show ? insert-after(marker, render(~details)) : remove)" "lisp"))
(p "Equivalent to SolidJS's " (code "Show") " — but falls out naturally from the evaluator."))
(~doc-subsection :title "List rendering"
(~doc-code :code (highlight "(map (fn (item) (li :key (get item \"id\") (get item \"name\")))\n (deref items))" "lisp"))
(~docs/subsection :title "List rendering"
(~docs/code :code (highlight "(map (fn (item) (li :key (get item \"id\") (get item \"name\")))\n (deref items))" "lisp"))
(p "Keyed elements are reused and reordered. Unkeyed elements are morphed.")))
(~doc-section :title "Status" :id "status"
(~docs/section :title "Status" :id "status"
(div :class "overflow-x-auto rounded border border-stone-200"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-100"
@@ -146,7 +146,7 @@
(td :class "px-3 py-2 text-stone-500 font-medium" "N/A")
(td :class "px-3 py-2 text-stone-700" "covered by existing primitives"))))))
(~doc-section :title "Design Principles" :id "principles"
(~docs/section :title "Design Principles" :id "principles"
(ol :class "space-y-3 text-stone-600 list-decimal list-inside"
(li (strong "Islands are opt-in.") " " (code "defcomp") " remains the default. Components are inert unless you choose " (code "defisland") ". No reactive overhead for static content.")
(li (strong "Signals are values, not hooks.") " Create them anywhere. Pass them as arguments. Store them in dicts. No rules about calling order or conditional creation.")

View File

@@ -1,8 +1,8 @@
;; Reference page layouts — receive data from Python primitives
;; @css bg-blue-100 text-blue-700 bg-emerald-100 text-emerald-700 bg-amber-100 text-amber-700
(defcomp ~reference-attrs-content (&key req-table beh-table uniq-table)
(~doc-page :title "Attribute Reference"
(defcomp ~reference/attrs-content (&key req-table beh-table uniq-table)
(~docs/page :title "Attribute Reference"
(p :class "text-stone-600 mb-6"
"sx attributes mirror htmx where possible. This table shows all available attributes and their status.")
(div :class "space-y-8"
@@ -10,49 +10,49 @@
beh-table
uniq-table)))
(defcomp ~reference-headers-content (&key req-table resp-table)
(~doc-page :title "Headers"
(defcomp ~reference/headers-content (&key req-table resp-table)
(~docs/page :title "Headers"
(p :class "text-stone-600 mb-6"
"sx uses custom HTTP headers to coordinate between client and server.")
(div :class "space-y-8"
req-table
resp-table)))
(defcomp ~reference-events-content (&key table)
(~doc-page :title "Events"
(defcomp ~reference/events-content (&key table)
(~docs/page :title "Events"
(p :class "text-stone-600 mb-6"
"sx fires custom DOM events at various points in the request lifecycle. "
"Listen for them with sx-on:* attributes or addEventListener. "
"Client-side routing fires sx:clientRoute instead of request lifecycle events.")
table))
(defcomp ~reference-js-api-content (&key table)
(~doc-page :title "JavaScript API"
(defcomp ~reference/js-api-content (&key table)
(~docs/page :title "JavaScript API"
table))
(defcomp ~reference-attr-detail-content (&key (title :as string) (description :as string) demo
(defcomp ~reference/attr-detail-content (&key (title :as string) (description :as string) demo
(example-code :as string) (handler-code :as string?) (wire-placeholder-id :as string?))
(~doc-page :title title
(~docs/page :title title
(p :class "text-stone-600 mb-6" description)
(when demo
(~example-card :title "Demo"
(~example-demo demo)))
(~examples/card :title "Demo"
(~examples/demo demo)))
(h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")
(~example-source :code (highlight example-code "lisp"))
(~examples/source :code (highlight example-code "lisp"))
(when handler-code
(<>
(h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")
(~example-source :code (highlight handler-code "lisp"))))
(~examples/source :code (highlight handler-code "lisp"))))
(when wire-placeholder-id
(<>
(h3 :class "text-lg font-semibold text-stone-700 mt-6" "Wire response")
(p :class "text-stone-500 text-sm mb-2"
"Trigger the demo to see the raw response the server sends.")
(~doc-placeholder :id wire-placeholder-id)))))
(~docs/placeholder :id wire-placeholder-id)))))
(defcomp ~reference-header-detail-content (&key (title :as string) (direction :as string) (description :as string)
(defcomp ~reference/header-detail-content (&key (title :as string) (direction :as string) (description :as string)
(example-code :as string?) demo)
(~doc-page :title title
(~docs/page :title title
(let ((badge-class (if (= direction "request")
"bg-blue-100 text-blue-700"
(if (= direction "response")
@@ -66,25 +66,25 @@
badge-label)))
(p :class "text-stone-600 mb-6" description)
(when demo
(~example-card :title "Demo"
(~example-demo demo)))
(~examples/card :title "Demo"
(~examples/demo demo)))
(when example-code
(<>
(h3 :class "text-lg font-semibold text-stone-700 mt-6" "Example usage")
(~example-source :code (highlight example-code "lisp"))))))
(~examples/source :code (highlight example-code "lisp"))))))
(defcomp ~reference-event-detail-content (&key title description example-code demo)
(~doc-page :title title
(defcomp ~reference/event-detail-content (&key title description example-code demo)
(~docs/page :title title
(p :class "text-stone-600 mb-6" description)
(when demo
(~example-card :title "Demo"
(~example-demo demo)))
(~examples/card :title "Demo"
(~examples/demo demo)))
(when example-code
(<>
(h3 :class "text-lg font-semibold text-stone-700 mt-6" "Example usage")
(~example-source :code (highlight example-code "lisp"))))))
(~examples/source :code (highlight example-code "lisp"))))))
(defcomp ~reference-attr-not-found (&key (slug :as string))
(~doc-page :title "Not Found"
(defcomp ~reference/attr-not-found (&key (slug :as string))
(~docs/page :title "Not Found"
(p :class "text-stone-600"
(str "No documentation found for \"" slug "\"."))))

View File

@@ -2,9 +2,9 @@
;; Shows which pages route client-side (pure, instant) vs server-side (IO/data).
;; @css bg-green-100 text-green-800 bg-violet-600 bg-stone-200 text-violet-600 text-stone-600 text-green-600 rounded-full h-2.5 grid-cols-2 bg-blue-100 text-blue-800 bg-amber-100 text-amber-800 grid-cols-4 marker:text-stone-400 bg-blue-50 bg-amber-50 text-blue-700 text-amber-700 border-blue-200 border-amber-200 bg-blue-500 bg-amber-500 grid-cols-3 border-green-200 bg-green-50 text-green-700
(defcomp ~routing-analyzer-content (&key pages total-pages client-count
(defcomp ~routing-analyzer/content (&key pages total-pages client-count
server-count registry-sample)
(~doc-page :title "Routing Analyzer"
(~docs/page :title "Routing Analyzer"
(p :class "text-stone-600 mb-6"
"Live classification of all " (strong (str total-pages)) " pages by routing mode. "
@@ -20,13 +20,13 @@
" IO detection.")
(div :class "mb-8 grid grid-cols-4 gap-4"
(~analyzer-stat :label "Total Pages" :value (str total-pages)
(~analyzer/stat :label "Total Pages" :value (str total-pages)
:cls "text-violet-600")
(~analyzer-stat :label "Client-Routable" :value (str client-count)
(~analyzer/stat :label "Client-Routable" :value (str client-count)
:cls "text-green-600")
(~analyzer-stat :label "Server-Only" :value (str server-count)
(~analyzer/stat :label "Server-Only" :value (str server-count)
:cls "text-amber-600")
(~analyzer-stat :label "Client Ratio" :value (str (round (* (/ client-count total-pages) 100)) "%")
(~analyzer/stat :label "Client Ratio" :value (str (round (* (/ client-count total-pages) 100)) "%")
:cls "text-blue-600"))
;; Route classification bar
@@ -39,10 +39,10 @@
(div :class "bg-green-500 h-4 rounded-l-full transition-all"
:style (str "width: " (round (* (/ client-count total-pages) 100)) "%"))))
(~doc-section :title "Route Table" :id "routes"
(~docs/section :title "Route Table" :id "routes"
(div :class "space-y-2"
(map (fn (page)
(~routing-row
(~routing-analyzer/routing-row
:name (get page "name")
:path (get page "path")
:mode (get page "mode")
@@ -51,7 +51,7 @@
:reason (get page "reason")))
pages)))
(~doc-section :title "Page Registry Format" :id "registry"
(~docs/section :title "Page Registry Format" :id "registry"
(p :class "text-stone-600 mb-4"
"The server serializes page metadata as SX dict literals inside "
(code "<script type=\"text/sx-pages\">")
@@ -62,7 +62,7 @@
(pre :class "text-xs leading-relaxed whitespace-pre-wrap overflow-x-auto bg-stone-100 rounded border border-stone-200 p-4"
(code (highlight registry-sample "lisp"))))))
(~doc-section :title "How Client Routing Works" :id "how"
(~docs/section :title "How Client Routing Works" :id "how"
(ol :class "list-decimal pl-5 space-y-2 text-stone-700"
(li (strong "Boot: ") "boot.sx finds " (code "<script type=\"text/sx-pages\">") ", calls " (code "parse") " on the SX content, then " (code "parse-route-pattern") " on each page's path to build " (code "_page-routes") ".")
(li (strong "Click: ") "orchestration.sx intercepts boost link clicks via " (code "bind-client-route-link") ". Extracts the pathname from the href.")
@@ -72,7 +72,7 @@
(li (strong "Swap: ") "On success, the rendered DOM replaces " (code "#main-panel") " contents, " (code "pushState") " updates the URL, and the console logs " (code "sx:route client /path") ".")
(li (strong "Fallback: ") "If anything fails (no match, eval error, missing component), the click falls through to a standard server fetch. Console logs " (code "sx:route server /path") ". The user sees no difference.")))))
(defcomp ~routing-row (&key (name :as string) (path :as string) (mode :as string) (has-data :as boolean) (content-expr :as string?) (reason :as string?))
(defcomp ~routing-analyzer/routing-row (&key (name :as string) (path :as string) (mode :as string) (has-data :as boolean) (content-expr :as string?) (reason :as string?))
(div :class (str "rounded border p-3 flex items-center gap-3 "
(if (= mode "client")
"border-green-200 bg-green-50"

View File

@@ -2,34 +2,34 @@
;; Spec Explorer — structured interactive view of SX spec files
;; ---------------------------------------------------------------------------
(defcomp ~spec-explorer-content (&key data)
(~doc-page :title (str (get data "title") " — Explorer")
(defcomp ~specs-explorer/spec-explorer-content (&key data)
(~docs/page :title (str (get data "title") " — Explorer")
;; Header with filename and source link
(~spec-explorer-header
(~specs-explorer/spec-explorer-header
:filename (get data "filename")
:title (get data "title")
:desc (get data "desc")
:slug (replace (get data "filename") ".sx" ""))
;; Stats bar
(~spec-explorer-stats :stats (get data "stats"))
(~specs-explorer/spec-explorer-stats :stats (get data "stats"))
;; Sections
(map (fn (section)
(~spec-explorer-section :section section))
(~specs-explorer/spec-explorer-section :section section))
(get data "sections"))
;; Platform interface
(when (not (empty? (get data "platform-interface")))
(~spec-platform-interface :items (get data "platform-interface")))))
(~specs-explorer/spec-platform-interface :items (get data "platform-interface")))))
;; ---------------------------------------------------------------------------
;; Header
;; ---------------------------------------------------------------------------
(defcomp ~spec-explorer-header (&key filename title desc slug)
(defcomp ~specs-explorer/spec-explorer-header (&key filename title desc slug)
(div :class "mb-6"
(div :class "flex items-center justify-between"
(div
@@ -48,7 +48,7 @@
;; Stats bar
;; ---------------------------------------------------------------------------
(defcomp ~spec-explorer-stats (&key stats)
(defcomp ~specs-explorer/spec-explorer-stats (&key stats)
(div :class "flex flex-wrap gap-2 mb-6 text-xs"
(span :class "bg-stone-100 text-stone-600 px-2 py-0.5 rounded font-medium"
(str (get stats "total-defines") " defines"))
@@ -75,7 +75,7 @@
;; Section
;; ---------------------------------------------------------------------------
(defcomp ~spec-explorer-section (&key section)
(defcomp ~specs-explorer/spec-explorer-section (&key section)
(div :class "mb-8"
(h2 :class "text-lg font-semibold text-stone-700 border-b border-stone-200 pb-1 mb-3"
:id (replace (lower (get section "title")) " " "-")
@@ -83,7 +83,7 @@
(when (get section "comment")
(p :class "text-sm text-stone-500 mb-3" (get section "comment")))
(div :class "space-y-4"
(map (fn (d) (~spec-explorer-define :d d))
(map (fn (d) (~specs-explorer/spec-explorer-define :d d))
(get section "defines")))))
@@ -91,7 +91,7 @@
;; Define card — one function/constant with all five rings
;; ---------------------------------------------------------------------------
(defcomp ~spec-explorer-define (&key d)
(defcomp ~specs-explorer/spec-explorer-define (&key d)
(div :class "rounded border border-stone-200 p-4"
:id (str "fn-" (get d "name"))
@@ -101,15 +101,15 @@
(span :class "text-xs text-stone-400" (get d "kind"))
(if (empty? (get d "effects"))
(span :class "text-xs px-1.5 py-0.5 rounded bg-green-100 text-green-700" "pure")
(map (fn (eff) (~spec-effect-badge :effect eff))
(map (fn (eff) (~specs-explorer/spec-effect-badge :effect eff))
(get d "effects"))))
;; Params
(when (not (empty? (get d "params")))
(~spec-param-list :params (get d "params")))
(~specs-explorer/spec-param-list :params (get d "params")))
;; Ring 2: Translation panels (SX + Python + JavaScript + Z3)
(~spec-ring-translations
(~specs-explorer/spec-ring-translations
:source (get d "source")
:python (get d "python")
:javascript (get d "javascript")
@@ -117,11 +117,11 @@
;; Ring 3: Cross-references
(when (not (empty? (get d "refs")))
(~spec-ring-bridge :refs (get d "refs")))
(~specs-explorer/spec-ring-bridge :refs (get d "refs")))
;; Ring 4: Tests
(when (> (get d "test-count") 0)
(~spec-ring-runtime
(~specs-explorer/spec-ring-runtime
:tests (get d "tests")
:test-count (get d "test-count")))))
@@ -130,7 +130,7 @@
;; Effect badge
;; ---------------------------------------------------------------------------
(defcomp ~spec-effect-badge (&key effect)
(defcomp ~specs-explorer/spec-effect-badge (&key effect)
(span :class (str "text-xs px-1.5 py-0.5 rounded "
(case effect
"mutation" "bg-amber-100 text-amber-700"
@@ -144,7 +144,7 @@
;; Param list
;; ---------------------------------------------------------------------------
(defcomp ~spec-param-list (&key params)
(defcomp ~specs-explorer/spec-param-list (&key params)
(div :class "mt-1 flex flex-wrap gap-1"
(map (fn (p)
(let ((name (get p "name"))
@@ -164,7 +164,7 @@
;; Ring 2: Translation panels (nucleus + bootstrapper)
;; ---------------------------------------------------------------------------
(defcomp ~spec-ring-translations (&key source python javascript z3)
(defcomp ~specs-explorer/spec-ring-translations (&key source python javascript z3)
(when (not (= source ""))
(div :class "mt-3 border border-stone-200 rounded-lg overflow-hidden"
;; SX source — Ring 1: the nucleus (always open)
@@ -200,7 +200,7 @@
;; Ring 3: Cross-references (bridge)
;; ---------------------------------------------------------------------------
(defcomp ~spec-ring-bridge (&key refs)
(defcomp ~specs-explorer/spec-ring-bridge (&key refs)
(div :class "mt-2"
(span :class "text-xs font-medium text-stone-500" "References")
(div :class "flex flex-wrap gap-1 mt-1"
@@ -215,7 +215,7 @@
;; Ring 4: Tests (runtime)
;; ---------------------------------------------------------------------------
(defcomp ~spec-ring-runtime (&key tests test-count)
(defcomp ~specs-explorer/spec-ring-runtime (&key tests test-count)
(div :class "mt-2"
(div :class "flex items-center gap-1"
(span :class "text-xs font-medium text-stone-500" "Tests")
@@ -233,7 +233,7 @@
;; Platform interface table (Ring 3 overview)
;; ---------------------------------------------------------------------------
(defcomp ~spec-platform-interface (&key items)
(defcomp ~specs-explorer/spec-platform-interface (&key items)
(div :class "mt-8"
(h2 :class "text-lg font-semibold text-stone-700 border-b border-stone-200 pb-1 mb-3"
"Platform Interface")

View File

@@ -4,8 +4,8 @@
;; Architecture intro page
;; ---------------------------------------------------------------------------
(defcomp ~spec-architecture-content ()
(~doc-page :title "Spec Architecture"
(defcomp ~specs/architecture-content ()
(~docs/page :title "Spec Architecture"
(div :class "space-y-8"
(div :class "space-y-4"
@@ -292,8 +292,8 @@ deps.sx depends on: eval")))
;; Overview pages (Core / Adapters) — show truncated previews of each file
;; ---------------------------------------------------------------------------
(defcomp ~spec-overview-content (&key (spec-title :as string) (spec-files :as list))
(~doc-page :title (or spec-title "Specs")
(defcomp ~specs/overview-content (&key (spec-title :as string) (spec-files :as list))
(~docs/page :title (or spec-title "Specs")
(p :class "text-stone-600 mb-6"
(case spec-title
"Core Language"
@@ -332,8 +332,8 @@ deps.sx depends on: eval")))
;; Detail page — full source of a single spec file
;; ---------------------------------------------------------------------------
(defcomp ~spec-detail-content (&key (spec-title :as string) (spec-desc :as string) (spec-filename :as string) (spec-source :as string) (spec-prose :as string?))
(~doc-page :title spec-title
(defcomp ~specs/detail-content (&key (spec-title :as string) (spec-desc :as string) (spec-filename :as string) (spec-source :as string) (spec-prose :as string?))
(~docs/page :title spec-title
(div :class "flex items-center gap-3 mb-4"
(span :class "text-sm text-stone-400 font-mono" spec-filename)
(span :class "text-sm text-stone-500 flex-1" spec-desc)
@@ -357,8 +357,8 @@ deps.sx depends on: eval")))
;; Bootstrappers — summary index
;; ---------------------------------------------------------------------------
(defcomp ~bootstrappers-index-content ()
(~doc-page :title "Bootstrappers"
(defcomp ~specs/bootstrappers-index-content ()
(~docs/page :title "Bootstrappers"
(div :class "space-y-6"
(p :class "text-lg text-stone-600"
"A bootstrapper reads the canonical " (code :class "text-violet-700 text-sm" ".sx")
@@ -410,8 +410,8 @@ deps.sx depends on: eval")))
;; ---------------------------------------------------------------------------
;; @css border-violet-300 animate-pulse
(defcomp ~bootstrapper-js-content (&key bootstrapper-source bootstrapped-output)
(~doc-page :title "JavaScript Bootstrapper"
(defcomp ~specs/bootstrapper-js-content (&key bootstrapper-source bootstrapped-output)
(~docs/page :title "JavaScript Bootstrapper"
(div :class "space-y-8"
(div :class "space-y-3"
@@ -452,8 +452,8 @@ deps.sx depends on: eval")))
;; ---------------------------------------------------------------------------
;; @css bg-green-100 text-green-800 bg-green-50 border-green-200 text-green-700
(defcomp ~bootstrapper-self-hosting-content (&key (py-sx-source :as string) (g0-output :as string) (g1-output :as string) (defines-matched :as number) (defines-total :as number) (g0-lines :as number) (g0-bytes :as number) (verification-status :as string))
(~doc-page :title "Self-Hosting Bootstrapper (py.sx)"
(defcomp ~specs/bootstrapper-self-hosting-content (&key (py-sx-source :as string) (g0-output :as string) (g1-output :as string) (defines-matched :as number) (defines-total :as number) (g0-lines :as number) (g0-bytes :as number) (verification-status :as string))
(~docs/page :title "Self-Hosting Bootstrapper (py.sx)"
(div :class "space-y-8"
(div :class "space-y-3"
@@ -518,8 +518,8 @@ deps.sx depends on: eval")))
;; ---------------------------------------------------------------------------
;; @css bg-green-100 text-green-800 bg-green-50 border-green-200 text-green-700 bg-amber-50 border-amber-200 text-amber-700 text-amber-800 bg-amber-100
(defcomp ~bootstrapper-self-hosting-js-content (&key js-sx-source defines-matched defines-total js-sx-lines verification-status)
(~doc-page :title "Self-Hosting Bootstrapper (js.sx)"
(defcomp ~specs/bootstrapper-self-hosting-js-content (&key js-sx-source defines-matched defines-total js-sx-lines verification-status)
(~docs/page :title "Self-Hosting Bootstrapper (js.sx)"
(div :class "space-y-8"
(div :class "space-y-3"
@@ -627,8 +627,8 @@ deps.sx depends on: eval")))
;; Python bootstrapper detail
;; ---------------------------------------------------------------------------
(defcomp ~bootstrapper-py-content (&key bootstrapper-source bootstrapped-output)
(~doc-page :title "Python Bootstrapper"
(defcomp ~specs/bootstrapper-py-content (&key bootstrapper-source bootstrapped-output)
(~docs/page :title "Python Bootstrapper"
(div :class "space-y-8"
(div :class "space-y-3"
@@ -668,7 +668,7 @@ deps.sx depends on: eval")))
;; Not found
;; ---------------------------------------------------------------------------
(defcomp ~spec-not-found (&key (slug :as string))
(~doc-page :title "Spec Not Found"
(defcomp ~specs/not-found (&key (slug :as string))
(~docs/page :title "Spec Not Found"
(p :class "text-stone-600"
"No specification found for \"" slug "\". This spec may not exist yet.")))

View File

@@ -5,7 +5,7 @@
;; then content fills in as each IO resolves at staggered intervals.
;;
;; The :data expression is an async generator that yields three chunks
;; at 1s, 3s, and 5s. Each chunk resolves a different ~suspense slot.
;; at 1s, 3s, and 5s. Each chunk resolves a different ~shared:pages/suspense slot.
;; Color map for stream chunk styling (all string keys for get compatibility)
(define stream-colors
@@ -21,8 +21,8 @@
;; Generic streamed content chunk — rendered once per yield from the
;; async generator. The :content expression receives different bindings
;; each time, and the _stream_id determines which ~suspense slot it fills.
(defcomp ~streaming-demo-chunk (&key (stream-label :as string) (stream-color :as string) (stream-message :as string) (stream-time :as string))
;; each time, and the _stream_id determines which ~shared:pages/suspense slot it fills.
(defcomp ~streaming-demo/chunk (&key (stream-label :as string) (stream-color :as string) (stream-message :as string) (stream-time :as string))
(let ((colors (get stream-colors stream-color)))
(div :class (str "rounded-lg border p-5 space-y-3 " (get colors "border") " " (get colors "bg"))
(div :class "flex items-center gap-2"
@@ -33,7 +33,7 @@
"Resolved at: " (code :class (str "px-1 rounded " (get colors "code")) stream-time)))))
;; Skeleton placeholder for a stream slot
(defcomp ~stream-skeleton ()
(defcomp ~streaming-demo/stream-skeleton ()
(div :class "rounded-lg border border-stone-200 bg-stone-50 p-5 space-y-3 animate-pulse"
(div :class "flex items-center gap-2"
(div :class "w-3 h-3 rounded-full bg-stone-300")
@@ -42,7 +42,7 @@
(div :class "h-4 bg-stone-200 rounded w-1/2")))
;; Static layout — takes &rest children where the three suspense slots go.
(defcomp ~streaming-demo-layout (&rest children)
(defcomp ~streaming-demo/layout (&rest children)
(div :class "space-y-8"
(div :class "border-b border-stone-200 pb-6"
(h1 :class "text-2xl font-bold text-stone-900" "Streaming & Suspense Demo")
@@ -75,7 +75,7 @@
(h2 :class "text-lg font-semibold text-violet-900" "How Multi-Stream Works")
(ol :class "list-decimal list-inside text-violet-800 space-y-2 text-sm"
(li "Server evaluates " (code ":data") " — gets an " (em "async generator"))
(li "HTML shell with three " (code "~suspense") " placeholders sent immediately")
(li "HTML shell with three " (code "~shared:pages/suspense") " placeholders sent immediately")
(li "Generator yields first chunk after 1s — server sends " (code "__sxResolve(\"stream-fast\", ...)"))
(li "Generator yields second chunk after 3s — " (code "__sxResolve(\"stream-medium\", ...)"))
(li "Generator yields third chunk after 5s — " (code "__sxResolve(\"stream-slow\", ...)"))
@@ -87,7 +87,7 @@
(ul :class "list-disc list-inside text-stone-600 space-y-1"
(li (code "defpage :stream true") " — opts the page into chunked transfer encoding")
(li (code ":data") " helper is an async generator — each " (code "yield") " resolves a different suspense slot")
(li "Each yield includes " (code "_stream_id") " matching a " (code "~suspense :id") " in the shell")
(li "Each yield includes " (code "_stream_id") " matching a " (code "~shared:pages/suspense :id") " in the shell")
(li (code ":content") " expression is re-evaluated with each yield's bindings")
(li "Headers stream concurrently — independent of the data generator")
(li "Future: SSE/WebSocket for re-resolving slots after initial page load")))))

View File

@@ -5,18 +5,18 @@
;; Main documentation page
;; ---------------------------------------------------------------------------
(defcomp ~sx-urls-content ()
(~doc-page :title "SX URLs"
(defcomp ~sx-urls/urls-content ()
(~docs/page :title "SX URLs"
(p :class "text-stone-500 text-sm italic mb-8"
"S-expression URLs — where the address of a thing and the thing itself are the same kind of thing.")
;; -----------------------------------------------------------------
(~doc-section :title "What Is an SX URL?" :id "what"
(~docs/section :title "What Is an SX URL?" :id "what"
(p "An SX URL is a URL whose path is an s-expression. Instead of "
"a flat sequence of slash-separated path segments, the URL encodes "
"a nested function call that the server evaluates to produce a page.")
(p "Every page on this site is addressed by an SX URL. You are currently reading:")
(~doc-code :code (highlight
(~docs/code :code (highlight
"/sx/(applications.(sx-urls))"
"lisp"))
(p "This is a function call: " (code "applications") " is called with the result of "
@@ -25,7 +25,7 @@
"The URL is simultaneously a query, a render instruction, and an address."))
;; -----------------------------------------------------------------
(~doc-section :title "Dots as Spaces" :id "dots"
(~docs/section :title "Dots as Spaces" :id "dots"
(p "Spaces in URLs are ugly — they become " (code "%20") " in the address bar, "
"break when copy-pasted into terminals, and confuse proxies. "
"Dots are unreserved in "
@@ -34,12 +34,12 @@
(p "The rule: " (strong "dot = space, nothing more") ". "
"Before parsing, the server replaces every dot with a space. "
"Parens carry all structural meaning.")
(~doc-code :code (highlight
(~docs/code :code (highlight
";; What you type in the browser:\n/(language.(doc.introduction))\n\n;; After dot→space transform:\n(language (doc introduction))\n\n;; This is standard SX. Parens are nesting. Atoms are arguments.\n;; 'introduction' is a string slug, 'doc' is a function, 'language' is a function."
"lisp"))
(p "More examples:")
(~doc-code :code (highlight
(~docs/code :code (highlight
";; Geography section, hypermedia subsection, examples area, progress bar page\n/(geography.(hypermedia.(example.progress-bar)))\n→ (geography (hypermedia (example progress-bar)))\n\n;; Language section, spec subsection, signals page\n/(language.(spec.signals))\n→ (language (spec signals))\n\n;; Etc section, essay subsection, specific essay\n/(etc.(essay.sx-sucks))\n→ (etc (essay sx-sucks))"
"lisp"))
@@ -47,131 +47,131 @@
"Everything else is standard s-expression parsing."))
;; -----------------------------------------------------------------
(~doc-section :title "Nesting Is Scoping" :id "nesting"
(~docs/section :title "Nesting Is Scoping" :id "nesting"
(p "REST URLs have an inherent ambiguity: "
"does a filter apply to the last segment, or the whole path? "
"S-expression nesting makes scope explicit.")
(~doc-code :code (highlight
(~docs/code :code (highlight
";; REST — ambiguous:\n/users/123/posts?filter=published\n;; Does 'filter' apply to posts? To the user? To the whole query?\n;; The answer depends on API documentation.\n\n;; SX URLs — explicit scoping:\n/(users.(posts.123.(filter.published))) ;; filter scoped to posts\n/(users.posts.123.(filter.published)) ;; filter scoped to the whole expression\n\n;; These are structurally different. The paren boundaries ARE scope boundaries.\n;; No documentation needed — the syntax tells you."
"lisp"))
(p "This extends to every level of nesting on this site:")
(~doc-code :code (highlight
(~docs/code :code (highlight
";; These all have different scoping:\n/(language.(spec.signals)) ;; 'signals' scoped to spec\n/(language.(spec.(explore.signals))) ;; 'signals' scoped to explore\n/(language.(spec.(explore.signals.:section.\"batch\"))) ;; keyword scoped to explore call"
"lisp"))
(p "What took REST thirty years of convention documents to approximate, "
"SX URLs express in the syntax itself."))
;; -----------------------------------------------------------------
(~doc-section :title "The URL Hierarchy" :id "hierarchy"
(~docs/section :title "The URL Hierarchy" :id "hierarchy"
(p "Every URL on this site follows a hierarchical structure. "
"Sections are functions that receive the result of their inner expressions.")
(~doc-subsection :title "Section functions"
(~docs/subsection :title "Section functions"
(p "Top-level sections — " (code "geography") ", " (code "language") ", "
(code "applications") ", " (code "etc") " — are structural. "
"Called with no arguments, they return their index page. "
"Called with content, they pass it through:")
(~doc-code :code (highlight
(~docs/code :code (highlight
"/sx/(geography) ;; geography section index\n/(language) ;; language section index\n/(applications) ;; applications section index\n/(etc) ;; etc section index"
"lisp")))
(~doc-subsection :title "Sub-sections"
(~docs/subsection :title "Sub-sections"
(p "Sub-sections nest inside sections:")
(~doc-code :code (highlight
(~docs/code :code (highlight
"/sx/(geography.(hypermedia)) ;; hypermedia sub-section index\n/(geography.(reactive)) ;; reactive islands sub-section\n/(geography.(isomorphism)) ;; isomorphism sub-section\n/(language.(doc)) ;; documentation sub-section\n/(language.(spec)) ;; specification sub-section\n/(language.(bootstrapper)) ;; bootstrappers sub-section\n/(applications.(cssx)) ;; CSSX sub-section\n/(applications.(protocol)) ;; protocols sub-section"
"lisp")))
(~doc-subsection :title "Leaf pages"
(~docs/subsection :title "Leaf pages"
(p "Leaf pages are the innermost function calls. "
"The slug becomes a string argument to the page function:")
(~doc-code :code (highlight
(~docs/code :code (highlight
";; Documentation pages\n/(language.(doc.introduction)) ;; doc(\"introduction\")\n/(language.(doc.getting-started)) ;; doc(\"getting-started\")\n/(language.(doc.components)) ;; doc(\"components\")\n/(language.(doc.evaluator)) ;; doc(\"evaluator\")\n/(language.(doc.primitives)) ;; doc(\"primitives\")\n/(language.(doc.special-forms)) ;; doc(\"special-forms\")\n/(language.(doc.server-rendering)) ;; doc(\"server-rendering\")\n\n;; Specification pages\n/(language.(spec.core)) ;; spec(\"core\")\n/(language.(spec.parser)) ;; spec(\"parser\")\n/(language.(spec.evaluator)) ;; spec(\"evaluator\")\n/(language.(spec.signals)) ;; spec(\"signals\")\n\n;; Deeply nested pages\n/(language.(spec.(explore.signals))) ;; explore(\"signals\") inside spec\n/(geography.(hypermedia.(example.progress-bar))) ;; example(\"progress-bar\")\n/(geography.(hypermedia.(reference.attributes))) ;; reference(\"attributes\")"
"lisp")))
(~doc-subsection :title "All working URLs on this site"
(~docs/subsection :title "All working URLs on this site"
(p "Every link in the navigation tree is a live SX URL. Here is a sample:")
(~doc-code :code (highlight
(~docs/code :code (highlight
";; Geography\n/(geography.(reactive)) ;; reactive islands overview\n/(geography.(reactive.demo)) ;; live demo\n/(geography.(hypermedia.(reference.attributes))) ;; attribute reference\n/(geography.(hypermedia.(example.click-to-load))) ;; click-to-load example\n/(geography.(hypermedia.(example.infinite-scroll))) ;; infinite scroll\n/(geography.(isomorphism.bundle-analyzer)) ;; bundle analyzer\n/(geography.(isomorphism.streaming)) ;; streaming demo\n\n;; Language\n/(language.(doc.introduction)) ;; getting started\n/(language.(spec.core)) ;; core spec\n/(language.(spec.(explore.signals))) ;; spec explorer: signals\n/(language.(bootstrapper.python)) ;; python bootstrapper\n/(language.(bootstrapper.self-hosting)) ;; self-hosting bootstrapper\n/(language.(test.eval)) ;; evaluator tests\n/(language.(test.router)) ;; router tests\n\n;; Applications\n/(applications.(cssx)) ;; CSSX overview\n/(applications.(cssx.patterns)) ;; CSSX patterns\n/(applications.(protocol.wire-format)) ;; wire format protocol\n/(applications.(sx-urls)) ;; this page\n\n;; Etc\n/(etc.(essay.sx-sucks)) ;; the honest critique\n/(etc.(essay.self-defining-medium)) ;; the metacircular web\n/(etc.(philosophy.sx-manifesto)) ;; the manifesto\n/(etc.(plan.spec-explorer)) ;; spec explorer plan"
"lisp"))))
;; -----------------------------------------------------------------
(~doc-section :title "Direct Component URLs" :id "direct"
(~docs/section :title "Direct Component URLs" :id "direct"
(p "Every " (code "defcomp") " in the component environment is directly "
"addressable by its " (code "~name") " — no page function, no routing wiring, no case statement.")
(~doc-code :code (highlight
";; Any component is instantly a URL:\n/(~essay-sx-sucks) ;; renders the essay component\n/(~plan-sx-urls-content) ;; this documentation page\n/(~docs-evaluator-content) ;; the evaluator docs\n/(~bundle-analyzer-content) ;; the bundle analyzer tool\n/(~reactive-islands-demo-content) ;; the reactive demo\n\n;; This is not a convenience — it's a consequence of the architecture.\n;; Components are values. Values are addressable. The URL evaluator\n;; sees ~name, looks it up in env, evaluates it, wraps in ~sx-doc."
"addressable by its " (code "~plans/content-addressed-components/name") " — no page function, no routing wiring, no case statement.")
(~docs/code :code (highlight
";; Any component is instantly a URL:\n/(~essays/sx-sucks/essay-sx-sucks) ;; renders the essay component\n/(~plans/sx-urls/plan-sx-urls-content) ;; this documentation page\n/(~docs-content/docs-evaluator-content) ;; the evaluator docs\n/(~analyzer/bundle-analyzer-content) ;; the bundle analyzer tool\n/(~reactive-islands/demo/reactive-islands-demo-content) ;; the reactive demo\n\n;; This is not a convenience — it's a consequence of the architecture.\n;; Components are values. Values are addressable. The URL evaluator\n;; sees ~plans/content-addressed-components/name, looks it up in env, evaluates it, wraps in ~sx-doc."
"lisp"))
(p "New components are URL-accessible the moment they are defined. "
"No registration, no routing table update, no deploy. "
"Define a " (code "defcomp") ", refresh the page, visit " (code "/sx/(~your-component)") "."))
;; -----------------------------------------------------------------
(~doc-section :title "Relative URLs" :id "relative"
(~docs/section :title "Relative URLs" :id "relative"
(p "SX URLs support a structural algebra for relative navigation. "
"Instead of manipulating path strings, you navigate the nesting hierarchy. "
"The leading dots tell you how many levels to move.")
(~doc-subsection :title "Same level: one dot"
(~docs/subsection :title "Same level: one dot"
(p "A single dot means \"at the current nesting level\" — append a sibling:")
(~doc-code :code (highlight
(~docs/code :code (highlight
";; Current page: /(geography.(hypermedia.(example.progress-bar)))\n\n(.click-to-load)\n;; → /(geography.(hypermedia.(example.progress-bar.click-to-load)))\n;; Appends 'click-to-load' at the same level as 'progress-bar'\n\n;; In a link:\n(a :href \"(.click-to-load)\" \"Next: Click to Load\")\n;; The client resolves this relative to the current page"
"lisp")))
(~doc-subsection :title "Up one level: two dots"
(~docs/subsection :title "Up one level: two dots"
(p "Two dots pop one level of nesting — like " (code "cd ..") " in a filesystem:")
(~doc-code :code (highlight
(~docs/code :code (highlight
";; Current: /(geography.(hypermedia.(example.progress-bar)))\n\n(..)\n;; → /(geography.(hypermedia.(example)))\n;; Strips the innermost expression, keeps the rest\n\n;; Up and over — pop then append:\n(..inline-edit)\n;; → /(geography.(hypermedia.(example.inline-edit)))\n;; Pop 'progress-bar', append 'inline-edit'\n\n;; Navigate to a sibling sub-section:\n(..reference)\n;; → /(geography.(hypermedia.(reference)))\n;; Pop the 'example' level, land on 'reference'"
"lisp")))
(~doc-subsection :title "Up multiple levels"
(~docs/subsection :title "Up multiple levels"
(p "More dots pop more levels. N dots = pop N-1 levels:")
(~doc-code :code (highlight
(~docs/code :code (highlight
";; Current: /(geography.(hypermedia.(example.progress-bar)))\n\n(...)\n;; → /(geography.(hypermedia))\n;; 3 dots = pop 2 levels (example + progress-bar)\n\n(....)\n;; → /(geography)\n;; 4 dots = pop 3 levels\n\n(.....)\n;; → /\n;; 5 dots = pop 4 levels (all the way to root)"
"lisp")))
(~doc-subsection :title "Up and sideways"
(~docs/subsection :title "Up and sideways"
(p "Combine dots with a slug to navigate up, then into a different branch:")
(~doc-code :code (highlight
(~docs/code :code (highlight
";; Current: /(language.(spec.(explore.signals)))\n\n(..eval)\n;; → /(language.(spec.eval))\n;; Pop one level (explore.signals → spec), append eval\n\n(...doc.introduction)\n;; → /(language.(doc.introduction))\n;; Pop two levels (to language), descend into doc.introduction\n\n(....geography.(reactive.demo))\n;; → /(geography.(reactive.demo))\n;; Pop three levels (to root), into a completely different section"
"lisp")))
(~doc-subsection :title "Bare-dot shorthand"
(~docs/subsection :title "Bare-dot shorthand"
(p "For brevity, the outer parentheses are optional. "
"A URL starting with " (code ".") " is automatically wrapped:")
(~doc-code :code (highlight
(~docs/code :code (highlight
";; These pairs are identical:\n.click-to-load → (.click-to-load)\n.. → (..)\n..inline-edit → (..inline-edit)\n... → (...)\n\n;; In practice:\n(a :href \"..inline-edit\" \"Inline Edit\")\n;; is more natural than:\n(a :href \"(..inline-edit)\" \"Inline Edit\")"
"lisp"))))
;; -----------------------------------------------------------------
(~doc-section :title "Keyword Arguments in URLs" :id "keywords"
(~docs/section :title "Keyword Arguments in URLs" :id "keywords"
(p "URLs can carry keyword arguments — named parameters that modify the innermost expression. "
"Keywords use the same " (code ":name") " syntax as SX keyword arguments.")
(~doc-subsection :title "Setting a keyword"
(~docs/subsection :title "Setting a keyword"
(p "Use " (code ".:key.value") " to set a parameter:")
(~doc-code :code (highlight
(~docs/code :code (highlight
";; Add page number to a spec explorer URL:\n/(language.(spec.(explore.signals.:page.3)))\n→ (language (spec (explore signals :page 3)))\n\n;; Format parameter on a doc page:\n/(language.(doc.primitives.:format.\"print\"))\n→ (language (doc primitives :format \"print\"))\n\n;; Section parameter:\n/(language.(spec.(explore.signals.:section.\"batch\")))\n→ (language (spec (explore signals :section \"batch\")))"
"lisp")))
(~doc-subsection :title "Relative keywords"
(~docs/subsection :title "Relative keywords"
(p "Keywords combine with relative navigation. "
"Set a keyword on the current page without changing the path:")
(~doc-code :code (highlight
(~docs/code :code (highlight
";; Current: /(language.(spec.(explore.signals.:page.1)))\n\n;; Go to page 4 (set keyword, no structural navigation):\n.:page.4\n;; → /(language.(spec.(explore.signals.:page.4)))\n;; One dot + keyword-only = modify current page's keywords\n\n;; Navigate to a sibling AND set a keyword:\n..eval.:page.1\n;; → /(language.(spec.(eval.:page.1)))\n;; Pop one level, append 'eval', set :page to 1"
"lisp")))
(~doc-subsection :title "Delta values"
(~docs/subsection :title "Delta values"
(p "Prefix a keyword value with " (code "+") " or " (code "-") " "
"to apply a numeric delta to the current value:")
(~doc-code :code (highlight
(~docs/code :code (highlight
";; Current: /(language.(spec.(explore.signals.:page.3)))\n\n;; Next page:\n.:page.+1\n;; → /(language.(spec.(explore.signals.:page.4)))\n;; Reads current :page (3), adds 1, sets to 4\n\n;; Previous page:\n.:page.-1\n;; → /(language.(spec.(explore.signals.:page.2)))\n;; Reads current :page (3), subtracts 1, sets to 2\n\n;; Jump forward 5:\n.:page.+5\n;; → /(language.(spec.(explore.signals.:page.8)))"
"lisp"))
(p "This is pagination in the URL algebra. No JavaScript, no event handlers, "
"no state management. Just a relative URL that says \"same page, next page number.\"")))
;; -----------------------------------------------------------------
(~doc-section :title "URL Special Forms" :id "special-forms"
(~docs/section :title "URL Special Forms" :id "special-forms"
(p "Special forms are meta-operations on URLs, distinguished by the "
(code "!") " prefix. They transform how content is resolved or displayed. "
"The " (code "!") " sigil prevents collisions with section or page function names.")
@@ -186,7 +186,7 @@
(tbody :class "text-stone-600"
(tr :class "border-b border-stone-100"
(td :class "py-2 px-3 font-mono text-violet-700" "!source")
(td :class "py-2 px-3 font-mono text-sm" "/sx/(!source.(~essay-sx-sucks))")
(td :class "py-2 px-3 font-mono text-sm" "/sx/(!source.(~essays/sx-sucks/essay-sx-sucks))")
(td :class "py-2 px-3" "Show the defcomp source code"))
(tr :class "border-b border-stone-100"
(td :class "py-2 px-3 font-mono text-violet-700" "!inspect")
@@ -211,82 +211,82 @@
(p "SX has four sigils, each marking a different kind of name:")
(ul :class "space-y-1 text-stone-600 list-disc pl-5"
(li (code "~") " — component (" (code "~card") ", " (code "~essay-sx-sucks") ")")
(li (code "~") " — component (" (code "~card") ", " (code "~essays/sx-sucks/essay-sx-sucks") ")")
(li (code ":") " — keyword (" (code ":title") ", " (code ":page") ")")
(li (code ".") " — relative navigation (" (code ".slug") ", " (code "..") ")")
(li (code "!") " — URL special form (" (code "!source") ", " (code "!inspect") ")")))
;; -----------------------------------------------------------------
(~doc-section :title "SX URLs in Hypermedia" :id "hypermedia"
(~docs/section :title "SX URLs in Hypermedia" :id "hypermedia"
(p "SX URLs integrate naturally with the SX hypermedia system. "
"Every hypermedia attribute accepts SX URLs — absolute, relative, or special forms.")
(~doc-subsection :title "Links"
(~docs/subsection :title "Links"
(p "Standard anchor tags with SX URL hrefs:")
(~doc-code :code (highlight
";; Absolute links (the site navigation uses these):\n(a :href \"/sx/(language.(doc.introduction))\" \"Introduction\")\n(a :href \"/sx/(etc.(essay.sx-sucks))\" \"SX Sucks\")\n\n;; Relative links (navigate from current page):\n(a :href \"..inline-edit\" \"Inline Edit\") ;; up and over\n(a :href \".getting-started\" \"Getting Started\") ;; same level\n(a :href \"..\" \"Back\") ;; up one level\n\n;; Direct component links:\n(a :href \"/sx/(~essay-self-defining-medium)\" \"The True Hypermedium\")"
(~docs/code :code (highlight
";; Absolute links (the site navigation uses these):\n(a :href \"/sx/(language.(doc.introduction))\" \"Introduction\")\n(a :href \"/sx/(etc.(essay.sx-sucks))\" \"SX Sucks\")\n\n;; Relative links (navigate from current page):\n(a :href \"..inline-edit\" \"Inline Edit\") ;; up and over\n(a :href \".getting-started\" \"Getting Started\") ;; same level\n(a :href \"..\" \"Back\") ;; up one level\n\n;; Direct component links:\n(a :href \"/sx/(~essays/self-defining-medium/essay-self-defining-medium)\" \"The True Hypermedium\")"
"lisp")))
(~doc-subsection :title "sx-get — HTMX-style fetching"
(~docs/subsection :title "sx-get — HTMX-style fetching"
(p (code "sx-get") " fetches content from an SX URL and swaps it into the DOM. "
"SX URLs work exactly like path URLs:")
(~doc-code :code (highlight
(~docs/code :code (highlight
";; Fetch and swap a page section:\n(div :sx-get \"/sx/(geography.(hypermedia.(example.progress-bar)))\"\n :sx-trigger \"click\"\n :sx-target \"#content\"\n :sx-swap \"innerHTML\"\n \"Load Progress Bar Example\")\n\n;; Relative sx-get — resolve relative to the current page:\n(div :sx-get \"..inline-edit\"\n :sx-trigger \"click\"\n :sx-target \"#content\"\n :sx-swap \"innerHTML\"\n \"Load Inline Edit\")\n\n;; Paginated content with keyword deltas:\n(button :sx-get \".:page.+1\"\n :sx-trigger \"click\"\n :sx-target \"#results\"\n :sx-swap \"innerHTML\"\n \"Next Page\")"
"lisp")))
(~doc-subsection :title "sx-boost — progressive enhancement"
(~docs/subsection :title "sx-boost — progressive enhancement"
(p (code "sx-boost") " upgrades regular links to use HTMX-style fetch+swap "
"instead of full-page navigation:")
(~doc-code :code (highlight
(~docs/code :code (highlight
";; A navigation list with boosted links:\n(nav :sx-boost \"true\"\n (ul\n (li (a :href \"/sx/(language.(doc.introduction))\" \"Introduction\"))\n (li (a :href \"/sx/(language.(doc.components))\" \"Components\"))\n (li (a :href \"/sx/(language.(doc.evaluator))\" \"Evaluator\"))))\n\n;; Clicking any link fetches content via SX URL and swaps,\n;; rather than triggering a full page load.\n;; URL bar updates. Back button works. No JavaScript needed."
"lisp")))
(~doc-subsection :title "Pagination pattern"
(~docs/subsection :title "Pagination pattern"
(p "The relative URL algebra makes pagination trivial. "
"No state, no event handlers — just URLs:")
(~doc-code :code (highlight
";; Current URL: /(language.(spec.(explore.signals.:page.3)))\n\n(defcomp ~paginator (&key current-page total-pages)\n (nav :class \"flex gap-2\"\n (when (> current-page 1)\n (a :href \".:page.-1\" :class \"px-3 py-1 border rounded\"\n \"Previous\"))\n (span :class \"px-3 py-1\"\n (str \"Page \" current-page \" of \" total-pages))\n (when (< current-page total-pages)\n (a :href \".:page.+1\" :class \"px-3 py-1 border rounded\"\n \"Next\"))))\n\n;; \"Previous\" resolves to .:page.-1 → :page goes from 3 to 2\n;; \"Next\" resolves to .:page.+1 → :page goes from 3 to 4\n;; Each link is a static href. Server renders the right page.\n;; Back button works. Bookmarkable. Shareable. Cacheable."
(~docs/code :code (highlight
";; Current URL: /(language.(spec.(explore.signals.:page.3)))\n\n(defcomp ~sx-urls/paginator (&key current-page total-pages)\n (nav :class \"flex gap-2\"\n (when (> current-page 1)\n (a :href \".:page.-1\" :class \"px-3 py-1 border rounded\"\n \"Previous\"))\n (span :class \"px-3 py-1\"\n (str \"Page \" current-page \" of \" total-pages))\n (when (< current-page total-pages)\n (a :href \".:page.+1\" :class \"px-3 py-1 border rounded\"\n \"Next\"))))\n\n;; \"Previous\" resolves to .:page.-1 → :page goes from 3 to 2\n;; \"Next\" resolves to .:page.+1 → :page goes from 3 to 4\n;; Each link is a static href. Server renders the right page.\n;; Back button works. Bookmarkable. Shareable. Cacheable."
"lisp"))
(p "Compare this to the typical JavaScript pagination component with "
(code "useState") ", " (code "useEffect") ", " (code "onClick") " handlers, "
"URL sync logic, and loading states. "
"In SX, pagination is a URL transform."))
(~doc-subsection :title "Sibling navigation"
(~docs/subsection :title "Sibling navigation"
(p "Navigate between sibling pages using relative two-dot URLs:")
(~doc-code :code (highlight
(~docs/code :code (highlight
";; Current: /(language.(doc.components))\n;; Sibling pages: introduction, getting-started, components, evaluator, ...\n\n(nav :class \"flex justify-between\"\n (a :href \"..getting-started\" :class \"text-violet-600\"\n \"← Getting Started\")\n (a :href \"..evaluator\" :class \"text-violet-600\"\n \"Evaluator →\"))\n\n;; '..getting-started' from /(language.(doc.components))\n;; = pop 'components', append 'getting-started'\n;; = /(language.(doc.getting-started))"
"lisp"))))
;; -----------------------------------------------------------------
(~doc-section :title "The Evaluation Model" :id "eval"
(~docs/section :title "The Evaluation Model" :id "eval"
(p "When the server receives an SX URL, it evaluates it in four steps:")
(~doc-subsection :title "Step 1: Parse"
(~docs/subsection :title "Step 1: Parse"
(p "Strip the leading " (code "/") ", replace dots with spaces, parse as SX:")
(~doc-code :code (highlight
(~docs/code :code (highlight
"/sx/(language.(doc.introduction))\n→ strip / → (language.(doc.introduction))\n→ dots to spaces → (language (doc introduction))\n→ parse → [Symbol(\"language\"), [Symbol(\"doc\"), Symbol(\"introduction\")]]"
"lisp")))
(~doc-subsection :title "Step 2: Auto-quote"
(~docs/subsection :title "Step 2: Auto-quote"
(p "Unknown symbols become strings. Known functions stay as symbols:")
(~doc-code :code (highlight
(~docs/code :code (highlight
";; 'language' is a known section function → stays as Symbol\n;; 'doc' is a known page function → stays as Symbol\n;; 'introduction' is NOT a known function → becomes \"introduction\"\n\n[Symbol(\"language\"), [Symbol(\"doc\"), \"introduction\"]]"
"lisp"))
(p "This is " (strong "soft eval") ": the evaluator treats unknown symbols "
"as self-quoting strings rather than raising an error. "
"Slugs work naturally without explicit quoting."))
(~doc-subsection :title "Step 3: Evaluate"
(~docs/subsection :title "Step 3: Evaluate"
(p "Standard inside-out evaluation:")
(~doc-code :code (highlight
(~docs/code :code (highlight
";; 1. Eval \"introduction\" → \"introduction\" (string, self-evaluating)\n;; 2. Eval (doc \"introduction\") → call doc function → page content AST\n;; 3. Eval (language content) → call language function → passes content through"
"lisp")))
(~doc-subsection :title "Step 4: Wrap"
(p "The router wraps the result in " (code "~sx-doc") " with the URL as " (code ":path") ":")
(~doc-code :code (highlight
";; Final: (~sx-doc :path \"(language (doc introduction))\" <content>)\n;; ~sx-doc provides the layout shell, navigation, breadcrumbs, etc.\n;; The :path tells nav resolution which tree branch to highlight."
(~docs/subsection :title "Step 4: Wrap"
(p "The router wraps the result in " (code "~layouts/doc") " with the URL as " (code ":path") ":")
(~docs/code :code (highlight
";; Final: (~layouts/doc :path \"(language (doc introduction))\" <content>)\n;; ~layouts/doc provides the layout shell, navigation, breadcrumbs, etc.\n;; The :path tells nav resolution which tree branch to highlight."
"lisp")))
(p "The entire routing layer is one function. There is no routing table, "
@@ -294,7 +294,7 @@
"The server evaluates it. The result is a page."))
;; -----------------------------------------------------------------
(~doc-section :title "GraphSX: URLs as Queries" :id "graphsx"
(~docs/section :title "GraphSX: URLs as Queries" :id "graphsx"
(p "The SX URL scheme is not just a routing convention — it is a query language. "
"The structural parallel with "
(a :href "https://graphql.org/" :class "text-violet-600 hover:underline" "GraphQL")
@@ -350,12 +350,12 @@
"shareable, and indexable — everything GraphQL had to sacrifice by using POST."))
;; -----------------------------------------------------------------
(~doc-section :title "Composability" :id "composability"
(~docs/section :title "Composability" :id "composability"
(p "Because URLs are expressions, they compose. "
"The algebra of URLs is the algebra of s-expressions.")
(~doc-code :code (highlight
";; Two specs side by side:\n/(!diff.(language.(spec.signals)).(language.(spec.eval)))\n\n;; Source code of any page:\n/(!source.(~essay-sx-sucks))\n\n;; Inspect a page's dependency graph:\n/(!inspect.(language.(doc.primitives)))\n\n;; Search within a spec:\n/(!search.\"define\".:in.(language.(spec.signals)))\n\n;; The URL bar is a REPL:\n/(about) ;; the about page\n/(!source.(about)) ;; its source code\n\n;; Scoping composes — these are different queries:\n/(users.(posts.123.(filter.published))) ;; filter on posts\n/(users.posts.123.(filter.published)) ;; filter on the whole query"
(~docs/code :code (highlight
";; Two specs side by side:\n/(!diff.(language.(spec.signals)).(language.(spec.eval)))\n\n;; Source code of any page:\n/(!source.(~essays/sx-sucks/essay-sx-sucks))\n\n;; Inspect a page's dependency graph:\n/(!inspect.(language.(doc.primitives)))\n\n;; Search within a spec:\n/(!search.\"define\".:in.(language.(spec.signals)))\n\n;; The URL bar is a REPL:\n/(about) ;; the about page\n/(!source.(about)) ;; its source code\n\n;; Scoping composes — these are different queries:\n/(users.(posts.123.(filter.published))) ;; filter on posts\n/(users.posts.123.(filter.published)) ;; filter on the whole query"
"lisp"))
(p "In the REST world, building a \"diff two specs\" page requires a "
@@ -364,11 +364,11 @@
"In SX, it is a URL."))
;; -----------------------------------------------------------------
(~doc-section :title "Components as Resolvers" :id "resolvers"
(~docs/section :title "Components as Resolvers" :id "resolvers"
(p "The " (code "~") " sigil means \"find and execute this component.\" "
"Components can make onward queries, process results, and return composed content — "
"like server-side includes but with composition.")
(~doc-code :code (highlight
(~docs/code :code (highlight
";; A component that fetches data and returns content:\n/(~get.latest-posts.(limit.5))\n\n;; The flow:\n;; 1. Server finds ~get in env\n;; 2. ~get queries the database (async IO)\n;; 3. Returns composed hypermedia\n;; 4. Router wraps in ~sx-doc\n\n;; Components compose naturally:\n/(~dashboard\n .(~get.latest-posts.(limit.5))\n .(~get.notifications)\n .(~stats.weekly))\n\n;; Each sub-expression is:\n;; - independently cacheable\n;; - independently reusable\n;; - independently testable\n;; Visit /(~get.latest-posts.(limit.5)) to test it in isolation."
"lisp"))
(p "This is what "
@@ -378,7 +378,7 @@
(code "\"use server\"") " pragmas. SX gets it from the evaluator."))
;; -----------------------------------------------------------------
(~doc-section :title "HTTP Alignment" :id "http"
(~docs/section :title "HTTP Alignment" :id "http"
(p "GraphQL sends queries as POST, even though reads are idempotent. "
"This breaks HTTP semantics — POST implies side effects, "
"so caches, CDNs, and intermediaries can't help.")
@@ -391,20 +391,20 @@
(li (strong "Indexable") " — crawlers follow " (code "<a href>") " links")
(li (strong "No client library") " — " (code "curl '/(language.(doc.intro))'") " returns content"))
(~doc-code :code (highlight
(~docs/code :code (highlight
";; HTTP verbs align naturally:\n\n;; GET — pure evaluation, cacheable, bookmarkable:\nGET /(language.(doc.introduction))\n\n;; POST — side effects, mutations:\nPOST /(submit-form)\nPOST /(cart.(add.42))\n\n;; This is what REST always wanted but GraphQL abandoned."
"lisp")))
;; -----------------------------------------------------------------
(~doc-section :title "The Router Spec" :id "spec"
(~docs/section :title "The Router Spec" :id "spec"
(p "SX URLs are not a Python or JavaScript feature — they are specified in SX itself. "
"The " (a :href "/sx/(language.(spec.router))" :class "text-violet-600 hover:underline" "router spec")
" (" (code "router.sx") ") defines all URL parsing, matching, relative resolution, "
"and special form detection as pure functions.")
(p "Key functions from the spec:")
(~doc-code :code (highlight
";; Parse a URL into a typed descriptor:\n(parse-sx-url \"/sx/(language.(doc.intro))\")\n→ {\"type\" \"absolute\" \"raw\" \"/sx/(language.(doc.intro))\"}\n\n(parse-sx-url \"/sx/(!source.(~essay))\")\n→ {\"type\" \"special-form\" \"form\" \"!source\"\n \"inner\" \"(~essay)\" \"raw\" \"/sx/(!source.(~essay))\"}\n\n(parse-sx-url \"/sx/(~essay-sx-sucks)\")\n→ {\"type\" \"direct-component\" \"name\" \"~essay-sx-sucks\"\n \"raw\" \"/sx/(~essay-sx-sucks)\"}\n\n(parse-sx-url \"..eval\")\n→ {\"type\" \"relative\" \"raw\" \"..eval\"}\n\n;; Resolve relative URLs:\n(resolve-relative-url\n \"/sx/(geography.(hypermedia.(example.progress-bar)))\"\n \"..inline-edit\")\n→ \"/sx/(geography.(hypermedia.(example.inline-edit)))\"\n\n;; Keyword delta:\n(resolve-relative-url\n \"/sx/(language.(spec.(explore.signals.:page.3)))\"\n \".:page.+1\")\n→ \"/sx/(language.(spec.(explore.signals.:page.4)))\"\n\n;; Check URL type:\n(relative-sx-url? \"..eval\") → true\n(relative-sx-url? \"(.slug)\") → true\n(relative-sx-url? \"/sx/(foo)\") → false\n\n;; Special form inspection:\n(url-special-form? \"!source\") → true\n(url-special-form? \"inspect\") → false\n(url-special-form-name \"/sx/(!source.(~essay))\") → \"!source\"\n(url-special-form-inner \"/sx/(!source.(~essay))\") → \"(~essay)\""
(~docs/code :code (highlight
";; Parse a URL into a typed descriptor:\n(parse-sx-url \"/sx/(language.(doc.intro))\")\n→ {\"type\" \"absolute\" \"raw\" \"/sx/(language.(doc.intro))\"}\n\n(parse-sx-url \"/sx/(!source.(~essay))\")\n→ {\"type\" \"special-form\" \"form\" \"!source\"\n \"inner\" \"(~essay)\" \"raw\" \"/sx/(!source.(~essay))\"}\n\n(parse-sx-url \"/sx/(~essays/sx-sucks/essay-sx-sucks)\")\n→ {\"type\" \"direct-component\" \"name\" \"~essay-sx-sucks\"\n \"raw\" \"/sx/(~essays/sx-sucks/essay-sx-sucks)\"}\n\n(parse-sx-url \"..eval\")\n→ {\"type\" \"relative\" \"raw\" \"..eval\"}\n\n;; Resolve relative URLs:\n(resolve-relative-url\n \"/sx/(geography.(hypermedia.(example.progress-bar)))\"\n \"..inline-edit\")\n→ \"/sx/(geography.(hypermedia.(example.inline-edit)))\"\n\n;; Keyword delta:\n(resolve-relative-url\n \"/sx/(language.(spec.(explore.signals.:page.3)))\"\n \".:page.+1\")\n→ \"/sx/(language.(spec.(explore.signals.:page.4)))\"\n\n;; Check URL type:\n(relative-sx-url? \"..eval\") → true\n(relative-sx-url? \"(.slug)\") → true\n(relative-sx-url? \"/sx/(foo)\") → false\n\n;; Special form inspection:\n(url-special-form? \"!source\") → true\n(url-special-form? \"inspect\") → false\n(url-special-form-name \"/sx/(!source.(~essay))\") → \"!source\"\n(url-special-form-inner \"/sx/(!source.(~essay))\") → \"(~essay)\""
"lisp"))
(p "These functions are "
@@ -415,9 +415,9 @@
"Python (" (code "sx_ref.py") "), so client and server share the same URL algebra."))
;; -----------------------------------------------------------------
(~doc-section :title "The Lisp Tax" :id "parens"
(~docs/section :title "The Lisp Tax" :id "parens"
(p "People will object to the parentheses. Consider what they already accept:")
(~doc-code :code (highlight
(~docs/code :code (highlight
";; Developers write this every day:\nhttps://api.site.com/v2/users/123/posts?filter=published&sort=date&order=desc&limit=10&offset=20\n\n;; And would complain about this?\nhttps://site.com/(users.(posts.123.(filter.published.sort.date.limit.10)))\n\n;; The second is shorter, structured, unambiguous, and composable."
"lisp"))
@@ -431,10 +431,10 @@
"The parentheses are not a tax — they are the point."))
;; -----------------------------------------------------------------
(~doc-section :title "The Site Is a REPL" :id "repl"
(~docs/section :title "The Site Is a REPL" :id "repl"
(p "The address bar is the input line. The page is the output.")
(~doc-code :code (highlight
"/sx/(about) ;; renders the about page\n/(!source.(about)) ;; returns its source code\n\n/(language.(spec.signals)) ;; the signals spec\n/(!inspect.(language.(spec.signals))) ;; its dependency graph\n\n/(~essay-sx-sucks) ;; the essay\n/(!source.(~essay-sx-sucks)) ;; its s-expression source\n\n;; The website IS a REPL. Every page is a function call.\n;; Every function call is a URL. Evaluation is navigation."
(~docs/code :code (highlight
"/sx/(about) ;; renders the about page\n/(!source.(about)) ;; returns its source code\n\n/(language.(spec.signals)) ;; the signals spec\n/(!inspect.(language.(spec.signals))) ;; its dependency graph\n\n/(~essays/sx-sucks/essay-sx-sucks) ;; the essay\n/(!source.(~essays/sx-sucks/essay-sx-sucks)) ;; its s-expression source\n\n;; The website IS a REPL. Every page is a function call.\n;; Every function call is a URL. Evaluation is navigation."
"lisp"))
(p "You do not need to explain what SX is. "
"Show someone a URL, and they immediately understand the philosophy. "

View File

@@ -6,8 +6,8 @@
;; Overview page
;; ---------------------------------------------------------------------------
(defcomp ~testing-overview-content (&key server-results framework-source eval-source parser-source router-source render-source deps-source engine-source)
(~doc-page :title "Testing"
(defcomp ~testing/overview-content (&key server-results framework-source eval-source parser-source router-source render-source deps-source engine-source)
(~docs/page :title "Testing"
(div :class "space-y-8"
;; Intro
@@ -139,8 +139,8 @@ Per-spec platform functions:
;; Per-spec test page (reusable for eval, parser, router, render)
;; ---------------------------------------------------------------------------
(defcomp ~testing-spec-content (&key (spec-name :as string) (spec-title :as string) (spec-desc :as string) (spec-source :as string) (framework-source :as string) (server-results :as dict?))
(~doc-page :title spec-title
(defcomp ~testing/spec-content (&key (spec-name :as string) (spec-title :as string) (spec-desc :as string) (spec-source :as string) (framework-source :as string) (server-results :as dict?))
(~docs/page :title spec-title
(div :class "space-y-8"
;; Description
@@ -194,8 +194,8 @@ Per-spec platform functions:
;; Runners page
;; ---------------------------------------------------------------------------
(defcomp ~testing-runners-content ()
(~doc-page :title "Test Runners"
(defcomp ~testing/runners-content ()
(~docs/page :title "Test Runners"
(div :class "space-y-8"
(div :class "space-y-4"
@@ -211,7 +211,7 @@ Per-spec platform functions:
(h2 :class "text-2xl font-semibold text-stone-800" "Node.js: run.js")
(div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3"
(h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "Usage")
(~doc-code :code
(~docs/code :code
(highlight "# Run all specs\nnode shared/sx/tests/run.js\n\n# Run specific specs\nnode shared/sx/tests/run.js eval parser\n\n# Legacy mode (monolithic test.sx)\nnode shared/sx/tests/run.js --legacy" "bash")))
(p :class "text-stone-600 text-sm"
"Uses "
@@ -227,7 +227,7 @@ Per-spec platform functions:
(h2 :class "text-2xl font-semibold text-stone-800" "Python: run.py")
(div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3"
(h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "Usage")
(~doc-code :code
(~docs/code :code
(highlight "# Run all specs\npython shared/sx/tests/run.py\n\n# Run specific specs\npython shared/sx/tests/run.py eval parser\n\n# Legacy mode (monolithic test.sx)\npython shared/sx/tests/run.py --legacy" "bash")))
(p :class "text-stone-600 text-sm"
"Uses the hand-written Python evaluator ("

View File

@@ -1,28 +1,28 @@
;; SX docs — documentation page components
(defcomp ~doc-page (&key title &rest children)
(defcomp ~docs/page (&key title &rest children)
(div :class "max-w-4xl mx-auto px-6 py-8"
(div :class "prose prose-stone max-w-none space-y-6" children)))
(defcomp ~doc-section (&key title id &rest children)
(defcomp ~docs/section (&key title id &rest children)
(section :id id :class "space-y-4"
(h2 :class "text-2xl font-semibold text-stone-800" title)
children))
(defcomp ~doc-subsection (&key title &rest children)
(defcomp ~docs/subsection (&key title &rest children)
(div :class "space-y-3"
(h3 :class "text-xl font-semibold text-stone-700" title)
children))
(defcomp ~doc-code (&key code)
(defcomp ~docs/code (&key code)
(div :class "not-prose bg-stone-100 rounded-lg p-5 mx-auto max-w-3xl"
(pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words" (code code))))
(defcomp ~doc-note (&key &rest children)
(defcomp ~docs/note (&key &rest children)
(div :class "border-l-4 border-violet-400 bg-violet-50 p-4 text-stone-700 text-sm"
children))
(defcomp ~doc-table (&key headers rows)
(defcomp ~docs/table (&key headers rows)
(div :class "overflow-x-auto rounded border border-stone-200"
(table :class "w-full text-left text-sm"
(thead
@@ -34,7 +34,7 @@
(map (fn (cell) (td :class "px-3 py-2 text-stone-700" cell)) row)))
rows)))))
(defcomp ~doc-attr-row (&key attr description exists href)
(defcomp ~docs/attr-row (&key attr description exists href)
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm whitespace-nowrap"
(if href
@@ -49,7 +49,7 @@
(span :class "text-emerald-600 text-sm" "yes")
(span :class "text-stone-400 text-sm italic" "not yet")))))
(defcomp ~doc-primitives-table (&key category primitives)
(defcomp ~docs/primitives-table (&key category primitives)
(div :class "space-y-2"
(h4 :class "text-lg font-semibold text-stone-700" category)
(div :class "flex flex-wrap gap-2"
@@ -57,7 +57,7 @@
(span :class "inline-block px-2 py-1 rounded bg-stone-100 font-mono text-sm text-stone-700" p))
primitives))))
(defcomp ~doc-nav (&key items current)
(defcomp ~docs/nav (&key items current)
(nav :class "flex flex-wrap gap-2 mb-8"
(map (fn (item)
(a :href (nth item 1)

View File

@@ -1,6 +1,6 @@
;; SX docs — example and demo components
(defcomp ~example-card (&key title description &rest children)
(defcomp ~examples/card (&key title description &rest children)
(div :class "border border-stone-200 rounded-lg overflow-hidden"
(div :class "bg-stone-100 px-4 py-3 border-b border-stone-200"
(h3 :class "font-semibold text-stone-800" title)
@@ -8,16 +8,16 @@
(p :class "text-sm text-stone-500 mt-1" description)))
(div :class "p-4" children)))
(defcomp ~example-demo (&key &rest children)
(defcomp ~examples/demo (&key &rest children)
(div :class "border border-dashed border-stone-300 rounded p-4 bg-stone-100" children))
(defcomp ~example-source (&key code)
(defcomp ~examples/source (&key code)
(div :class "not-prose bg-stone-100 rounded p-5 mt-3 mx-auto max-w-3xl"
(pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words" (code code))))
;; --- Click to load demo ---
(defcomp ~click-to-load-demo ()
(defcomp ~examples/click-to-load-demo ()
(div :class "space-y-4"
(div :id "click-result" :class "p-4 rounded bg-stone-100 text-stone-500 text-center"
"Click the button to load content.")
@@ -28,7 +28,7 @@
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors"
"Load content")))
(defcomp ~click-result (&key time)
(defcomp ~examples/click-result (&key time)
(div :class "space-y-2"
(p :class "text-stone-800 font-medium" "Content loaded!")
(p :class "text-stone-500 text-sm"
@@ -36,7 +36,7 @@
;; --- Form submission demo ---
(defcomp ~form-demo ()
(defcomp ~examples/form-demo ()
(div :class "space-y-4"
(form
:sx-post "/sx/(geography.(hypermedia.(example.(api.form))))"
@@ -53,14 +53,14 @@
(div :id "form-result" :class "p-3 rounded bg-stone-100 text-stone-500 text-sm text-center"
"Submit the form to see the result.")))
(defcomp ~form-result (&key name)
(defcomp ~examples/form-result (&key name)
(div :class "text-stone-800"
(p (str "Hello, " (if (empty? name) "stranger" name) "!"))
(p :class "text-sm text-stone-500 mt-1" "Submitted via sx-post. The form data was sent as a POST request.")))
;; --- Polling demo ---
(defcomp ~polling-demo ()
(defcomp ~examples/polling-demo ()
(div :class "space-y-4"
(div :id "poll-target"
:sx-get "/sx/(geography.(hypermedia.(example.(api.poll))))"
@@ -69,7 +69,7 @@
:class "p-4 rounded border border-stone-200 bg-stone-100 text-center font-mono"
"Loading...")))
(defcomp ~poll-result (&key time count)
(defcomp ~examples/poll-result (&key time count)
(div
(p :class "text-stone-800 font-medium" (str "Server time: " time))
(p :class "text-stone-500 text-sm mt-1" (str "Poll count: " count))
@@ -82,7 +82,7 @@
;; --- Delete row demo ---
(defcomp ~delete-demo (&key items)
(defcomp ~examples/delete-demo (&key items)
(div
(table :class "w-full text-left text-sm"
(thead
@@ -91,10 +91,10 @@
(th :class "px-3 py-2 font-medium text-stone-600 w-20" "")))
(tbody :id "delete-rows"
(map (fn (item)
(~delete-row :id (nth item 0) :name (nth item 1)))
(~examples/delete-row :id (nth item 0) :name (nth item 1)))
items)))))
(defcomp ~delete-row (&key id name)
(defcomp ~examples/delete-row (&key id name)
(tr :id (str "row-" id) :class "border-b border-stone-100 transition-all"
(td :class "px-3 py-2 text-stone-700" name)
(td :class "px-3 py-2"
@@ -108,11 +108,11 @@
;; --- Inline edit demo ---
(defcomp ~inline-edit-demo ()
(defcomp ~examples/inline-edit-demo ()
(div :id "edit-target" :class "space-y-3"
(~inline-view :value "Click edit to change this text")))
(~examples/inline-view :value "Click edit to change this text")))
(defcomp ~inline-view (&key value)
(defcomp ~examples/inline-view (&key value)
(div :class "flex items-center justify-between p-3 rounded border border-stone-200"
(span :class "text-stone-800" value)
(button
@@ -122,7 +122,7 @@
:class "text-sm text-violet-600 hover:text-violet-800"
"edit")))
(defcomp ~inline-edit-form (&key value)
(defcomp ~examples/inline-edit-form (&key value)
(form
:sx-post "/sx/(geography.(hypermedia.(example.(api.edit))))"
:sx-target "#edit-target"
@@ -142,7 +142,7 @@
;; --- OOB swap demo ---
(defcomp ~oob-demo ()
(defcomp ~examples/oob-demo ()
(div :class "space-y-4"
(div :class "grid grid-cols-2 gap-4"
(div :id "oob-box-a" :class "p-4 rounded border border-stone-200 bg-stone-100 text-center"
@@ -160,7 +160,7 @@
;; --- Lazy loading demo ---
(defcomp ~lazy-loading-demo ()
(defcomp ~examples/lazy-loading-demo ()
(div :class "space-y-4"
(p :class "text-sm text-stone-500" "The content below loads automatically when the page renders.")
(div :id "lazy-target"
@@ -172,14 +172,14 @@
(div :class "h-4 bg-stone-200 rounded w-3/4 mx-auto")
(div :class "h-4 bg-stone-200 rounded w-1/2 mx-auto")))))
(defcomp ~lazy-result (&key time)
(defcomp ~examples/lazy-result (&key time)
(div :class "space-y-2"
(p :class "text-stone-800 font-medium" "Content loaded on page render!")
(p :class "text-stone-500 text-sm" (str "Loaded via sx-trigger=\"load\" at " time))))
;; --- Infinite scroll demo ---
(defcomp ~infinite-scroll-demo ()
(defcomp ~examples/infinite-scroll-demo ()
(div :class "h-64 overflow-y-auto border border-stone-200 rounded" :id "scroll-container"
(div :id "scroll-items"
(map-indexed (fn (i item)
@@ -194,7 +194,7 @@
:class "p-3 text-center text-stone-400 text-sm"
"Loading more..."))))
(defcomp ~scroll-items (&key items page)
(defcomp ~examples/scroll-items (&key items page)
(<>
(map (fn (item)
(div :class "px-4 py-3 border-b border-stone-100 text-sm text-stone-700" item))
@@ -210,7 +210,7 @@
;; --- Progress bar demo ---
(defcomp ~progress-bar-demo ()
(defcomp ~examples/progress-bar-demo ()
(div :class "space-y-4"
(div :id "progress-target" :class "space-y-3"
(div :class "w-full bg-stone-200 rounded-full h-4"
@@ -223,7 +223,7 @@
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
"Start job")))
(defcomp ~progress-status (&key percent job-id)
(defcomp ~examples/progress-status (&key percent job-id)
(div :class "space-y-3"
(div :class "w-full bg-stone-200 rounded-full h-4"
(div :class "bg-violet-600 h-4 rounded-full transition-all"
@@ -239,7 +239,7 @@
;; --- Active search demo ---
(defcomp ~active-search-demo ()
(defcomp ~examples/active-search-demo ()
(div :class "space-y-3"
(input :type "text" :name "q"
:sx-get "/sx/(geography.(hypermedia.(example.(api.search))))"
@@ -251,7 +251,7 @@
(div :id "search-results" :class "border border-stone-200 rounded divide-y divide-stone-100"
(p :class "p-3 text-sm text-stone-400" "Type to search..."))))
(defcomp ~search-results (&key items query)
(defcomp ~examples/search-results (&key items query)
(<>
(if (empty? items)
(p :class "p-3 text-sm text-stone-400" (str "No results for \"" query "\""))
@@ -261,7 +261,7 @@
;; --- Inline validation demo ---
(defcomp ~inline-validation-demo ()
(defcomp ~examples/inline-validation-demo ()
(form :class "space-y-4" :sx-post "/sx/(geography.(hypermedia.(example.(api.validate-submit))))" :sx-target "#validation-result" :sx-swap "innerHTML"
(div
(label :class "block text-sm font-medium text-stone-700 mb-1" "Email")
@@ -277,15 +277,15 @@
"Submit")
(div :id "validation-result")))
(defcomp ~validation-ok (&key email)
(defcomp ~examples/validation-ok (&key email)
(p :class "text-sm text-emerald-600" (str email " is available")))
(defcomp ~validation-error (&key message)
(defcomp ~examples/validation-error (&key message)
(p :class "text-sm text-rose-600" message))
;; --- Value select demo ---
(defcomp ~value-select-demo ()
(defcomp ~examples/value-select-demo ()
(div :class "space-y-3"
(div
(label :class "block text-sm font-medium text-stone-700 mb-1" "Category")
@@ -305,13 +305,13 @@
:class "w-full px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500"
(option :value "" "Select a category first...")))))
(defcomp ~value-options (&key items)
(defcomp ~examples/value-options (&key items)
(<>
(map (fn (item) (option :value item item)) items)))
;; --- Reset on submit demo ---
(defcomp ~reset-on-submit-demo ()
(defcomp ~examples/reset-on-submit-demo ()
(div :class "space-y-3"
(form :id "reset-form"
:sx-post "/sx/(geography.(hypermedia.(example.(api.reset-submit))))"
@@ -327,13 +327,13 @@
(div :id "reset-result" :class "space-y-2"
(p :class "text-sm text-stone-400" "Messages will appear here."))))
(defcomp ~reset-message (&key message time)
(defcomp ~examples/reset-message (&key message time)
(div :class "px-3 py-2 bg-stone-100 rounded text-sm text-stone-700"
(str "[" time "] " message)))
;; --- Edit row demo ---
(defcomp ~edit-row-demo (&key rows)
(defcomp ~examples/edit-row-demo (&key rows)
(div
(table :class "w-full text-left text-sm"
(thead
@@ -344,10 +344,10 @@
(th :class "px-3 py-2 font-medium text-stone-600 w-24" "")))
(tbody :id "edit-rows"
(map (fn (row)
(~edit-row-view :id (nth row 0) :name (nth row 1) :price (nth row 2) :stock (nth row 3)))
(~examples/edit-row-view :id (nth row 0) :name (nth row 1) :price (nth row 2) :stock (nth row 3)))
rows)))))
(defcomp ~edit-row-view (&key id name price stock)
(defcomp ~examples/edit-row-view (&key id name price stock)
(tr :id (str "erow-" id) :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" name)
(td :class "px-3 py-2 text-stone-700" (str "$" price))
@@ -360,7 +360,7 @@
:class "text-sm text-violet-600 hover:text-violet-800"
"edit"))))
(defcomp ~edit-row-form (&key id name price stock)
(defcomp ~examples/edit-row-form (&key id name price stock)
(tr :id (str "erow-" id) :class "border-b border-stone-100 bg-violet-50"
(td :class "px-3 py-2"
(input :type "text" :name "name" :value name
@@ -388,7 +388,7 @@
;; --- Bulk update demo ---
(defcomp ~bulk-update-demo (&key users)
(defcomp ~examples/bulk-update-demo (&key users)
(div :class "space-y-3"
(form :id "bulk-form"
(div :class "flex gap-2 mb-3"
@@ -415,10 +415,10 @@
(th :class "px-3 py-2 font-medium text-stone-600" "Status")))
(tbody :id "bulk-table"
(map (fn (u)
(~bulk-row :id (nth u 0) :name (nth u 1) :email (nth u 2) :status (nth u 3)))
(~examples/bulk-row :id (nth u 0) :name (nth u 1) :email (nth u 2) :status (nth u 3)))
users))))))
(defcomp ~bulk-row (&key id name email status)
(defcomp ~examples/bulk-row (&key id name email status)
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2"
(input :type "checkbox" :name "ids" :value id))
@@ -433,7 +433,7 @@
;; --- Swap positions demo ---
(defcomp ~swap-positions-demo ()
(defcomp ~examples/swap-positions-demo ()
(div :class "space-y-3"
(div :class "flex gap-2"
(button
@@ -459,13 +459,13 @@
:class "border border-stone-200 rounded h-48 overflow-y-auto divide-y divide-stone-100"
(p :class "p-3 text-sm text-stone-400" "Log entries will appear here."))))
(defcomp ~swap-entry (&key time mode)
(defcomp ~examples/swap-entry (&key time mode)
(div :class "px-3 py-2 text-sm text-stone-700"
(str "[" time "] " mode)))
;; --- Select filter demo ---
(defcomp ~select-filter-demo ()
(defcomp ~examples/select-filter-demo ()
(div :class "space-y-3"
(div :class "flex gap-2"
(button
@@ -493,17 +493,17 @@
;; --- Tabs demo ---
(defcomp ~tabs-demo ()
(defcomp ~examples/tabs-demo ()
(div :class "space-y-0"
(div :class "flex border-b border-stone-200" :id "tab-buttons"
(~tab-btn :tab "tab1" :label "Overview" :active "true")
(~tab-btn :tab "tab2" :label "Details" :active "false")
(~tab-btn :tab "tab3" :label "History" :active "false"))
(~examples/tab-btn :tab "tab1" :label "Overview" :active "true")
(~examples/tab-btn :tab "tab2" :label "Details" :active "false")
(~examples/tab-btn :tab "tab3" :label "History" :active "false"))
(div :id "tab-content" :class "p-4 border border-t-0 border-stone-200 rounded-b"
(p :class "text-stone-700" "Welcome to the Overview tab. This content is loaded by default.")
(p :class "text-stone-500 text-sm mt-2" "Click the tabs above to navigate. Watch the browser URL update."))))
(defcomp ~tab-btn (&key tab label active)
(defcomp ~examples/tab-btn (&key tab label active)
(button
:sx-get (str "/sx/(geography.(hypermedia.(example.(api.(tabs." tab ")))))")
:sx-target "#tab-content"
@@ -517,7 +517,7 @@
;; --- Animations demo ---
(defcomp ~animations-demo ()
(defcomp ~examples/animations-demo ()
(div :class "space-y-4"
(button
:sx-get "/sx/(geography.(hypermedia.(example.(api.animate))))"
@@ -528,7 +528,7 @@
(div :id "anim-target" :class "p-4 rounded border border-stone-200 bg-stone-100 text-center"
(p :class "text-stone-400" "Content will fade in here."))))
(defcomp ~anim-result (&key color time)
(defcomp ~examples/anim-result (&key color time)
(div :class "sx-fade-in space-y-2"
(div :class (str "p-4 rounded transition-colors duration-700 " color)
(p :class "font-medium" "Faded in!")
@@ -536,7 +536,7 @@
;; --- Dialogs demo ---
(defcomp ~dialogs-demo ()
(defcomp ~examples/dialogs-demo ()
(div
(button
:sx-get "/sx/(geography.(hypermedia.(example.(api.dialog))))"
@@ -546,7 +546,7 @@
"Open Dialog")
(div :id "dialog-container")))
(defcomp ~dialog-modal (&key title message)
(defcomp ~examples/dialog-modal (&key title message)
(div :class "fixed inset-0 z-50 flex items-center justify-center"
(div :class "absolute inset-0 bg-black/50"
:sx-get "/sx/(geography.(hypermedia.(example.(api.dialog-close))))"
@@ -571,7 +571,7 @@
;; --- Keyboard shortcuts demo ---
(defcomp ~keyboard-shortcuts-demo ()
(defcomp ~examples/keyboard-shortcuts-demo ()
(div :class "space-y-4"
(div :class "p-4 rounded border border-stone-200 bg-stone-100"
(p :class "text-sm text-stone-600 font-medium mb-2" "Keyboard shortcuts:")
@@ -600,18 +600,18 @@
:sx-target "#kbd-target"
:sx-swap "innerHTML")))
(defcomp ~kbd-result (&key key action)
(defcomp ~examples/kbd-result (&key key action)
(div :class "space-y-1"
(p :class "text-stone-800 font-medium" action)
(p :class "text-sm text-stone-500" (str "Triggered by pressing '" key "'"))))
;; --- PUT / PATCH demo ---
(defcomp ~put-patch-demo (&key name email role)
(defcomp ~examples/put-patch-demo (&key name email role)
(div :id "pp-target" :class "space-y-4"
(~pp-view :name name :email email :role role)))
(~examples/pp-view :name name :email email :role role)))
(defcomp ~pp-view (&key name email role)
(defcomp ~examples/pp-view (&key name email role)
(div :class "space-y-3"
(div :class "flex justify-between items-start"
(div
@@ -625,7 +625,7 @@
:class "text-sm text-violet-600 hover:text-violet-800"
"Edit All (PUT)"))))
(defcomp ~pp-form-full (&key name email role)
(defcomp ~examples/pp-form-full (&key name email role)
(form
:sx-put "/sx/(geography.(hypermedia.(example.(api.putpatch))))"
:sx-target "#pp-target"
@@ -656,7 +656,7 @@
;; --- JSON encoding demo ---
(defcomp ~json-encoding-demo ()
(defcomp ~examples/json-encoding-demo ()
(div :class "space-y-4"
(form
:sx-post "/sx/(geography.(hypermedia.(example.(api.json-echo))))"
@@ -678,7 +678,7 @@
(div :id "json-result" :class "p-3 rounded bg-stone-100 text-stone-500 text-sm"
"Submit the form to see the server echo the parsed JSON.")))
(defcomp ~json-result (&key body content-type)
(defcomp ~examples/json-result (&key body content-type)
(div :class "space-y-2"
(p :class "text-stone-800 font-medium" "Server received:")
(pre :class "text-sm bg-stone-100 p-3 rounded overflow-x-auto" (code body))
@@ -686,7 +686,7 @@
;; --- Vals & Headers demo ---
(defcomp ~vals-headers-demo ()
(defcomp ~examples/vals-headers-demo ()
(div :class "space-y-6"
(div :class "space-y-2"
(h4 :class "text-sm font-semibold text-stone-700" "sx-vals — send extra values")
@@ -711,7 +711,7 @@
(div :id "headers-result" :class "p-3 rounded bg-stone-100 text-sm text-stone-400"
"Click to see server-received headers."))))
(defcomp ~echo-result (&key label items)
(defcomp ~examples/echo-result (&key label items)
(div :class "space-y-1"
(p :class "text-stone-800 font-medium" (str "Server received " label ":"))
(map (fn (item)
@@ -720,7 +720,7 @@
;; --- Loading states demo ---
(defcomp ~loading-states-demo ()
(defcomp ~examples/loading-states-demo ()
(div :class "space-y-4"
(button
:sx-get "/sx/(geography.(hypermedia.(example.(api.slow))))"
@@ -732,14 +732,14 @@
(div :id "loading-result" :class "p-4 rounded border border-stone-200 bg-stone-100 text-center"
(p :class "text-stone-400 text-sm" "Click the button — it takes 2 seconds."))))
(defcomp ~loading-result (&key time)
(defcomp ~examples/loading-result (&key time)
(div
(p :class "text-stone-800 font-medium" "Loaded!")
(p :class "text-sm text-stone-500" (str "Response arrived at " time))))
;; --- Sync replace demo (request abort) ---
(defcomp ~sync-replace-demo ()
(defcomp ~examples/sync-replace-demo ()
(div :class "space-y-3"
(input :type "text" :name "q"
:sx-get "/sx/(geography.(hypermedia.(example.(api.slow-search))))"
@@ -752,14 +752,14 @@
(div :id "sync-result" :class "p-4 rounded border border-stone-200 bg-stone-100"
(p :class "text-sm text-stone-400" "Type to trigger requests — stale ones get aborted."))))
(defcomp ~sync-result (&key query delay)
(defcomp ~examples/sync-result (&key query delay)
(div
(p :class "text-stone-800 font-medium" (str "Result for: \"" query "\""))
(p :class "text-sm text-stone-500" (str "Server took " delay "ms to respond"))))
;; --- Retry demo ---
(defcomp ~retry-demo ()
(defcomp ~examples/retry-demo ()
(div :class "space-y-4"
(button
:sx-get "/sx/(geography.(hypermedia.(example.(api.flaky))))"
@@ -771,7 +771,7 @@
(div :id "retry-result" :class "p-4 rounded border border-stone-200 bg-stone-100 text-center"
(p :class "text-stone-400 text-sm" "Endpoint fails twice, succeeds on 3rd attempt."))))
(defcomp ~retry-result (&key attempt message)
(defcomp ~examples/retry-result (&key attempt message)
(div :class "space-y-1"
(p :class "text-stone-800 font-medium" message)
(p :class "text-sm text-stone-500" (str "Succeeded on attempt #" attempt))))

View File

@@ -1,7 +1,7 @@
;; SX docs — home page components
(defcomp ~sx-hero (&key &rest children)
(defcomp ~home/hero (&key &rest children)
(div :class "max-w-4xl mx-auto px-6 py-16 text-center"
(h1 :class "text-5xl font-bold text-stone-900 mb-4"
(span :class "text-violet-600 font-mono" "(<sx>)"))
@@ -14,7 +14,7 @@
(div :class "bg-stone-100 rounded-lg p-6 text-left font-mono text-sm mx-auto max-w-2xl"
(pre :class "leading-relaxed whitespace-pre-wrap" children))))
(defcomp ~sx-philosophy ()
(defcomp ~home/philosophy ()
(div :class "max-w-4xl mx-auto px-6 py-12"
(h2 :class "text-3xl font-bold text-stone-900 mb-8" "Design philosophy")
(div :class "grid md:grid-cols-2 gap-8"
@@ -35,7 +35,7 @@
(li "On-demand CSS — only ship what's used")
(li "DOM morphing for smooth history navigation"))))))
(defcomp ~sx-how-it-works ()
(defcomp ~home/how-it-works ()
(div :class "max-w-4xl mx-auto px-6 py-12"
(h2 :class "text-3xl font-bold text-stone-900 mb-8" "How it works")
(div :class "space-y-6"
@@ -55,7 +55,7 @@
(h3 :class "font-semibold text-stone-900" "Client evaluates + renders")
(p :class "text-stone-600" "sx.js parses, evaluates, and renders to DOM. Same evaluator runs server-side (Python) and client-side (JS)."))))))
(defcomp ~sx-credits ()
(defcomp ~home/credits ()
(div :class "max-w-4xl mx-auto px-6 py-12 border-t border-stone-200"
(p :class "text-stone-500 text-sm"
"sx is heavily inspired by "

View File

@@ -11,7 +11,7 @@
:path "/"
:auth :public
:layout :sx-docs
:content (~sx-doc :path "/" (~sx-home-content)))
:content (~layouts/doc :path "/" (~docs-content/home-content)))
;; ---------------------------------------------------------------------------
;; Language section (parent of Docs, Specs, Bootstrappers, Testing)
@@ -21,7 +21,7 @@
:path "/language/"
:auth :public
:layout :sx-docs
:content (~sx-doc :path "/sx/(language)"))
:content (~layouts/doc :path "/sx/(language)"))
;; ---------------------------------------------------------------------------
;; Docs section (under Language)
@@ -31,24 +31,24 @@
:path "/language/docs/"
:auth :public
:layout :sx-docs
:content (~sx-doc :path "/sx/(language.(doc))" (~docs-introduction-content)))
:content (~layouts/doc :path "/sx/(language.(doc))" (~docs-content/docs-introduction-content)))
(defpage docs-page
:path "/language/docs/<slug>"
:auth :public
:layout :sx-docs
:content (~sx-doc :path (str "/sx/(language.(doc." slug "))")
:content (~layouts/doc :path (str "/sx/(language.(doc." 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))))
"introduction" (~docs-content/docs-introduction-content)
"getting-started" (~docs-content/docs-getting-started-content)
"components" (~docs-content/docs-components-content)
"evaluator" (~docs-content/docs-evaluator-content)
"primitives" (~docs-content/docs-primitives-content
:prims (~docs/primitives-tables :primitives (primitives-data)))
"special-forms" (~docs-content/docs-special-forms-content
:forms (~docs/special-forms-tables :forms (special-forms-data)))
"server-rendering" (~docs-content/docs-server-rendering-content)
:else (~docs-content/docs-introduction-content))))
;; ---------------------------------------------------------------------------
;; Reference section
@@ -58,50 +58,50 @@
:path "/geography/hypermedia/"
:auth :public
:layout :sx-docs
:content (~sx-doc :path "/sx/(geography.(hypermedia))"))
:content (~layouts/doc :path "/sx/(geography.(hypermedia))"))
(defpage reference-index
:path "/geography/hypermedia/reference/"
:auth :public
:layout :sx-docs
:content (~sx-doc :path "/sx/(geography.(hypermedia.(reference)))" (~reference-index-content)))
:content (~layouts/doc :path "/sx/(geography.(hypermedia.(reference)))" (~examples/reference-index-content)))
(defpage reference-page
:path "/geography/hypermedia/reference/<slug>"
:auth :public
:layout :sx-docs
:data (reference-data slug)
:content (~sx-doc :path (str "/sx/(geography.(hypermedia.(reference." slug ")))")
:content (~layouts/doc :path (str "/sx/(geography.(hypermedia.(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
"attributes" (~reference/attrs-content
:req-table (~docs/attr-table-from-data :title "Request Attributes" :attrs req-attrs)
:beh-table (~docs/attr-table-from-data :title "Behavior Attributes" :attrs beh-attrs)
:uniq-table (~docs/attr-table-from-data :title "Unique to sx" :attrs uniq-attrs))
"headers" (~reference/headers-content
:req-table (~docs/headers-table-from-data :title "Request Headers" :headers req-headers)
:resp-table (~docs/headers-table-from-data :title "Response Headers" :headers resp-headers))
"events" (~reference/events-content
:table (~docs/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
"js-api" (~reference/js-api-content
:table (~docs/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)))))
:else (~reference/attrs-content
:req-table (~docs/attr-table-from-data :title "Request Attributes" :attrs req-attrs)
:beh-table (~docs/attr-table-from-data :title "Behavior Attributes" :attrs beh-attrs)
:uniq-table (~docs/attr-table-from-data :title "Unique to sx" :attrs uniq-attrs)))))
(defpage reference-attr-detail
:path "/geography/hypermedia/reference/attributes/<slug>"
:auth :public
:layout :sx-docs
:data (attr-detail-data slug)
:content (~sx-doc :path (str "/sx/(geography.(hypermedia.(reference-detail.attributes." slug ")))")
:content (~layouts/doc :path (str "/sx/(geography.(hypermedia.(reference-detail.attributes." slug ")))")
(if attr-not-found
(~reference-attr-not-found :slug slug)
(~reference-attr-detail-content
(~reference/attr-not-found :slug slug)
(~reference/attr-detail-content
:title attr-title
:description attr-description
:demo attr-demo
@@ -114,10 +114,10 @@
:auth :public
:layout :sx-docs
:data (header-detail-data slug)
:content (~sx-doc :path (str "/sx/(geography.(hypermedia.(reference-detail.headers." slug ")))")
:content (~layouts/doc :path (str "/sx/(geography.(hypermedia.(reference-detail.headers." slug ")))")
(if header-not-found
(~reference-attr-not-found :slug slug)
(~reference-header-detail-content
(~reference/attr-not-found :slug slug)
(~reference/header-detail-content
:title header-title
:direction header-direction
:description header-description
@@ -129,10 +129,10 @@
:auth :public
:layout :sx-docs
:data (event-detail-data slug)
:content (~sx-doc :path (str "/sx/(geography.(hypermedia.(reference-detail.events." slug ")))")
:content (~layouts/doc :path (str "/sx/(geography.(hypermedia.(reference-detail.events." slug ")))")
(if event-not-found
(~reference-attr-not-found :slug slug)
(~reference-event-detail-content
(~reference/attr-not-found :slug slug)
(~reference/event-detail-content
:title event-title
:description event-description
:example-code event-example
@@ -146,7 +146,7 @@
:path "/applications/"
:auth :public
:layout :sx-docs
:content (~sx-doc :path "/sx/(applications)"))
:content (~layouts/doc :path "/sx/(applications)"))
;; ---------------------------------------------------------------------------
;; Protocols section (under Applications)
@@ -156,21 +156,21 @@
:path "/applications/protocols/"
:auth :public
:layout :sx-docs
:content (~sx-doc :path "/sx/(applications.(protocol))" (~protocol-wire-format-content)))
:content (~layouts/doc :path "/sx/(applications.(protocol))" (~protocols/wire-format-content)))
(defpage protocol-page
:path "/applications/protocols/<slug>"
:auth :public
:layout :sx-docs
:content (~sx-doc :path (str "/sx/(applications.(protocol." slug "))")
:content (~layouts/doc :path (str "/sx/(applications.(protocol." 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))))
"wire-format" (~protocols/wire-format-content)
"fragments" (~protocols/fragments-content)
"resolver-io" (~protocols/resolver-io-content)
"internal-services" (~protocols/internal-services-content)
"activitypub" (~protocols/activitypub-content)
"future" (~protocols/future-content)
:else (~protocols/wire-format-content))))
;; ---------------------------------------------------------------------------
;; Examples section
@@ -180,42 +180,42 @@
:path "/geography/hypermedia/examples/"
:auth :public
:layout :sx-docs
:content (~sx-doc :path "/sx/(geography.(hypermedia.(example)))"))
:content (~layouts/doc :path "/sx/(geography.(hypermedia.(example)))"))
(defpage examples-page
:path "/geography/hypermedia/examples/<slug>"
:auth :public
:layout :sx-docs
:content (~sx-doc :path (str "/sx/(geography.(hypermedia.(example." slug ")))")
:content (~layouts/doc :path (str "/sx/(geography.(hypermedia.(example." 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))))
"click-to-load" (~examples-content/example-click-to-load)
"form-submission" (~examples-content/example-form-submission)
"polling" (~examples-content/example-polling)
"delete-row" (~examples-content/example-delete-row)
"inline-edit" (~examples-content/example-inline-edit)
"oob-swaps" (~examples-content/example-oob-swaps)
"lazy-loading" (~examples-content/example-lazy-loading)
"infinite-scroll" (~examples-content/example-infinite-scroll)
"progress-bar" (~examples-content/example-progress-bar)
"active-search" (~examples-content/example-active-search)
"inline-validation" (~examples-content/example-inline-validation)
"value-select" (~examples-content/example-value-select)
"reset-on-submit" (~examples-content/example-reset-on-submit)
"edit-row" (~examples-content/example-edit-row)
"bulk-update" (~examples-content/example-bulk-update)
"swap-positions" (~examples-content/example-swap-positions)
"select-filter" (~examples-content/example-select-filter)
"tabs" (~examples-content/example-tabs)
"animations" (~examples-content/example-animations)
"dialogs" (~examples-content/example-dialogs)
"keyboard-shortcuts" (~examples-content/example-keyboard-shortcuts)
"put-patch" (~examples-content/example-put-patch)
"json-encoding" (~examples-content/example-json-encoding)
"vals-and-headers" (~examples-content/example-vals-and-headers)
"loading-states" (~examples-content/example-loading-states)
"sync-replace" (~examples-content/example-sync-replace)
"retry" (~examples-content/example-retry)
:else (~examples-content/example-click-to-load))))
;; ---------------------------------------------------------------------------
;; Etc section (parent of Essays, Philosophy, Plans)
@@ -225,7 +225,7 @@
:path "/etc/"
:auth :public
:layout :sx-docs
:content (~sx-doc :path "/sx/(etc)"))
:content (~layouts/doc :path "/sx/(etc)"))
;; ---------------------------------------------------------------------------
;; Essays section (under Etc)
@@ -235,33 +235,33 @@
:path "/etc/essays/"
:auth :public
:layout :sx-docs
:content (~sx-doc :path "/sx/(etc.(essay))" (~essays-index-content)))
:content (~layouts/doc :path "/sx/(etc.(essay))" (~essays/index/essays-index-content)))
(defpage essay-page
:path "/etc/essays/<slug>"
:auth :public
:layout :sx-docs
:content (~sx-doc :path (str "/sx/(etc.(essay." slug "))")
:content (~layouts/doc :path (str "/sx/(etc.(essay." 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)
"hegelian-synthesis" (~essay-hegelian-synthesis)
"the-art-chain" (~essay-the-art-chain)
"self-defining-medium" (~essay-self-defining-medium)
:else (~essays-index-content))))
"sx-sucks" (~essays/sx-sucks/essay-sx-sucks)
"why-sexps" (~essays/why-sexps/essay-why-sexps)
"htmx-react-hybrid" (~essays/htmx-react-hybrid/essay-htmx-react-hybrid)
"on-demand-css" (~essays/on-demand-css/essay-on-demand-css)
"client-reactivity" (~essays/client-reactivity/essay-client-reactivity)
"sx-native" (~essays/sx-native/essay-sx-native)
"tail-call-optimization" (~essays/tail-call-optimization/essay-tail-call-optimization)
"continuations" (~essays/continuations/essay-continuations)
"reflexive-web" (~essays/reflexive-web/essay-reflexive-web)
"server-architecture" (~essays/server-architecture/essay-server-architecture)
"separation-of-concerns" (~essays/separation-of-concerns/essay-separation-of-concerns)
"sx-and-ai" (~essays/sx-and-ai/essay-sx-and-ai)
"no-alternative" (~essays/no-alternative/essay-no-alternative)
"zero-tooling" (~essays/zero-tooling/essay-zero-tooling)
"react-is-hypermedia" (~essays/react-is-hypermedia/essay-react-is-hypermedia)
"hegelian-synthesis" (~essays/hegelian-synthesis/essay-hegelian-synthesis)
"the-art-chain" (~essays/the-art-chain/essay-the-art-chain)
"self-defining-medium" (~essays/self-defining-medium/essay-self-defining-medium)
:else (~essays/index/essays-index-content))))
;; ---------------------------------------------------------------------------
;; Philosophy section
@@ -271,20 +271,20 @@
:path "/etc/philosophy/"
:auth :public
:layout :sx-docs
:content (~sx-doc :path "/sx/(etc.(philosophy))" (~philosophy-index-content)))
:content (~layouts/doc :path "/sx/(etc.(philosophy))" (~essays/philosophy-index/content)))
(defpage philosophy-page
:path "/etc/philosophy/<slug>"
:auth :public
:layout :sx-docs
:content (~sx-doc :path (str "/sx/(etc.(philosophy." slug "))")
:content (~layouts/doc :path (str "/sx/(etc.(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))))
"godel-escher-bach" (~essays/godel-escher-bach/essay-godel-escher-bach)
"wittgenstein" (~essays/sx-and-wittgenstein/essay-sx-and-wittgenstein)
"dennett" (~essays/sx-and-dennett/essay-sx-and-dennett)
"existentialism" (~essays/s-existentialism/essay-s-existentialism)
:else (~essays/philosophy-index/content))))
;; ---------------------------------------------------------------------------
;; CSSX section
@@ -294,21 +294,21 @@
:path "/applications/cssx/"
:auth :public
:layout :sx-docs
:content (~sx-doc :path "/sx/(applications.(cssx))" (~cssx-overview-content)))
:content (~layouts/doc :path "/sx/(applications.(cssx))" (~cssx/overview-content)))
(defpage cssx-page
:path "/applications/cssx/<slug>"
:auth :public
:layout :sx-docs
:content (~sx-doc :path (str "/sx/(applications.(cssx." slug "))")
:content (~layouts/doc :path (str "/sx/(applications.(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))))
"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
@@ -318,13 +318,13 @@
:path "/language/specs/"
:auth :public
:layout :sx-docs
:content (~sx-doc :path "/sx/(language.(spec))" (~spec-architecture-content)))
:content (~layouts/doc :path "/sx/(language.(spec))" (~specs/architecture-content)))
(defpage specs-page
:path "/language/specs/<slug>"
:auth :public
:layout :sx-docs
:content (~sx-doc :path (str "/sx/(language.(spec." slug "))")
:content (~layouts/doc :path (str "/sx/(language.(spec." slug "))")
(let ((make-spec-files (fn (items)
(map (fn (item)
(dict :title (get item "title") :desc (get item "desc")
@@ -333,33 +333,33 @@
:source (read-spec-file (get item "filename"))))
items))))
(case slug
"core" (~spec-overview-content
"core" (~specs/overview-content
:spec-title "Core Language"
:spec-files (make-spec-files core-spec-items))
"adapters" (~spec-overview-content
"adapters" (~specs/overview-content
:spec-title "Adapters"
:spec-files (make-spec-files adapter-spec-items))
"browser" (~spec-overview-content
"browser" (~specs/overview-content
:spec-title "Browser Runtime"
:spec-files (make-spec-files browser-spec-items))
"reactive" (~spec-overview-content
"reactive" (~specs/overview-content
:spec-title "Reactive System"
:spec-files (make-spec-files reactive-spec-items))
"host" (~spec-overview-content
"host" (~specs/overview-content
:spec-title "Host Interface"
:spec-files (make-spec-files host-spec-items))
"extensions" (~spec-overview-content
"extensions" (~specs/overview-content
:spec-title "Extensions"
:spec-files (make-spec-files extension-spec-items))
:else (let ((spec (find-spec slug)))
(if spec
(~spec-detail-content
(~specs/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)))))))
(~specs/not-found :slug slug)))))))
;; ---------------------------------------------------------------------------
;; Spec Explorer — structured interactive view of spec files
@@ -369,7 +369,7 @@
:path "/language/specs/explore/<slug>"
:auth :public
:layout :sx-docs
:content (~sx-doc :path (str "/sx/(language.(spec.(explore." slug ")))")
:content (~layouts/doc :path (str "/sx/(language.(spec.(explore." slug ")))")
(let ((spec (find-spec slug)))
(if spec
(let ((data (spec-explorer-data
@@ -377,9 +377,9 @@
(get spec "title")
(get spec "desc"))))
(if data
(~spec-explorer-content :data data)
(~spec-not-found :slug slug)))
(~spec-not-found :slug slug)))))
(~specs-explorer/spec-explorer-content :data data)
(~specs/not-found :slug slug)))
(~specs/not-found :slug slug)))))
;; ---------------------------------------------------------------------------
;; Bootstrappers section
@@ -389,19 +389,19 @@
:path "/language/bootstrappers/"
:auth :public
:layout :sx-docs
:content (~sx-doc :path "/sx/(language.(bootstrapper))" (~bootstrappers-index-content)))
:content (~layouts/doc :path "/sx/(language.(bootstrapper))" (~specs/bootstrappers-index-content)))
(defpage bootstrapper-page
:path "/language/bootstrappers/<slug>"
:auth :public
:layout :sx-docs
:data (bootstrapper-data slug)
:content (~sx-doc :path (str "/sx/(language.(bootstrapper." slug "))")
:content (~layouts/doc :path (str "/sx/(language.(bootstrapper." slug "))")
(if bootstrapper-not-found
(~spec-not-found :slug slug)
(~specs/not-found :slug slug)
(case slug
"self-hosting"
(~bootstrapper-self-hosting-content
(~specs/bootstrapper-self-hosting-content
:py-sx-source py-sx-source
:g0-output g0-output
:g1-output g1-output
@@ -411,18 +411,18 @@
:g0-bytes g0-bytes
:verification-status verification-status)
"self-hosting-js"
(~bootstrapper-self-hosting-js-content
(~specs/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
(~specs/bootstrapper-py-content
:bootstrapper-source bootstrapper-source
:bootstrapped-output bootstrapped-output)
:else
(~bootstrapper-js-content
(~specs/bootstrapper-js-content
:bootstrapper-source bootstrapper-source
:bootstrapped-output bootstrapped-output)))))
@@ -434,15 +434,15 @@
:path "/geography/isomorphism/"
:auth :public
:layout :sx-docs
:content (~sx-doc :path "/sx/(geography.(isomorphism))" (~plan-isomorphic-content)))
:content (~layouts/doc :path "/sx/(geography.(isomorphism))" (~plans/isomorphic/plan-isomorphic-content)))
(defpage bundle-analyzer
:path "/geography/isomorphism/bundle-analyzer"
:auth :public
:layout :sx-docs
:data (bundle-analyzer-data)
:content (~sx-doc :path "/sx/(geography.(isomorphism.bundle-analyzer))"
(~bundle-analyzer-content
:content (~layouts/doc :path "/sx/(geography.(isomorphism.bundle-analyzer))"
(~analyzer/bundle-analyzer-content
:pages pages :total-components total-components :total-macros total-macros
:pure-count pure-count :io-count io-count)))
@@ -451,8 +451,8 @@
:auth :public
:layout :sx-docs
:data (routing-analyzer-data)
:content (~sx-doc :path "/sx/(geography.(isomorphism.routing-analyzer))"
(~routing-analyzer-content
:content (~layouts/doc :path "/sx/(geography.(isomorphism.routing-analyzer))"
(~routing-analyzer/content
:pages pages :total-pages total-pages :client-count client-count
:server-count server-count :registry-sample registry-sample)))
@@ -461,8 +461,8 @@
:auth :public
:layout :sx-docs
:data (data-test-data)
:content (~sx-doc :path "/sx/(geography.(isomorphism.data-test))"
(~data-test-content
:content (~layouts/doc :path "/sx/(geography.(isomorphism.data-test))"
(~data-test/content
:server-time server-time :items items
:phase phase :transport transport)))
@@ -470,20 +470,20 @@
:path "/geography/isomorphism/async-io"
:auth :public
:layout :sx-docs
:content (~sx-doc :path "/sx/(geography.(isomorphism.async-io))" (~async-io-demo-content)))
:content (~layouts/doc :path "/sx/(geography.(isomorphism.async-io))" (~async-io-demo/content)))
(defpage streaming-demo
:path "/geography/isomorphism/streaming"
:auth :public
:stream true
:layout :sx-docs
:shell (~sx-doc :path "/sx/(geography.(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))))
:shell (~layouts/doc :path "/sx/(geography.(isomorphism.streaming))"
(~streaming-demo/layout
(~shared:pages/suspense :id "stream-fast" :fallback (~streaming-demo/stream-skeleton))
(~shared:pages/suspense :id "stream-medium" :fallback (~streaming-demo/stream-skeleton))
(~shared:pages/suspense :id "stream-slow" :fallback (~streaming-demo/stream-skeleton))))
:data (streaming-demo-data)
:content (~streaming-demo-chunk
:content (~streaming-demo/chunk
:stream-label stream-label
:stream-color stream-color
:stream-message stream-message
@@ -494,39 +494,39 @@
:auth :public
:layout :sx-docs
:data (affinity-demo-data)
:content (~sx-doc :path "/sx/(geography.(isomorphism.affinity))"
(~affinity-demo-content :components components :page-plans page-plans)))
:content (~layouts/doc :path "/sx/(geography.(isomorphism.affinity))"
(~affinity-demo/content :components components :page-plans page-plans)))
(defpage optimistic-demo
:path "/geography/isomorphism/optimistic"
:auth :public
:layout :sx-docs
:data (optimistic-demo-data)
:content (~sx-doc :path "/sx/(geography.(isomorphism.optimistic))"
(~optimistic-demo-content :items items :server-time server-time)))
:content (~layouts/doc :path "/sx/(geography.(isomorphism.optimistic))"
(~optimistic-demo/content :items items :server-time server-time)))
(defpage offline-demo
:path "/geography/isomorphism/offline"
:auth :public
:layout :sx-docs
:data (offline-demo-data)
:content (~sx-doc :path "/sx/(geography.(isomorphism.offline))"
(~offline-demo-content :notes notes :server-time server-time)))
:content (~layouts/doc :path "/sx/(geography.(isomorphism.offline))"
(~offline-demo/content :notes notes :server-time server-time)))
;; Wildcard must come AFTER specific routes (first-match routing)
(defpage isomorphism-page
:path "/geography/isomorphism/<slug>"
:auth :public
:layout :sx-docs
:content (~sx-doc :path (str "/sx/(geography.(isomorphism." slug "))")
:content (~layouts/doc :path (str "/sx/(geography.(isomorphism." slug "))")
(case slug
"bundle-analyzer" (~bundle-analyzer-content
"bundle-analyzer" (~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
"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))))
:else (~plans/isomorphic/plan-isomorphic-content))))
;; ---------------------------------------------------------------------------
;; Plans section
@@ -536,7 +536,7 @@
:path "/etc/plans/"
:auth :public
:layout :sx-docs
:content (~sx-doc :path "/sx/(etc.(plan))" (~plans-index-content)))
:content (~layouts/doc :path "/sx/(etc.(plan))" (~plans/index/plans-index-content)))
(defpage plan-page
:path "/etc/plans/<slug>"
@@ -545,38 +545,38 @@
:data (case slug
"theorem-prover" (prove-data)
:else nil)
:content (~sx-doc :path (str "/sx/(etc.(plan." slug "))")
:content (~layouts/doc :path (str "/sx/(etc.(plan." 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)
"sx-forge" (~plan-sx-forge-content)
"sx-swarm" (~plan-sx-swarm-content)
"sx-proxy" (~plan-sx-proxy-content)
"async-eval-convergence" (~plan-async-eval-convergence-content)
"wasm-bytecode-vm" (~plan-wasm-bytecode-vm-content)
"generative-sx" (~plan-generative-sx-content)
"art-dag-sx" (~plan-art-dag-sx-content)
"spec-explorer" (~plan-spec-explorer-content)
"sx-urls" (~plan-sx-urls-content)
"sx-protocol" (~plan-sx-protocol-content)
:else (~plans-index-content))))
"status" (~plans/status/plan-status-content)
"reader-macros" (~plans/reader-macros/plan-reader-macros-content)
"reader-macro-demo" (~plans/reader-macro-demo/plan-reader-macro-demo-content)
"theorem-prover" (~plans/theorem-prover/plan-theorem-prover-content)
"self-hosting-bootstrapper" (~plans/self-hosting-bootstrapper/plan-self-hosting-bootstrapper-content)
"js-bootstrapper" (~plans/js-bootstrapper/plan-js-bootstrapper-content)
"sx-activity" (~plans/sx-activity/plan-sx-activity-content)
"predictive-prefetch" (~plans/predictive-prefetch/plan-predictive-prefetch-content)
"content-addressed-components" (~plans/content-addressed-components/plan-content-addressed-components-content)
"environment-images" (~plans/environment-images/plan-environment-images-content)
"runtime-slicing" (~plans/runtime-slicing/plan-runtime-slicing-content)
"typed-sx" (~plans/typed-sx/plan-typed-sx-content)
"nav-redesign" (~plans/nav-redesign/plan-nav-redesign-content)
"fragment-protocol" (~plans/fragment-protocol/plan-fragment-protocol-content)
"glue-decoupling" (~plans/glue-decoupling/plan-glue-decoupling-content)
"social-sharing" (~plans/social-sharing/plan-social-sharing-content)
"sx-ci" (~plans/sx-ci/plan-sx-ci-content)
"live-streaming" (~plans/live-streaming/plan-live-streaming-content)
"sx-web-platform" (~plans/sx-web-platform/plan-sx-web-platform-content)
"sx-forge" (~plans/sx-forge/plan-sx-forge-content)
"sx-swarm" (~plans/sx-swarm/plan-sx-swarm-content)
"sx-proxy" (~plans/sx-proxy/plan-sx-proxy-content)
"async-eval-convergence" (~plans/async-eval-convergence/plan-async-eval-convergence-content)
"wasm-bytecode-vm" (~plans/wasm-bytecode-vm/plan-wasm-bytecode-vm-content)
"generative-sx" (~plans/generative-sx/plan-generative-sx-content)
"art-dag-sx" (~plans/art-dag-sx/plan-art-dag-sx-content)
"spec-explorer" (~plans/spec-explorer/plan-spec-explorer-content)
"sx-urls" (~plans/sx-urls/plan-sx-urls-content)
"sx-protocol" (~plans/sx-protocol/plan-sx-protocol-content)
:else (~plans/index/plans-index-content))))
;; ---------------------------------------------------------------------------
;; Geography section (parent of Reactive Islands, Hypermedia Lakes, Marshes)
@@ -586,7 +586,7 @@
:path "/geography/"
:auth :public
:layout :sx-docs
:content (~sx-doc :path "/sx/(geography)"))
:content (~layouts/doc :path "/sx/(geography)"))
;; ---------------------------------------------------------------------------
;; Reactive Islands section (under Geography)
@@ -596,20 +596,20 @@
:path "/geography/reactive/"
:auth :public
:layout :sx-docs
:content (~sx-doc :path "/sx/(geography.(reactive))" (~reactive-islands-index-content)))
:content (~layouts/doc :path "/sx/(geography.(reactive))" (~reactive-islands/index/reactive-islands-index-content)))
(defpage reactive-islands-page
:path "/geography/reactive/<slug>"
:auth :public
:layout :sx-docs
:content (~sx-doc :path (str "/sx/(geography.(reactive." slug "))")
:content (~layouts/doc :path (str "/sx/(geography.(reactive." 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))))
"demo" (~reactive-islands/demo/reactive-islands-demo-content)
"event-bridge" (~reactive-islands/event-bridge/reactive-islands-event-bridge-content)
"named-stores" (~reactive-islands/named-stores/reactive-islands-named-stores-content)
"plan" (~reactive-islands/plan/reactive-islands-plan-content)
"phase2" (~reactive-islands/phase2/reactive-islands-phase2-content)
:else (~reactive-islands/index/reactive-islands-index-content))))
;; ---------------------------------------------------------------------------
;; Marshes section (under Geography)
@@ -619,7 +619,7 @@
:path "/geography/marshes/"
:auth :public
:layout :sx-docs
:content (~sx-doc :path "/sx/(geography.(marshes))" (~reactive-islands-marshes-content)))
:content (~layouts/doc :path "/sx/(geography.(marshes))" (~reactive-islands/marshes/reactive-islands-marshes-content)))
;; ---------------------------------------------------------------------------
;; Bootstrapped page helpers demo
@@ -630,8 +630,8 @@
:auth :public
:layout :sx-docs
:data (page-helpers-demo-data)
:content (~sx-doc :path "/sx/(language.(bootstrapper.page-helpers))"
(~page-helpers-demo-content
:content (~layouts/doc :path "/sx/(language.(bootstrapper.page-helpers))"
(~page-helpers-demo/content
:sf-categories sf-categories :sf-total sf-total :sf-ms sf-ms
:ref-sample ref-sample :ref-ms ref-ms
:attr-result attr-result :attr-ms attr-ms
@@ -652,8 +652,8 @@
:auth :public
:layout :sx-docs
:data (run-modular-tests "all")
:content (~sx-doc :path "/sx/(language.(test))"
(~testing-overview-content
:content (~layouts/doc :path "/sx/(language.(test))"
(~testing/overview-content
:server-results server-results
:framework-source framework-source
:eval-source eval-source
@@ -676,57 +676,57 @@
"engine" (run-modular-tests "engine")
"orchestration" (run-modular-tests "orchestration")
:else (dict))
:content (~sx-doc :path (str "/sx/(language.(test." slug "))")
:content (~layouts/doc :path (str "/sx/(language.(test." slug "))")
(case slug
"eval" (~testing-spec-content
"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
"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
"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
"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
"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
"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
"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
"runners" (~testing/runners-content)
:else (~testing/overview-content
:server-results server-results))))

View File

@@ -1372,7 +1372,7 @@ def _data_test_data() -> dict:
async def _streaming_demo_data():
"""Multi-stream demo — yields three chunks at staggered intervals.
Each yield is a dict with _stream_id (matching a ~suspense :id in the
Each yield is a dict with _stream_id (matching a ~shared:pages/suspense :id in the
shell) plus bindings for the :content expression. The streaming
infrastructure detects the async generator and resolves each suspense
placeholder as each chunk arrives.
@@ -1418,8 +1418,8 @@ def _affinity_demo_data() -> dict:
# I/O edge: extract component data and page render plans
env = get_component_env()
demo_names = [
"~aff-demo-auto", "~aff-demo-client", "~aff-demo-server",
"~aff-demo-io-auto", "~aff-demo-io-client",
"~affinity-demo/aff-demo-auto", "~affinity-demo/aff-demo-client", "~affinity-demo/aff-demo-server",
"~affinity-demo/aff-demo-io-auto", "~affinity-demo/aff-demo-io-client",
]
components = []
for name in demo_names:

View File

@@ -5,7 +5,7 @@ Handles URLs like /(language.(doc.introduction)) by:
2. Parsing the path as an SX expression
3. Auto-quoting unknown symbols to strings (slugs)
4. Evaluating the expression against page functions
5. Wrapping the result in (~sx-doc :path "..." content)
5. Wrapping the result in (~layouts/doc :path "..." content)
6. Returning full page or OOB response
Special cases:
@@ -217,7 +217,7 @@ async def eval_sx_url(raw_path: str) -> Any:
# Nav hrefs use /sx/ prefix — reconstruct the full path for nav matching
path_str = f"/sx{raw_path}" if raw_path != "/" else "/sx/"
# Check if expression head is a component (~name) — if so, skip
# Check if expression head is a component (~plans/content-addressed-components/name) — if so, skip
# async_eval and pass directly to _eval_slot. Components contain HTML
# tags that only the aser path can handle, not eval_expr.
head = quoted_expr[0] if isinstance(quoted_expr, list) and quoted_expr else None
@@ -227,7 +227,7 @@ async def eval_sx_url(raw_path: str) -> Any:
)
if is_component_call:
# Direct component URL: /(~essay-sx-sucks) or /(~comp :key val)
# Direct component URL: /(~essays/sx-sucks/essay-sx-sucks) or /(~comp :key val)
# Pass straight to _eval_slot — aser handles component expansion.
page_ast = quoted_expr
else:
@@ -237,7 +237,7 @@ async def eval_sx_url(raw_path: str) -> Any:
# [Symbol("~docs-intro-content")] or quasiquoted trees with data).
# This phase resolves routing + fetches data, but does NOT expand
# components or handle HTML tags (eval_expr can't do that).
# Phase 2: Wrap the returned AST in (~sx-doc :path "..." <ast>) and
# Phase 2: Wrap the returned AST in (~layouts/doc :path "..." <ast>) and
# pass to _eval_slot (aser), which expands components and handles
# HTML tags correctly.
import os
@@ -257,7 +257,7 @@ async def eval_sx_url(raw_path: str) -> Any:
page_ast = [] # empty content for sections with no index
wrapped_ast = [
Symbol("~sx-doc"), Keyword("path"), path_str,
Symbol("~layouts/doc"), Keyword("path"), path_str,
page_ast,
]

View File

@@ -7,7 +7,7 @@
;; sx-get
;; ---------------------------------------------------------------------------
(defcomp ~ref-get-demo ()
(defcomp ~reference/ref-get-demo ()
(div :class "space-y-3"
(button
:sx-get "/sx/(geography.(hypermedia.(reference.(api.time))))"
@@ -23,7 +23,7 @@
;; sx-post
;; ---------------------------------------------------------------------------
(defcomp ~ref-post-demo ()
(defcomp ~reference/ref-post-demo ()
(div :class "space-y-3"
(form
:sx-post "/sx/(geography.(hypermedia.(reference.(api.greet))))"
@@ -43,7 +43,7 @@
;; sx-put
;; ---------------------------------------------------------------------------
(defcomp ~ref-put-demo ()
(defcomp ~reference/ref-put-demo ()
(div :id "ref-put-view"
(div :class "flex items-center justify-between p-3 bg-stone-100 rounded"
(span :class "text-stone-700 text-sm" "Status: " (strong "draft"))
@@ -59,7 +59,7 @@
;; sx-delete
;; ---------------------------------------------------------------------------
(defcomp ~ref-delete-demo ()
(defcomp ~reference/ref-delete-demo ()
(div :class "space-y-2"
(div :id "ref-del-1" :class "flex items-center justify-between p-2 border border-stone-200 rounded"
(span :class "text-sm text-stone-700" "Item A")
@@ -81,7 +81,7 @@
;; sx-patch
;; ---------------------------------------------------------------------------
(defcomp ~ref-patch-demo ()
(defcomp ~reference/ref-patch-demo ()
(div :id "ref-patch-view" :class "space-y-2"
(div :class "p-3 bg-stone-100 rounded"
(span :class "text-stone-700 text-sm" "Theme: " (strong :id "ref-patch-val" "light")))
@@ -99,7 +99,7 @@
;; sx-trigger
;; ---------------------------------------------------------------------------
(defcomp ~ref-trigger-demo ()
(defcomp ~reference/ref-trigger-demo ()
(div :class "space-y-3"
(input :type "text" :name "q" :placeholder "Type to search..."
:sx-get "/sx/(geography.(hypermedia.(reference.(api.trigger-search))))"
@@ -115,7 +115,7 @@
;; sx-target
;; ---------------------------------------------------------------------------
(defcomp ~ref-target-demo ()
(defcomp ~reference/ref-target-demo ()
(div :class "space-y-3"
(div :class "flex gap-2"
(button :sx-get "/sx/(geography.(hypermedia.(reference.(api.time))))"
@@ -138,7 +138,7 @@
;; sx-swap
;; ---------------------------------------------------------------------------
(defcomp ~ref-swap-demo ()
(defcomp ~reference/ref-swap-demo ()
(div :class "space-y-3"
(div :class "flex gap-2 flex-wrap"
(button :sx-get "/sx/(geography.(hypermedia.(reference.(api.swap-item))))"
@@ -158,7 +158,7 @@
;; sx-swap-oob
;; ---------------------------------------------------------------------------
(defcomp ~ref-oob-demo ()
(defcomp ~reference/ref-oob-demo ()
(div :class "space-y-3"
(button :sx-get "/sx/(geography.(hypermedia.(reference.(api.oob))))"
:sx-target "#ref-oob-main"
@@ -177,7 +177,7 @@
;; sx-select
;; ---------------------------------------------------------------------------
(defcomp ~ref-select-demo ()
(defcomp ~reference/ref-select-demo ()
(div :class "space-y-3"
(button :sx-get "/sx/(geography.(hypermedia.(reference.(api.select-page))))"
:sx-target "#ref-select-result"
@@ -193,7 +193,7 @@
;; sx-confirm
;; ---------------------------------------------------------------------------
(defcomp ~ref-confirm-demo ()
(defcomp ~reference/ref-confirm-demo ()
(div :class "space-y-2"
(div :id "ref-confirm-item"
:class "flex items-center justify-between p-3 border border-stone-200 rounded"
@@ -208,7 +208,7 @@
;; sx-push-url
;; ---------------------------------------------------------------------------
(defcomp ~ref-pushurl-demo ()
(defcomp ~reference/ref-pushurl-demo ()
(div :class "space-y-3"
(div :class "flex gap-2"
(a :href "/sx/(geography.(hypermedia.(reference-detail.attributes.sx-get)))"
@@ -230,7 +230,7 @@
;; sx-sync
;; ---------------------------------------------------------------------------
(defcomp ~ref-sync-demo ()
(defcomp ~reference/ref-sync-demo ()
(div :class "space-y-3"
(input :type "text" :name "q" :placeholder "Type quickly..."
:sx-get "/sx/(geography.(hypermedia.(reference.(api.slow-echo))))"
@@ -249,7 +249,7 @@
;; sx-encoding
;; ---------------------------------------------------------------------------
(defcomp ~ref-encoding-demo ()
(defcomp ~reference/ref-encoding-demo ()
(div :class "space-y-3"
(form :sx-post "/sx/(geography.(hypermedia.(reference.(api.upload-name))))"
:sx-encoding "multipart/form-data"
@@ -269,7 +269,7 @@
;; sx-headers
;; ---------------------------------------------------------------------------
(defcomp ~ref-headers-demo ()
(defcomp ~reference/ref-headers-demo ()
(div :class "space-y-3"
(button :sx-get "/sx/(geography.(hypermedia.(reference.(api.echo-headers))))"
:sx-headers {:X-Custom-Token "abc123" :X-Request-Source "demo"}
@@ -285,7 +285,7 @@
;; sx-include
;; ---------------------------------------------------------------------------
(defcomp ~ref-include-demo ()
(defcomp ~reference/ref-include-demo ()
(div :class "space-y-3"
(div :class "flex gap-2 items-end"
(div
@@ -309,7 +309,7 @@
;; sx-vals
;; ---------------------------------------------------------------------------
(defcomp ~ref-vals-demo ()
(defcomp ~reference/ref-vals-demo ()
(div :class "space-y-3"
(button :sx-post "/sx/(geography.(hypermedia.(reference.(api.echo-vals))))"
:sx-vals "{\"source\": \"demo\", \"page\": \"3\"}"
@@ -325,7 +325,7 @@
;; sx-media
;; ---------------------------------------------------------------------------
(defcomp ~ref-media-demo ()
(defcomp ~reference/ref-media-demo ()
(div :class "space-y-3"
(a :href "/sx/(geography.(hypermedia.(reference-detail.attributes.sx-get)))"
:sx-get "/sx/(geography.(hypermedia.(reference-detail.attributes.sx-get)))"
@@ -341,7 +341,7 @@
;; sx-disable
;; ---------------------------------------------------------------------------
(defcomp ~ref-disable-demo ()
(defcomp ~reference/ref-disable-demo ()
(div :class "space-y-3"
(div :class "grid grid-cols-2 gap-3"
(div :class "p-3 border border-stone-200 rounded"
@@ -362,7 +362,7 @@
;; sx-on:*
;; ---------------------------------------------------------------------------
(defcomp ~ref-on-demo ()
(defcomp ~reference/ref-on-demo ()
(div :class "space-y-3"
(button
:sx-on:click "document.getElementById('ref-on-result').textContent = 'Clicked at ' + new Date().toLocaleTimeString()"
@@ -376,7 +376,7 @@
;; sx-retry
;; ---------------------------------------------------------------------------
(defcomp ~ref-retry-demo ()
(defcomp ~reference/ref-retry-demo ()
(div :class "space-y-3"
(button :sx-get "/sx/(geography.(hypermedia.(reference.(api.flaky))))"
:sx-target "#ref-retry-result"
@@ -392,7 +392,7 @@
;; data-sx
;; ---------------------------------------------------------------------------
(defcomp ~ref-data-sx-demo ()
(defcomp ~reference/ref-data-sx-demo ()
(div :class "space-y-3"
(div :data-sx "(div :class \"p-3 bg-violet-50 rounded\" (h3 :class \"font-semibold text-violet-800\" \"Client-rendered\") (p :class \"text-sm text-stone-600\" \"This was evaluated in the browser — no server request.\"))")
(p :class "text-xs text-stone-400" "The content above is rendered client-side from the data-sx attribute.")))
@@ -401,7 +401,7 @@
;; data-sx-env
;; ---------------------------------------------------------------------------
(defcomp ~ref-data-sx-env-demo ()
(defcomp ~reference/ref-data-sx-env-demo ()
(div :class "space-y-3"
(div :data-sx "(div :class \"p-3 bg-emerald-50 rounded\" (h3 :class \"font-semibold text-emerald-800\" title) (p :class \"text-sm text-stone-600\" message))"
:data-sx-env "{\"title\": \"Dynamic content\", \"message\": \"Variables passed via data-sx-env are available in the expression.\"}")
@@ -411,7 +411,7 @@
;; sx-boost
;; ---------------------------------------------------------------------------
(defcomp ~ref-boost-demo ()
(defcomp ~reference/ref-boost-demo ()
(div :class "space-y-3"
(nav :sx-boost "true" :class "flex gap-3"
(a :href "/sx/(geography.(hypermedia.(reference-detail.attributes.sx-get)))"
@@ -431,7 +431,7 @@
;; sx-preload
;; ---------------------------------------------------------------------------
(defcomp ~ref-preload-demo ()
(defcomp ~reference/ref-preload-demo ()
(div :class "space-y-3"
(button
:sx-get "/sx/(geography.(hypermedia.(reference.(api.time))))"
@@ -448,7 +448,7 @@
;; sx-preserve
;; ---------------------------------------------------------------------------
(defcomp ~ref-preserve-demo ()
(defcomp ~reference/ref-preserve-demo ()
(div :class "space-y-3"
(div :class "flex gap-2 items-center"
(button
@@ -469,7 +469,7 @@
;; sx-indicator
;; ---------------------------------------------------------------------------
(defcomp ~ref-indicator-demo ()
(defcomp ~reference/ref-indicator-demo ()
(div :class "space-y-3"
(div :class "flex gap-3 items-center"
(button
@@ -492,7 +492,7 @@
;; sx-validate
;; ---------------------------------------------------------------------------
(defcomp ~ref-validate-demo ()
(defcomp ~reference/ref-validate-demo ()
(div :class "space-y-3"
(form
:sx-post "/sx/(geography.(hypermedia.(reference.(api.greet))))"
@@ -514,7 +514,7 @@
;; sx-ignore
;; ---------------------------------------------------------------------------
(defcomp ~ref-ignore-demo ()
(defcomp ~reference/ref-ignore-demo ()
(div :class "space-y-3"
(button
:sx-get "/sx/(geography.(hypermedia.(reference.(api.time))))"
@@ -534,7 +534,7 @@
;; sx-optimistic
;; ---------------------------------------------------------------------------
(defcomp ~ref-optimistic-demo ()
(defcomp ~reference/ref-optimistic-demo ()
(div :class "space-y-2"
(div :id "ref-opt-item-1"
:class "flex items-center justify-between p-2 border border-stone-200 rounded"
@@ -557,7 +557,7 @@
;; sx-replace-url
;; ---------------------------------------------------------------------------
(defcomp ~ref-replace-url-demo ()
(defcomp ~reference/ref-replace-url-demo ()
(div :class "space-y-3"
(button
:sx-get "/sx/(geography.(hypermedia.(reference.(api.time))))"
@@ -574,7 +574,7 @@
;; sx-disabled-elt
;; ---------------------------------------------------------------------------
(defcomp ~ref-disabled-elt-demo ()
(defcomp ~reference/ref-disabled-elt-demo ()
(div :class "space-y-3"
(div :class "flex gap-3 items-center"
(button :id "ref-diselt-btn"
@@ -594,7 +594,7 @@
;; sx-prompt
;; ---------------------------------------------------------------------------
(defcomp ~ref-prompt-demo ()
(defcomp ~reference/ref-prompt-demo ()
(div :class "space-y-3"
(button
:sx-get "/sx/(geography.(hypermedia.(reference.(api.prompt-echo))))"
@@ -611,7 +611,7 @@
;; sx-params
;; ---------------------------------------------------------------------------
(defcomp ~ref-params-demo ()
(defcomp ~reference/ref-params-demo ()
(div :class "space-y-3"
(form
:sx-post "/sx/(geography.(hypermedia.(reference.(api.echo-vals))))"
@@ -634,7 +634,7 @@
;; sx-sse
;; ---------------------------------------------------------------------------
(defcomp ~ref-sse-demo ()
(defcomp ~reference/ref-sse-demo ()
(div :class "space-y-3"
(div :sx-sse "/sx/(geography.(hypermedia.(reference.(api.sse-time))))"
:sx-sse-swap "time"
@@ -653,7 +653,7 @@
;; SX-Prompt header demo
;; ---------------------------------------------------------------------------
(defcomp ~ref-header-prompt-demo ()
(defcomp ~reference/ref-header-prompt-demo ()
(div :class "space-y-3"
(button
:sx-get "/sx/(geography.(hypermedia.(reference.(api.prompt-echo))))"
@@ -670,7 +670,7 @@
;; SX-Trigger response header demo
;; ---------------------------------------------------------------------------
(defcomp ~ref-header-trigger-demo ()
(defcomp ~reference/ref-header-trigger-demo ()
(div :class "space-y-3"
(button
:sx-get "/sx/(geography.(hypermedia.(reference.(api.trigger-event))))"
@@ -687,7 +687,7 @@
;; SX-Retarget response header demo
;; ---------------------------------------------------------------------------
(defcomp ~ref-header-retarget-demo ()
(defcomp ~reference/ref-header-retarget-demo ()
(div :class "space-y-3"
(button
:sx-get "/sx/(geography.(hypermedia.(reference.(api.retarget))))"
@@ -711,7 +711,7 @@
;; sx:beforeRequest event demo
;; ---------------------------------------------------------------------------
(defcomp ~ref-event-before-request-demo ()
(defcomp ~reference/ref-event-before-request-demo ()
(div :class "space-y-3"
(div :class "flex gap-2 items-center"
(input :id "ref-evt-br-input" :type "text" :placeholder "Type something first..."
@@ -731,7 +731,7 @@
;; sx:afterRequest event demo
;; ---------------------------------------------------------------------------
(defcomp ~ref-event-after-request-demo ()
(defcomp ~reference/ref-event-after-request-demo ()
(div :class "space-y-3"
(button
:sx-get "/sx/(geography.(hypermedia.(reference.(api.time))))"
@@ -751,7 +751,7 @@
;; sx:afterSwap event demo
;; ---------------------------------------------------------------------------
(defcomp ~ref-event-after-swap-demo ()
(defcomp ~reference/ref-event-after-swap-demo ()
(div :class "space-y-3"
(button
:sx-get "/sx/(geography.(hypermedia.(reference.(api.swap-item))))"
@@ -771,7 +771,7 @@
;; sx:responseError event demo
;; ---------------------------------------------------------------------------
(defcomp ~ref-event-response-error-demo ()
(defcomp ~reference/ref-event-response-error-demo ()
(div :class "space-y-3"
(button
:sx-get "/sx/(geography.(hypermedia.(reference.(api.error-500))))"
@@ -793,7 +793,7 @@
;; ---------------------------------------------------------------------------
;; @css invalid:border-red-400
(defcomp ~ref-event-validation-failed-demo ()
(defcomp ~reference/ref-event-validation-failed-demo ()
(div :class "space-y-3"
(form
:sx-post "/sx/(geography.(hypermedia.(reference.(api.greet))))"
@@ -820,7 +820,7 @@
;; sx:requestError event demo
;; ---------------------------------------------------------------------------
(defcomp ~ref-event-request-error-demo ()
(defcomp ~reference/ref-event-request-error-demo ()
(div :class "space-y-3"
(button
:sx-get "https://this-domain-does-not-exist.invalid/api"
@@ -841,7 +841,7 @@
;; sx:clientRoute event demo
;; ---------------------------------------------------------------------------
(defcomp ~ref-event-client-route-demo ()
(defcomp ~reference/ref-event-client-route-demo ()
(div :class "space-y-3"
(p :class "text-sm text-stone-600"
"Open DevTools console, then navigate to a pure page (no :data expression). "
@@ -864,7 +864,7 @@
;; sx:sseOpen event demo
;; ---------------------------------------------------------------------------
(defcomp ~ref-event-sse-open-demo ()
(defcomp ~reference/ref-event-sse-open-demo ()
(div :class "space-y-3"
(div :sx-sse "/sx/(geography.(hypermedia.(reference.(api.sse-time))))"
:sx-sse-swap "time"
@@ -882,7 +882,7 @@
;; sx:sseMessage event demo
;; ---------------------------------------------------------------------------
(defcomp ~ref-event-sse-message-demo ()
(defcomp ~reference/ref-event-sse-message-demo ()
(div :class "space-y-3"
(div :sx-sse "/sx/(geography.(hypermedia.(reference.(api.sse-time))))"
:sx-sse-swap "time"
@@ -900,7 +900,7 @@
;; sx:sseError event demo
;; ---------------------------------------------------------------------------
(defcomp ~ref-event-sse-error-demo ()
(defcomp ~reference/ref-event-sse-error-demo ()
(div :class "space-y-3"
(div :sx-sse "/sx/(geography.(hypermedia.(reference.(api.sse-time))))"
:sx-sse-swap "time"