# 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 **What it enables:** Per-page component bundles instead of sending every definition to every page. Smaller payloads, faster boot, better cache hit rates. **The problem:** `client_components_tag()` in `shared/sx/jinja_bridge.py` serializes ALL entries in `_COMPONENT_ENV`. The `sx_page()` template sends everything or nothing based on a single global hash. No mechanism determines which components a page actually needs. **Approach:** 1. **Transitive closure analyzer** — new module `shared/sx/deps.py` - Walk `Component.body` AST, collect all `Symbol` refs starting with `~` - Recursively follow into their bodies - Handle control forms (`if`/`when`/`cond`/`case`) — include ALL branches - Handle macros — expand during walk using limited eval - Function: `transitive_deps(name: str, env: dict) -> set[str]` - Cache result on `Component` object (invalidate on hot-reload) 2. **Runtime component scanning** — after `_aser` serializes page content, scan the SX string for `(~name` patterns (parallel to existing `scan_classes_from_sx` for CSS). Then compute transitive closure to get sub-components. 3. **Per-page component block** in `sx_page()` — replace all-or-nothing with page-specific bundle. Hash changes per page, localStorage cache keyed by route pattern. 4. **SX partial responses** — `components_for_request()` already diffs against `SX-Components` header. Enhance with transitive closure so only truly needed missing components are sent. **Files:** - New: `shared/sx/deps.py` — dependency analysis - `shared/sx/jinja_bridge.py` — per-page bundle generation, cache deps on Component - `shared/sx/helpers.py` — modify `sx_page()` and `sx_response()` for page-specific bundles - `shared/sx/types.py` — add `deps: set[str]` to Component - `shared/sx/ref/boot.sx` — per-page component caching alongside global cache **Verification:** - Page using 5/50 components → `data-components` block contains only those 5 + transitive deps - No "Unknown component" errors after bundle reduction - Payload size reduction measurable --- ### Phase 2: Smart Server/Client Boundary **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." **Current mechanism:** `_aser` in `async_eval.py` already does partial evaluation — IO primitives are awaited and substituted, HTML tags and component calls serialize as SX. The `_expand_components` context var controls expansion. But this is a global toggle, not per-component. **Approach:** 1. **Automatic IO detection** — extend Phase 1 AST walker to check for references to `IO_PRIMITIVES` names (`frag`, `query`, `service`, `current-user`, etc.) - `has_io_deps(name: str, env: dict) -> bool` - Computed at registration time, cached on Component 2. **Component metadata** — enrich Component with analysis results: ```python ComponentMeta: deps: set[str] # transitive component deps (Phase 1) io_refs: set[str] # IO primitive names referenced is_pure: bool # True if io_refs empty (transitively) ``` 3. **Selective expansion** — refine `_aser` (line ~1335): instead of checking a global `_expand_components` flag, check the component's `is_pure` metadata: - IO-dependent → expand server-side (IO must resolve) - Pure → serialize for client (let client render) - Explicit override: `:server true` on defcomp forces server expansion 4. **Data manifest** for pages — `PageDef` produces a declaration of what IO the page needs, enabling Phase 3 (client can prefetch data) and Phase 5 (streaming). **Files:** - `shared/sx/deps.py` — add IO analysis - `shared/sx/types.py` — add metadata fields to Component - `shared/sx/async_eval.py` — refine `_aser` component expansion logic - `shared/sx/jinja_bridge.py` — compute IO metadata at registration - `shared/sx/pages.py` — data manifest on PageDef **Verification:** - Components calling `(query ...)` classified IO-dependent; pure components classified pure - Existing pages produce identical output (regression) --- ### Phase 3: Client-Side Routing (SPA Mode) **What it enables:** 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. **Current mechanism:** All routing is server-side via `defpage` → Quart routes. Client navigates via `sx-boost` links doing `sx-get` + morphing. Every navigation = server roundtrip. **Approach:** 1. **Client-side page registry** — serialize defpage routing info to client as `