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

@@ -416,6 +416,9 @@
"bundle-analyzer" (~bundle-analyzer-content
:pages pages :total-components total-components :total-macros total-macros
:pure-count pure-count :io-count io-count)
"routing-analyzer" (~routing-analyzer-content
:pages pages :total-pages total-pages :client-count client-count
:server-count server-count :registry-sample registry-sample)
:else (~plan-isomorphic-content)))
(defpage bundle-analyzer
@@ -432,6 +435,20 @@
:pages pages :total-components total-components :total-macros total-macros
:pure-count pure-count :io-count io-count))
(defpage routing-analyzer
:path "/isomorphism/routing-analyzer"
:auth :public
:layout (:sx-section
:section "Isomorphism"
:sub-label "Isomorphism"
:sub-href "/isomorphism/"
:sub-nav (~section-nav :items isomorphism-nav-items :current "Routing Analyzer")
:selected "Routing Analyzer")
:data (routing-analyzer-data)
:content (~routing-analyzer-content
:pages pages :total-pages total-pages :client-count client-count
:server-count server-count :registry-sample registry-sample))
;; ---------------------------------------------------------------------------
;; Plans section
;; ---------------------------------------------------------------------------

View File

@@ -22,6 +22,7 @@ def _register_sx_helpers() -> None:
"read-spec-file": _read_spec_file,
"bootstrapper-data": _bootstrapper_data,
"bundle-analyzer-data": _bundle_analyzer_data,
"routing-analyzer-data": _routing_analyzer_data,
})
@@ -342,6 +343,82 @@ def _bundle_analyzer_data() -> dict:
}
def _routing_analyzer_data() -> dict:
"""Compute per-page routing classification for the sx-docs app."""
from shared.sx.pages import get_all_pages
from shared.sx.parser import serialize as sx_serialize
from shared.sx.helpers import _sx_literal
pages_data = []
full_content: list[tuple[str, str, bool]] = [] # (name, full_content, has_data)
client_count = 0
server_count = 0
for name, page_def in sorted(get_all_pages("sx").items()):
has_data = page_def.data_expr is not None
content_src = ""
if page_def.content_expr is not None:
try:
content_src = sx_serialize(page_def.content_expr)
except Exception:
pass
full_content.append((name, content_src, has_data))
# Determine routing mode and reason
if has_data:
mode = "server"
reason = "Has :data expression — needs server IO"
server_count += 1
elif not content_src:
mode = "server"
reason = "No content expression"
server_count += 1
else:
mode = "client"
reason = ""
client_count += 1
pages_data.append({
"name": name,
"path": page_def.path,
"mode": mode,
"has-data": has_data,
"content-expr": content_src[:80] + ("..." if len(content_src) > 80 else ""),
"reason": reason,
})
# Sort: client pages first, then server
pages_data.sort(key=lambda p: (0 if p["mode"] == "client" else 1, p["name"]))
# Build a sample of the SX page registry format (use full content, first 3)
total = client_count + server_count
sample_entries = []
sorted_full = sorted(full_content, key=lambda x: x[0])
for name, csrc, hd in sorted_full[:3]:
page_def = get_all_pages("sx").get(name)
if not page_def:
continue
entry = (
"{:name " + _sx_literal(name)
+ "\n :path " + _sx_literal(page_def.path)
+ "\n :auth " + _sx_literal("public")
+ " :has-data " + ("true" if hd else "false")
+ "\n :content " + _sx_literal(csrc)
+ "\n :closure {}}"
)
sample_entries.append(entry)
registry_sample = "\n\n".join(sample_entries)
return {
"pages": pages_data,
"total-pages": total,
"client-count": client_count,
"server-count": server_count,
"registry-sample": registry_sample,
}
def _attr_detail_data(slug: str) -> dict:
"""Return attribute detail data for a specific attribute slug.