# SX Isomorphic Architecture Roadmap ## Context 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 — same spec, same semantics, both sides. What's missing is the **plumbing** that makes the boundary between server and client a sliding window rather than a fixed wall. The key insight: **s-expressions can partially unfold on the server after IO, then finish unfolding on the client.** The system should be clever enough to know which downstream components have data fetches, resolve those server-side, and send the rest as pure SX for client rendering. Eventually, the client can also do IO (mapping server DB queries to REST calls), handle routing (SPA), and even work offline with cached data. ## Current State (what's solid) - **Primitive parity:** 100%. ~80 pure primitives, same names/semantics, JS and Python. - **eval/parse/render:** Complete both sides. sx-ref.js has eval, parse, render-to-html, render-to-dom, aser. - **Engine:** engine.sx (morph, swaps, triggers, history), orchestration.sx (fetch, events), boot.sx (hydration) — all transpiled. - **Wire format:** Server `_aser` → SX source → client parses → renders to DOM. Boundary is clean. - **Component caching:** Hash-based localStorage for component definitions and style dictionaries. - **CSS on-demand:** CSSX resolves keywords to CSS rules, injects only used rules. - **Boundary enforcement:** `boundary.sx` + `SX_BOUNDARY_STRICT=1` validates all primitives/IO/helpers at registration. ## Architecture Phases --- ### Phase 1: Component Distribution & Dependency Analysis — DONE **What it enables:** Per-page component bundles instead of sending every definition to every page. Smaller payloads, faster boot, better cache hit rates. **Implemented:** 1. **Transitive closure analyzer** — `shared/sx/deps.py` (now `shared/sx/ref/deps.sx`, spec-level) - Walk component body AST, collect all `~name` refs - Recursively follow into their bodies - Handle control forms (`if`/`when`/`cond`/`case`) — include ALL branches - `components_needed(source, env) -> set[str]` 2. **IO reference analysis** — `deps.sx` also tracks IO primitive usage - `scan-io-refs` / `transitive-io-refs` / `component-pure?` - Used by Phase 2 for automatic server/client boundary 3. **Per-page component block** — `_build_pages_sx()` in `helpers.py` - Each page entry includes `:deps` list of required components - Client page registry carries dep info for prefetching 4. **SX partial responses** — `components_for_request()` diffs against `SX-Components` header, sends only missing components **Files:** `shared/sx/ref/deps.sx`, `shared/sx/deps.py`, `shared/sx/helpers.py`, `shared/sx/jinja_bridge.py` --- ### Phase 2: Smart Server/Client Boundary — DONE **What it enables:** Formalized partial evaluation model. Server evaluates IO, serializes pure subtrees. The system automatically knows "this component needs server data" vs "this component is pure and can render anywhere." **Implemented:** 1. **Automatic IO detection** — `deps.sx` walks component bodies for IO primitive refs - `compute-all-io-refs` computes transitive IO analysis for all components - `component-pure?` returns true if no IO refs transitively 2. **Selective expansion** — `_aser` expands known components server-side via `_aser_component` - IO-dependent components expand server-side (IO must resolve) - Unknown components serialize for client rendering - `_expand_components` context var controls override 3. **Component metadata** — computed at registration, cached on Component objects **Files:** `shared/sx/ref/deps.sx`, `shared/sx/async_eval.py`, `shared/sx/jinja_bridge.py` --- ### Phase 3: Client-Side Routing (SPA Mode) — DONE **What it enables:** After initial page load, client resolves routes locally using cached components. Only hits server for fresh data or unknown routes. **Implemented:** 1. **Client-side page registry** — `_build_pages_sx()` serializes defpage routing info - `