Phase 3: Client-side routing with SX page registry + routing analyzer demo

Add client-side route matching so pure pages (no IO deps) can render
instantly without a server roundtrip. Page metadata serialized as SX
dict literals (not JSON) in <script type="text/sx-pages"> blocks.

- New router.sx spec: route pattern parsing and matching (6 pure functions)
- boot.sx: process page registry using SX parser at startup
- orchestration.sx: intercept boost links for client routing with
  try-first/fallback — client attempts local eval, falls back to server
- helpers.py: _build_pages_sx() serializes defpage metadata as SX
- Routing analyzer demo page showing per-page client/server classification
- 32 tests for Phase 2 IO detection (scan_io_refs, transitive_io_refs,
  compute_all_io_refs, component_pure?) + fallback/ref parity
- 37 tests for Phase 3 router functions + page registry serialization
- Fix bootstrap_py.py _emit_let cell variable initialization bug
- Fix missing primitive aliases (split, length, merge) in bootstrap_py.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 15:47:56 +00:00
parent 631394989c
commit cf5e767510
16 changed files with 2059 additions and 99 deletions

View File

@@ -615,7 +615,8 @@
(li (strong "CSS on-demand: ") "CSSX resolves keywords to CSS rules, injects only used rules.")
(li (strong "Boundary enforcement: ") "boundary.sx + SX_BOUNDARY_STRICT=1 validates all primitives/IO/helpers at registration.")
(li (strong "Dependency analysis: ") "deps.sx computes per-page component bundles — only definitions a page actually uses are sent.")
(li (strong "IO detection: ") "deps.sx classifies every component as pure or IO-dependent by scanning for boundary primitive references transitively. The spec provides the classification; each host's async evaluator acts on it — expanding IO-dependent components server-side, serializing pure ones for client rendering.")))
(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.")))
;; -----------------------------------------------------------------------
;; Phase 1
@@ -747,46 +748,74 @@
(~doc-section :title "Phase 3: Client-Side Routing (SPA Mode)" :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" "After initial page load, client resolves routes locally using cached components + data. Only hits server for fresh data or unknown routes. Like Next.js client-side navigation."))
(div :class "rounded border border-green-300 bg-green-50 p-4 mb-4"
(div :class "flex items-center gap-2 mb-2"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
(a :href "/specs/router" :class "text-green-700 underline text-sm font-medium" "View canonical spec: router.sx")
(a :href "/isomorphism/routing-analyzer" :class "text-green-700 underline text-sm font-medium" "Live routing analyzer"))
(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 "Current Mechanism"
(p "All routing is server-side via defpage → Quart routes. Client navigates via sx-boost links doing sx-get + morphing. Every navigation = server roundtrip."))
(~doc-subsection :title "Approach"
(~doc-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. Client-side page registry")
(p "Serialize defpage routing info to client:")
(~doc-code :code (highlight "(script :type \"text/sx-pages\")\n;; {\"docs-page\": {\"path\": \"/docs/:slug\", \"auth\": \"public\",\n;; \"content\": \"(case slug ...)\", \"data\": null}}" "lisp")))
(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 ;; \"/docs/hello\" → (\"docs\" \"hello\")\n(define parse-route-pattern ;; \"/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. Client route matcher")
(p "New spec file shared/sx/ref/router.sx — convert /docs/<slug> patterns to matchers. On boost-link click: match URL → if found and pure, evaluate locally. If IO needed: fetch data from server, evaluate content locally. No match: fall through to standard fetch."))
(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 \"/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
(h4 :class "font-semibold text-stone-700" "3. Data endpoint")
(~doc-code :code (highlight "GET /internal/page-data/<page-name>?<params>\n# Returns JSON with evaluated :data expression\n# Reuses execute_page() logic, stops after :data step" "python")))
(h4 :class "font-semibold text-stone-700" "3. Client-side interception (orchestration.sx)")
(p (code "bind-client-route-link") " replaces " (code "bind-boost-link") " in boost processing. On click:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Extract pathname from href")
(li "Call " (code "find-matching-route") " against " (code "_page-routes"))
(li "If match found AND no :data: evaluate content expression locally with component env + URL params")
(li "If evaluation succeeds: swap into #main-panel, pushState, log " (code "\"sx:route client /path\""))
(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."))))
(div
(h4 :class "font-semibold text-stone-700" "4. Layout caching")
(p "Layouts depend on auth/fragments, so cache current layout and reuse across navigations. SX-Layout-Hash header tracks staleness."))
(~doc-subsection :title "What becomes client-routable"
(p "Pages WITHOUT " (code ":data") " that have pure content expressions — most of this docs app:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code "/") ", " (code "/docs/") ", " (code "/docs/<slug>") " (most slugs), " (code "/protocols/") ", " (code "/protocols/<slug>"))
(li (code "/examples/") ", " (code "/examples/<slug>") ", " (code "/essays/") ", " (code "/essays/<slug>"))
(li (code "/plans/") ", " (code "/plans/<slug>") ", " (code "/isomorphism/") ", " (code "/bootstrappers/")))
(p "Pages that fall through to server:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code "/docs/primitives") " and " (code "/docs/special-forms") " (call " (code "primitives-data") " / " (code "special-forms-data") " helpers)")
(li (code "/reference/<slug>") " (has " (code ":data (reference-data slug)") ")")
(li (code "/bootstrappers/<slug>") " (has " (code ":data (bootstrapper-data slug)") ")")
(li (code "/isomorphism/bundle-analyzer") " (has " (code ":data (bundle-analyzer-data)") ")")))
(div
(h4 :class "font-semibold text-stone-700" "5. Integration with orchestration.sx")
(p "Intercept bind-boost-link to try client-side resolution first."))))
(~doc-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") "."))
(div :class "rounded border border-amber-200 bg-amber-50 p-3 mt-2"
(p :class "text-amber-800 text-sm" (strong "Depends on: ") "Phase 1 (client knows which components each page needs), Phase 2 (which pages are pure vs IO)."))
(~doc-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")
(li "shared/sx/ref/orchestration.sx — client route interception")
(li "shared/sx/ref/bootstrap_js.py — router spec module + platform functions")
(li "shared/sx/ref/bootstrap_py.py — router spec module (parity)")
(li "shared/sx/helpers.py — page registry SX serialization")))
(~doc-subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Pure page navigation: zero server requests")
(li "IO page navigation: exactly one data request (not full page fetch)")
(li "Browser back/forward works with client-resolved routes")
(li "Disabling client registry → identical behavior to current"))))
(li "Pure page navigation: zero server requests, console shows \"sx:route client\"")
(li "IO/data page fallback: falls through to server fetch transparently")
(li "Browser back/forward works with client-routed pages")
(li "Disabling page registry → identical behavior to before")
(li "Bootstrap parity: sx_ref.py and sx-ref.js both contain router functions"))))
;; -----------------------------------------------------------------------
;; Phase 4