diff --git a/.gitignore b/.gitignore
index 73b8d6a..ff099ff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,5 @@ build/
venv/
_snapshot/
_debug/
+sx-haskell/
+sx-rust/
diff --git a/_config/dev-sh-config.yaml b/_config/dev-sh-config.yaml
new file mode 100644
index 0000000..cbec5ef
--- /dev/null
+++ b/_config/dev-sh-config.yaml
@@ -0,0 +1,86 @@
+root: "/rose-ash-wholefood-coop" # no trailing slash needed (we normalize it)
+host: "https://rose-ash.com"
+base_host: "wholesale.suma.coop"
+base_login: https://wholesale.suma.coop/customer/account/login/
+base_url: https://wholesale.suma.coop/
+title: sx-web
+market_root: /market
+market_title: Market
+blog_root: /
+blog_title: all the news
+cart_root: /cart
+app_urls:
+ blog: "https://blog.rose-ash.com"
+ market: "https://market.rose-ash.com"
+ cart: "https://cart.rose-ash.com"
+ events: "https://events.rose-ash.com"
+ federation: "https://federation.rose-ash.com"
+ account: "https://account.rose-ash.com"
+ sx: "https://sx.rose-ash.com"
+ test: "https://test.rose-ash.com"
+ orders: "https://orders.rose-ash.com"
+cache:
+ fs_root: /app/_snapshot # <- absolute path to your snapshot dir
+categories:
+ allow:
+ Basics: basics
+ Branded Goods: branded-goods
+ Chilled: chilled
+ Frozen: frozen
+ Non-foods: non-foods
+ Supplements: supplements
+ Christmas: christmas
+slugs:
+ skip:
+ - ""
+ - customer
+ - account
+ - checkout
+ - wishlist
+ - sales
+ - contact
+ - privacy-policy
+ - terms-and-conditions
+ - delivery
+ - catalogsearch
+ - quickorder
+ - apply
+ - search
+ - static
+ - media
+section-titles:
+ - ingredients
+ - allergy information
+ - allergens
+ - nutritional information
+ - nutrition
+ - storage
+ - directions
+ - preparation
+ - serving suggestions
+ - origin
+ - country of origin
+ - recycling
+ - general information
+ - additional information
+ - a note about prices
+
+blacklist:
+ category:
+ - branded-goods/alcoholic-drinks
+ - branded-goods/beers
+ - branded-goods/ciders
+ - branded-goods/wines
+ product:
+ - list-price-suma-current-suma-price-list-each-bk012-2-html
+ product-details:
+ - General Information
+ - A Note About Prices
+sumup:
+ merchant_code: "ME4J6100"
+ currency: "GBP"
+ # Name of the environment variable that holds your SumUp API key
+ api_key_env: "SUMUP_API_KEY"
+ webhook_secret: "jfwlekjfwef798ewf769ew8f679ew8f7weflwef"
+
+
diff --git a/dev-sx.sh b/dev-sx.sh
new file mode 100755
index 0000000..e9f347a
--- /dev/null
+++ b/dev-sx.sh
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Dev mode for sx_docs only (standalone, no DB)
+# Bind-mounted source + auto-reload on externalnet
+# Browse to sx.rose-ash.com
+#
+# Usage:
+# ./dev-sx.sh # Start sx_docs dev
+# ./dev-sx.sh down # Stop
+# ./dev-sx.sh logs # Tail logs
+# ./dev-sx.sh --build # Rebuild image then start
+
+COMPOSE="docker compose -p sx-dev -f docker-compose.dev-sx.yml"
+
+case "${1:-up}" in
+ down)
+ $COMPOSE down
+ ;;
+ logs)
+ $COMPOSE logs -f sx_docs
+ ;;
+ *)
+ BUILD_FLAG=""
+ if [[ "${1:-}" == "--build" ]]; then
+ BUILD_FLAG="--build"
+ fi
+ $COMPOSE up $BUILD_FLAG
+ ;;
+esac
diff --git a/docker-compose.dev-sx.yml b/docker-compose.dev-sx.yml
new file mode 100644
index 0000000..abcc300
--- /dev/null
+++ b/docker-compose.dev-sx.yml
@@ -0,0 +1,60 @@
+# Standalone dev mode for sx_docs only
+# Replaces ~/sx-web production stack with bind-mounted source + auto-reload
+# Accessible at sx.rose-ash.com via Caddy on externalnet
+
+services:
+ sx_docs:
+ image: registry.rose-ash.com:5000/sx_docs:latest
+ environment:
+ SX_STANDALONE: "true"
+ SECRET_KEY: "${SECRET_KEY:-sx-dev-secret}"
+ REDIS_URL: redis://redis:6379/0
+ WORKERS: "1"
+ ENVIRONMENT: development
+ RELOAD: "true"
+ SX_USE_REF: "1"
+ SX_BOUNDARY_STRICT: "1"
+ SX_DEV: "1"
+ volumes:
+ - /root/rose-ash/_config/dev-sh-config.yaml:/app/config/app-config.yaml:ro
+ - ./shared:/app/shared
+ - ./sx/app.py:/app/app.py
+ - ./sx/sxc:/app/sxc
+ - ./sx/bp:/app/bp
+ - ./sx/services:/app/services
+ - ./sx/content:/app/content
+ - ./sx/sx:/app/sx
+ - ./sx/path_setup.py:/app/path_setup.py
+ - ./sx/entrypoint.sh:/usr/local/bin/entrypoint.sh
+ - ./sx/__init__.py:/app/__init__.py:ro
+ # sibling models for cross-domain SQLAlchemy imports
+ - ./blog/__init__.py:/app/blog/__init__.py:ro
+ - ./blog/models:/app/blog/models:ro
+ - ./market/__init__.py:/app/market/__init__.py:ro
+ - ./market/models:/app/market/models:ro
+ - ./cart/__init__.py:/app/cart/__init__.py:ro
+ - ./cart/models:/app/cart/models:ro
+ - ./events/__init__.py:/app/events/__init__.py:ro
+ - ./events/models:/app/events/models:ro
+ - ./federation/__init__.py:/app/federation/__init__.py:ro
+ - ./federation/models:/app/federation/models:ro
+ - ./account/__init__.py:/app/account/__init__.py:ro
+ - ./account/models:/app/account/models:ro
+ - ./relations/__init__.py:/app/relations/__init__.py:ro
+ - ./relations/models:/app/relations/models:ro
+ - ./likes/__init__.py:/app/likes/__init__.py:ro
+ - ./likes/models:/app/likes/models:ro
+ - ./orders/__init__.py:/app/orders/__init__.py:ro
+ - ./orders/models:/app/orders/models:ro
+ networks:
+ - externalnet
+ - default
+ restart: unless-stopped
+
+ redis:
+ image: redis:7-alpine
+ restart: unless-stopped
+
+networks:
+ externalnet:
+ external: true
diff --git a/sx/sx/plans/isolated-evaluator.sx b/sx/sx/plans/isolated-evaluator.sx
new file mode 100644
index 0000000..9809f5a
--- /dev/null
+++ b/sx/sx/plans/isolated-evaluator.sx
@@ -0,0 +1,333 @@
+;; ---------------------------------------------------------------------------
+;; Isolated Evaluator — Shared platform layer, isolated JS, Rust WASM
+;; ---------------------------------------------------------------------------
+
+(defcomp ~plans/isolated-evaluator/plan-isolated-evaluator-content ()
+ (~docs/page :title "Isolated Evaluator"
+
+ (~docs/section :title "Context" :id "context"
+ (p "The SX spec is already split into three layers:")
+ (ul :class "list-disc list-inside space-y-1 mt-2"
+ (li (code "spec/") " \u2014 Core language (19 files): evaluator, parser, primitives, CEK, types, continuations. Host-independent.")
+ (li (code "web/") " \u2014 Web framework (20 files): signals, adapters, engine, orchestration, boot, router, deps. Built on core spec.")
+ (li (code "sx/") " \u2014 Application (sx-docs website). Built on web framework."))
+ (p "Bootstrappers search " (code "spec/ \u2192 web/ \u2192 shared/sx/ref/") " for " (code ".sx") " files. The separation is clean.")
+ (p "This plan takes the next step: " (strong "isolate the evaluator from the real world") ". The JS evaluator should run in the same sandbox as Rust/WASM \u2014 unable to touch DOM, fetch, timers, or storage directly. Both evaluators call into a shared " (code "sx-platform.js") " for all browser access.")
+ (p "This also involves sorting out the JavaScript: eliminating hand-coded JS that duplicates specced " (code ".sx") " logic, and moving web framework " (code ".sx") " from compiled-into-evaluator to runtime-evaluated."))
+
+ (~docs/section :title "Existing Architecture" :id "existing"
+ (h4 :class "font-semibold mt-4 mb-2" "Three-layer spec split (DONE)")
+ (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"
+ (th :class "px-3 py-2 font-medium text-stone-600" "Layer")
+ (th :class "px-3 py-2 font-medium text-stone-600" "Directory")
+ (th :class "px-3 py-2 font-medium text-stone-600" "Files")
+ (th :class "px-3 py-2 font-medium text-stone-600" "Content")))
+ (tbody
+ (tr :class "border-b border-stone-100"
+ (td :class "px-3 py-2 text-stone-700" "Core spec")
+ (td :class "px-3 py-2 text-stone-700" (code "spec/"))
+ (td :class "px-3 py-2 text-stone-700" "19")
+ (td :class "px-3 py-2 text-stone-600" "eval, parser, primitives, render, types, CEK, continuations, boundary-core"))
+ (tr :class "border-b border-stone-100"
+ (td :class "px-3 py-2 text-stone-700" "Web framework")
+ (td :class "px-3 py-2 text-stone-700" (code "web/"))
+ (td :class "px-3 py-2 text-stone-700" "20")
+ (td :class "px-3 py-2 text-stone-600" "adapters, signals, engine, orchestration, boot, router, deps, forms, boundary-web"))
+ (tr
+ (td :class "px-3 py-2 text-stone-700" "Application")
+ (td :class "px-3 py-2 text-stone-700" (code "sx/"))
+ (td :class "px-3 py-2 text-stone-700" "\u2014")
+ (td :class "px-3 py-2 text-stone-600" "sx-docs website, page components, content")))))
+
+ (h4 :class "font-semibold mt-4 mb-2" "Rust/WASM evaluator (DONE)")
+ (p (code "sx-rust/") " has a working parser + evaluator + render-to-html in WASM: 9,823 lines generated Rust, 75 real primitives, 154 stubs, 92 tests passing. Currently pure computation \u2014 no DOM interaction.")
+
+ (h4 :class "font-semibold mt-4 mb-2" "What needs to change")
+ (p "Currently " (strong "everything") " (core + web framework) gets compiled into one monolithic " (code "sx-browser.js") ". The web framework " (code ".sx") " files (signals, engine, orchestration, boot, etc.) are baked into the evaluator output by the bootstrapper. They should instead be " (strong "evaluated at runtime") " by the core evaluator, like any other " (code ".sx") " code.")
+ (p "The JavaScript platform primitives (DOM, fetch, timers, storage) are also inlined into the bootstrapped output. They need to be extracted into a standalone " (code "sx-platform.js") " module that both JS and WASM evaluators share."))
+
+
+ ;; -----------------------------------------------------------------------
+ ;; What's core vs web (bootstrapped vs runtime)
+ ;; -----------------------------------------------------------------------
+
+ (~docs/section :title "Bootstrapped vs Runtime-Evaluated" :id "bootstrap-vs-runtime"
+ (p "The key question: what MUST be compiled into the evaluator vs what can be loaded as " (code ".sx") " at runtime?")
+
+ (h4 :class "font-semibold mt-4 mb-2" "Must be bootstrapped (core spec)")
+ (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"
+ (th :class "px-3 py-2 font-medium text-stone-600" "File")
+ (th :class "px-3 py-2 font-medium text-stone-600" "Dir")
+ (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 text-stone-700" (code "eval.sx"))
+ (td :class "px-3 py-2 text-stone-700" "spec/")
+ (td :class "px-3 py-2 text-stone-600" "IS the language \u2014 can't evaluate without it"))
+ (tr :class "border-b border-stone-100"
+ (td :class "px-3 py-2 text-stone-700" (code "parser.sx"))
+ (td :class "px-3 py-2 text-stone-700" "spec/")
+ (td :class "px-3 py-2 text-stone-600" "Can't read .sx source without a parser"))
+ (tr :class "border-b border-stone-100"
+ (td :class "px-3 py-2 text-stone-700" (code "primitives.sx"))
+ (td :class "px-3 py-2 text-stone-700" "spec/")
+ (td :class "px-3 py-2 text-stone-600" "80+ built-in pure functions \u2014 must be native"))
+ (tr :class "border-b border-stone-100"
+ (td :class "px-3 py-2 text-stone-700" (code "render.sx"))
+ (td :class "px-3 py-2 text-stone-700" "spec/")
+ (td :class "px-3 py-2 text-stone-600" "HTML_TAGS registry, parse-element-args"))
+ (tr :class "border-b border-stone-100"
+ (td :class "px-3 py-2 text-stone-700" (code "adapter-html.sx"))
+ (td :class "px-3 py-2 text-stone-700" "web/")
+ (td :class "px-3 py-2 text-stone-600" "render-to-html \u2014 co-recursive with eval-expr"))
+ (tr :class "border-b border-stone-100"
+ (td :class "px-3 py-2 text-stone-700" (code "adapter-sx.sx"))
+ (td :class "px-3 py-2 text-stone-700" "web/")
+ (td :class "px-3 py-2 text-stone-600" "aser (wire format) \u2014 co-recursive with eval-expr"))
+ (tr
+ (td :class "px-3 py-2 text-stone-700" (code "adapter-dom.sx"))
+ (td :class "px-3 py-2 text-stone-700" "web/")
+ (td :class "px-3 py-2 text-stone-600" "render-to-dom \u2014 co-recursive with eval-expr")))))
+
+ (h4 :class "font-semibold mt-4 mb-2" "Runtime-evaluated (web framework)")
+ (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"
+ (th :class "px-3 py-2 font-medium text-stone-600" "File")
+ (th :class "px-3 py-2 font-medium text-stone-600" "Dir")
+ (th :class "px-3 py-2 font-medium text-stone-600" "Why it can be runtime")))
+ (tbody
+ (tr :class "border-b border-stone-100"
+ (td :class "px-3 py-2 text-stone-700" (code "signals.sx"))
+ (td :class "px-3 py-2 text-stone-700" "web/")
+ (td :class "px-3 py-2 text-stone-600" "Pure computation \u2014 dicts with markers, no new types"))
+ (tr :class "border-b border-stone-100"
+ (td :class "px-3 py-2 text-stone-700" (code "engine.sx"))
+ (td :class "px-3 py-2 text-stone-700" "web/")
+ (td :class "px-3 py-2 text-stone-600" "Pure logic \u2014 trigger parsing, swap specs, morph"))
+ (tr :class "border-b border-stone-100"
+ (td :class "px-3 py-2 text-stone-700" (code "orchestration.sx"))
+ (td :class "px-3 py-2 text-stone-700" "web/")
+ (td :class "px-3 py-2 text-stone-600" "Event binding + fetch \u2014 calls platform primitives"))
+ (tr :class "border-b border-stone-100"
+ (td :class "px-3 py-2 text-stone-700" (code "boot.sx"))
+ (td :class "px-3 py-2 text-stone-700" "web/")
+ (td :class "px-3 py-2 text-stone-600" "Page lifecycle \u2014 calls platform primitives"))
+ (tr :class "border-b border-stone-100"
+ (td :class "px-3 py-2 text-stone-700" (code "router.sx"))
+ (td :class "px-3 py-2 text-stone-700" "web/")
+ (td :class "px-3 py-2 text-stone-600" "URL pattern matching \u2014 pure computation"))
+ (tr :class "border-b border-stone-100"
+ (td :class "px-3 py-2 text-stone-700" (code "deps.sx"))
+ (td :class "px-3 py-2 text-stone-700" "web/")
+ (td :class "px-3 py-2 text-stone-600" "Component dependency analysis \u2014 pure AST walking"))
+ (tr :class "border-b border-stone-100"
+ (td :class "px-3 py-2 text-stone-700" (code "page-helpers.sx"))
+ (td :class "px-3 py-2 text-stone-700" "web/")
+ (td :class "px-3 py-2 text-stone-600" "Data transformation helpers"))
+ (tr
+ (td :class "px-3 py-2 text-stone-700" (code "forms.sx"))
+ (td :class "px-3 py-2 text-stone-700" "web/")
+ (td :class "px-3 py-2 text-stone-600" "Server-only definition forms")))))
+
+ (h4 :class "font-semibold mt-4 mb-2" "adapter-dom.sx reactive split")
+ (p (code "adapter-dom.sx") " contains reactive-aware code (" (code "reactive-text") ", " (code "reactive-attr") ", " (code "render-dom-island") ", " (code "render-dom-lake") ") interleaved with core DOM rendering. These call " (code "signal?") " and " (code "deref") " from " (code "signals.sx") " via environment lookup \u2014 no compile-time dependency. Option: split reactive DOM functions into " (code "adapter-dom-reactive.sx") " (web/ layer), keeping base " (code "adapter-dom.sx") " purely about elements/text/fragments/components.")
+
+ (h4 :class "font-semibold mt-4 mb-2" "Hand-coded JS to clean up")
+ (ul :class "list-disc list-inside space-y-2 mt-2"
+ (li (code "CONTINUATIONS_JS") " in " (code "platform_js.py") " \u2014 hand-coded shift/reset. Should use specced " (code "continuations.sx") " or be eliminated if continuations are application-level.")
+ (li (code "ASYNC_IO_JS") " in " (code "platform_js.py") " \u2014 hand-coded async rendering dispatch. Already replaced by " (code "adapter-async.sx") " for Python. JS version should also be bootstrapped or eliminated.")
+ (li "Various wrapper functions in " (code "PLATFORM_BOOT_JS") " that duplicate logic from " (code "boot.sx") ".")))
+
+
+ ;; -----------------------------------------------------------------------
+ ;; Phase 1: Extract sx-platform.js
+ ;; -----------------------------------------------------------------------
+
+ (~docs/section :title "Phase 1: Extract sx-platform.js" :id "phase-1"
+ (p (strong "Goal:") " All real-world-touching JavaScript lives in one standalone module. The evaluator never directly accesses " (code "document") ", " (code "window") ", " (code "fetch") ", " (code "localStorage") ", " (code "history") ", etc.")
+
+ (h4 :class "font-semibold mt-4 mb-2" "Architecture")
+ (~docs/code :code (highlight " \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 sx-platform.js \u2502 \u2190 DOM, fetch, timers, storage\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 \u2502\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 sx-evaluator.js \u2502 \u2502 sx-wasm-shim.js \u2502\n \u2502 (isolated JS) \u2502 \u2502 (WASM instance \u2502\n \u2502 \u2502 \u2502 + handle table) \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518" "text"))
+
+ (h4 :class "font-semibold mt-4 mb-2" "What moves into sx-platform.js")
+ (p "Extracted from " (code "platform_js.py") " string constants:")
+ (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"
+ (th :class "px-3 py-2 font-medium text-stone-600" "Category")
+ (th :class "px-3 py-2 font-medium text-stone-600" "Source")
+ (th :class "px-3 py-2 font-medium text-stone-600" "~Functions")))
+ (tbody
+ (tr :class "border-b border-stone-100"
+ (td :class "px-3 py-2 text-stone-700" "DOM primitives")
+ (td :class "px-3 py-2 text-stone-700" (code "PLATFORM_DOM_JS"))
+ (td :class "px-3 py-2 text-stone-600" "~50 (createElement, setAttribute, appendChild...)"))
+ (tr :class "border-b border-stone-100"
+ (td :class "px-3 py-2 text-stone-700" "Engine platform")
+ (td :class "px-3 py-2 text-stone-700" (code "PLATFORM_ENGINE_PURE_JS"))
+ (td :class "px-3 py-2 text-stone-600" "~6 (locationHref, pushState, nowMs...)"))
+ (tr :class "border-b border-stone-100"
+ (td :class "px-3 py-2 text-stone-700" "Orchestration platform")
+ (td :class "px-3 py-2 text-stone-700" (code "PLATFORM_ORCHESTRATION_JS"))
+ (td :class "px-3 py-2 text-stone-600" "~80 (fetch, abort, timers, SSE, scroll, media...)"))
+ (tr :class "border-b border-stone-100"
+ (td :class "px-3 py-2 text-stone-700" "Boot platform")
+ (td :class "px-3 py-2 text-stone-700" (code "PLATFORM_BOOT_JS"))
+ (td :class "px-3 py-2 text-stone-600" "~20 (mount target, localStorage, cookies, logging...)"))
+ (tr
+ (td :class "px-3 py-2 text-stone-700" "Parser helpers")
+ (td :class "px-3 py-2 text-stone-700" (code "PLATFORM_PARSER_JS"))
+ (td :class "px-3 py-2 text-stone-600" "~4 (isIdentStart, parseNumber...)")))))
+
+ (h4 :class "font-semibold mt-4 mb-2" "Isolation rule")
+ (p "After extraction, searching " (code "sx-evaluator.js") " for " (code "document") ", " (code "window") ", " (code "fetch") ", " (code "localStorage") ", " (code "history") ", " (code "setTimeout") ", " (code "console") " should find " (strong "zero") " direct references.")
+
+ (h4 :class "font-semibold mt-4 mb-2" "The callSxFunction bridge")
+ (p "Platform code (event listeners, timers) needs to invoke SX lambdas. The evaluator provides a single " (code "callSxFunction(fn, args) \u2192 result") " bridge to the platform at registration time. This is the ONE evaluator-to-platform callback.")
+
+ (h4 :class "font-semibold mt-4 mb-2" "Implementation")
+ (ul :class "list-disc list-inside space-y-2 mt-2"
+ (li "Modify " (code "platform_js.py") " to emit platform functions as a separate output section")
+ (li "Create " (code "sx-platform.js") " as an IIFE that sets " (code "globalThis.SxPlatform = {...}"))
+ (li "The evaluator IIFE reads " (code "globalThis.SxPlatform") " at init, registers each function as a PRIMITIVE")
+ (li "Clean up " (code "CONTINUATIONS_JS") " and " (code "ASYNC_IO_JS") " \u2014 eliminate or bootstrap")
+ (li "Test that existing pages work identically")))
+
+
+ ;; -----------------------------------------------------------------------
+ ;; Phase 2: Isolate the JS Evaluator
+ ;; -----------------------------------------------------------------------
+
+ (~docs/section :title "Phase 2: Isolate the JS Evaluator" :id "phase-2"
+ (p (strong "Goal:") " " (code "sx-evaluator.js") " contains ONLY core spec + render adapters. Web framework " (code ".sx") " is evaluated at runtime.")
+
+ (h4 :class "font-semibold mt-4 mb-2" "Core-only build")
+ (p "The bootstrapper already supports selecting which modules to compile. A core-only build:")
+ (~docs/code :code (highlight "# In run_js_sx.py \u2014 core-only build\ncompile_ref_to_js(\n adapters=[\"parser\", \"html\", \"sx\", \"dom\"], # core spec + adapters\n modules=None, # no signals, engine, orchestration, boot\n extensions=None, # no continuations\n spec_modules=None # no deps, router, cek, frames, page-helpers\n)" "python"))
+
+ (h4 :class "font-semibold mt-4 mb-2" "Web framework loading")
+ (p "Web framework " (code ".sx") " files ship as " (code "\n\n\n" "html"))
+
+ (h4 :class "font-semibold mt-4 mb-2" "Boot chicken-and-egg")
+ (p (code "boot.sx") " orchestrates the boot sequence but is itself web framework code. Solution: thin native boot shim (~30 lines) in " (code "sx-platform.js") ":")
+ (~docs/code :code (highlight "SxPlatform.boot = function(evaluator) {\n // 1. Evaluate web framework .sx libraries\n var libs = document.querySelectorAll('script[type=\"text/sx-lib\"]');\n for (var i = 0; i < libs.length; i++) {\n evaluator.evalSource(libs[i].textContent);\n }\n // 2. Call boot-init (defined in boot.sx)\n evaluator.callFunction('boot-init');\n};" "javascript"))
+
+ (h4 :class "font-semibold mt-4 mb-2" "Performance")
+ (p "Parsing + evaluating ~5,000 lines of web framework " (code ".sx") " at startup takes ~10\u201350ms. After " (code "define") ", functions are Lambda objects dispatched identically to compiled functions. " (strong "Zero ongoing performance difference.")))
+
+
+ ;; -----------------------------------------------------------------------
+ ;; Phase 3: Wire Up Rust/WASM
+ ;; -----------------------------------------------------------------------
+
+ (~docs/section :title "Phase 3: Wire Up Rust/WASM" :id "phase-3"
+ (p (strong "Goal:") " Rust evaluator calls " (code "sx-platform.js") " via wasm-bindgen imports. Handle table bridges DOM references.")
+
+ (h4 :class "font-semibold mt-4 mb-2" "Handle table (JS-side)")
+ (~docs/code :code (highlight "// In sx-wasm-shim.js\nconst handles = [null]; // index 0 = null handle\nfunction allocHandle(obj) { handles.push(obj); return handles.length - 1; }\nfunction getHandle(id) { return handles[id]; }\nfunction freeHandle(id) { handles[id] = null; }" "javascript"))
+ (p "DOM nodes are JS objects. The handle table maps " (code "u32") " IDs to JS objects. Rust stores " (code "Value::Handle(u32)") " and passes the " (code "u32") " to imported JS functions.")
+
+ (h4 :class "font-semibold mt-4 mb-2" "Value::Handle in Rust")
+ (~docs/code :code (highlight "// In platform.rs\npub enum Value {\n // ... existing variants ...\n Handle(u32), // opaque reference to JS-side object\n}" "rust"))
+
+ (h4 :class "font-semibold mt-4 mb-2" "WASM imports from platform")
+ (~docs/code :code (highlight "#[wasm_bindgen(module = \"/sx-platform-wasm.js\")]\nextern \"C\" {\n fn platform_create_element(tag: &str) -> u32;\n fn platform_create_text_node(text: &str) -> u32;\n fn platform_set_attr(handle: u32, name: &str, value: &str);\n fn platform_append_child(parent: u32, child: u32);\n fn platform_add_event_listener(handle: u32, event: &str, callback_id: u32);\n // ... ~50 DOM primitives\n}" "rust"))
+
+ (h4 :class "font-semibold mt-4 mb-2" "Callback table for events")
+ (p "When Rust creates an event handler (a Lambda), it stores it in a callback table and gets a " (code "u32") " ID. JS " (code "addEventListener") " wraps it: when the event fires, JS calls into WASM with the callback ID. Rust looks up the Lambda and evaluates it.")
+
+ (h4 :class "font-semibold mt-4 mb-2" "sx-wasm-shim.js")
+ (p "Thin glue (~100 lines):")
+ (ul :class "list-disc list-inside space-y-1 mt-2"
+ (li "Instantiate WASM module")
+ (li "Wire handle table")
+ (li "Delegate all platform calls to " (code "sx-platform.js"))
+ (li "Provide " (code "invoke_callback") " \u2192 Rust for event dispatch")))
+
+
+ ;; -----------------------------------------------------------------------
+ ;; Phase 4: Web Framework Loading
+ ;; -----------------------------------------------------------------------
+
+ (~docs/section :title "Phase 4: Web Framework Loading" :id "phase-4"
+ (p (strong "Goal:") " Both JS and WASM evaluators load the same web framework " (code ".sx") " files at runtime.")
+
+ (h4 :class "font-semibold mt-4 mb-2" "Boot sequence (identical for both evaluators)")
+ (ol :class "list-decimal list-inside space-y-2 mt-2"
+ (li "Load " (code "sx-platform.js") " + evaluator (" (code "sx-evaluator.js") " or " (code "sx-wasm-shim.js") ")")
+ (li "Platform registers primitives with evaluator")
+ (li "Platform boot shim evaluates " (code "