Aser serialization: aser-call/fragment now return SxExpr instead of String.
serialize/inspect passes SxExpr through unquoted, preventing the double-
escaping (\" → \\\" ) that broke client-side parsing when aser wire format
was output via raw! into <script> tags. Added make-sx-expr + sx-expr-source
primitives to OCaml and JS hosts.
Binary blob protocol: eval, aser, aser-slot, and sx-page-full now send SX
source as length-prefixed blobs instead of escaped strings. Eliminates pipe
desync from concurrent requests and removes all string-escape round-trips
between Python and OCaml.
Bridge safety: re-entrancy guard (_in_io_handler) raises immediately if an
IO handler tries to call the bridge, preventing silent deadlocks.
Fetch error logging: orchestration.sx error callback now logs method + URL
via log-warn. Platform catches (fetchAndRestore, fetchPreload, bindBoostForm)
also log errors instead of silently swallowing them.
Transpiler fixes: makeEnv, scopePeek, scopeEmit, makeSxExpr added as
platform function definitions + transpiler mappings — were referenced in
transpiled code but never defined as JS functions.
Playwright test infrastructure:
- nav() captures JS errors and fails fast with the actual error message
- Checks for [object Object] rendering artifacts
- New tests: delete-row interaction, full page refresh, back button,
direct load with fresh context, code block content verification
- Default base URL changed to localhost:8013 (standalone dev server)
- docker-compose.dev-sx.yml: port 8013 exposed for local testing
- test-sx-build.sh: build + unit tests + Playwright smoke tests
Geography content: index page component written (sx/sx/geography/index.sx)
describing OCaml evaluator, wire formats, rendering pipeline, and topic
links. Wiring blocked by aser-expand-component children passing issue.
Tests: 1080/1080 JS, 952/952 OCaml, 66/66 Playwright
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Compiler fixes:
- Upvalue re-lookup returns own position (uv-index), not parent slot
- Spec: cek-call uses (make-env) not (dict) — OCaml Dict≠Env
- Bootstrap post-processes transpiler Dict→Env for cek_call
VM runtime fixes:
- compile_adapter evaluates constant defines (SPECIAL_FORM_NAMES etc.)
via execute_module instead of wrapping as NativeFn closures
- Native primitives: map-indexed, some, every?
- Nil-safe HO forms: map/filter/for-each/some/every? accept nil as empty
- expand-components? set in kernel env (not just VM globals)
- unwrap_env diagnostic: reports actual type received
sx-page-full command:
- Single OCaml call: aser-slot body + render-to-html shell
- Eliminates two pipe round-trips (was: aser-slot→Python→shell render)
- Shell statics (component_defs, CSS, pages_sx) cached in Python,
injected into kernel once, referenced by symbol in per-request command
- Large blobs use placeholder tokens — Python splices post-render,
pipe transfers ~51KB instead of 2MB
Performance (warm):
- Server total: 0.55s (was ~2s)
- aser-slot VM: 0.3s, shell render: 0.01s, pipe: 0.06s
- kwargs computation: 0.000s (cached)
SX_STANDALONE mode for sx_docs dev (skips fragment fetches).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Eliminated double-aser for HTMX requests: build OOB wrapper AST
(~shared:layout/oob-sx :content wrapped_ast) and aser_slot in ONE
pass — same pattern as the full-page path. Halves aser_slot calls.
Added kernel-side timing to stderr:
[aser-slot] eval=3.6s io_flush=0.0s batched=3 result=22235 chars
Results show batch IO works (io_flush=0.0s for 3 highlight calls)
and the bottleneck is pure CEK evaluation time, not IO.
Performance after single-pass fix:
Home: 0.7s eval (was 2.2s total)
Reactive: 3.6s eval (was 6.8s total)
Language: 1.1s eval (was 18.9s total — double-aser eliminated)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: OcamlBridge._send() used write() without drain().
asyncio.StreamWriter buffers writes — without drain(), multiple
commands accumulate and flush as a batch. The kernel processes
them sequentially, sending responses, but Python only reads one
response per command → pipe desync → "unexpected response" errors.
Fix: _send() is now async, calls drain() after every write.
All 14 callers updated to await.
Playwright tests rewritten:
- test_home_has_header: verifies #logo-opacity visible (was only
checking for "sx" text — never caught missing header)
- test_home_has_nav_children: Geography link must be visible
- test_home_has_main_panel: #main-panel must have child elements
- TestDirectPageLoad: fresh browser.new_context() per test to
avoid stale component hash in localStorage
- _setup_error_capture + _check_no_fatal_errors helpers
_render_to_sx uses aser_slot (not aser) — layout wrappers contain
re-parsed content that needs full expansion capability.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three fixes completing the aser_slot migration:
1. Single-pass full-page rendering: eval_sx_url builds layout+content
AST and aser_slots it in ONE call — avoids double-aser where
re-parsed content hits "Undefined symbol: title/deref" errors.
2. Pipe desync fix: _inject_helpers_locked runs INSIDE the aser_slot
lock acquisition (not as a separate lock). Prevents interleaved
commands from other coroutines between injection and aser-slot.
3. _render_to_sx uses aser_slot (not aser): layout wrappers like
oob_page_sx contain re-parsed content from earlier aser_slot
calls. Regular aser fails on symbols that were bound during
the earlier expansion. aser_slot handles them correctly.
HTMX path: aser_slot the content, then oob_page_sx wraps it.
Full page path: build (~shared:layout/app-body :content wrapped_ast),
aser_slot in one pass, pass directly to sx_page.
New Playwright tests: test_navigate_geography_to_reactive,
test_direct_load_reactive_page.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three bugs in aser-expand-component (adapter-sx.sx):
- Keyword values were eval'd (eval-expr can't handle <>, HTML tags);
now asered, matching the aser's rendering capabilities
- Missing default nil binding for unset &key params (caused
"Undefined symbol" errors for optional params like header-rows)
- aserCall string-quoted keyword values that were already serialized
SX — now inlines values starting with "(" directly
Server-affinity annotations for layout/nav shells:
- ~shared:layout/app-body, ~shared:layout/oob-sx — page structure
- ~layouts/nav-sibling-row, ~layouts/nav-children — server-side data
- ~layouts/doc already had :affinity :server
- ~cssx/flush marked :affinity :client (browser-only state)
Navigation fix: restore oob_page_sx wrapper for HTMX responses
so #main-panel section exists for sx-select/sx-swap targeting.
OCaml bridge: lazy page helper injection into kernel via IO proxy
(define name (fn (...) (helper "name" ...))) — enables aser_slot
to evaluate highlight/component-source etc. via coroutine bridge.
Playwright tests: added pageerror listener to test_no_console_errors,
new test_navigate_from_home_to_geography for HTMX nav regression.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
adapter-sx.sx: aser-expand-component expands :affinity :server components
inline during SX wire format serialization. Binds keyword args via
eval-expr, children via aser (handles HTML tags), then asers the body.
ocaml_bridge.py: 10MB readline buffer for large spec responses.
nav-data.sx: evaluator.sx filename fix.
Page rendering stays on Python _eval_slot for now — full OCaml rendering
needs the page shell IO (headers, CSRF, CSS) migrated to OCaml IO bridge.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Major architectural change: page function dispatch and handler execution
now go through the OCaml kernel instead of the Python bootstrapped evaluator.
OCaml integration:
- Page dispatch: bridge.eval() evaluates SX URL expressions (geography, marshes, etc.)
- Handler aser: bridge.aser() serializes handler responses as SX wire format
- _ensure_components loads all .sx files into OCaml kernel (spec, web adapter, handlers)
- defhandler/defpage registered as no-op special forms so handler files load
- helper IO primitive dispatches to Python page helpers + IO handlers
- ok-raw response format for SX wire format (no double-escaping)
- Natural list serialization in eval (no (list ...) wrapper)
- Clean pipe: _read_until_ok always sends io-response on error
SX adapter (aser):
- scope-emit!/scope-peek aliases to avoid CEK special form conflict
- aser-fragment/aser-call: strings starting with "(" pass through unserialized
- Registered cond-scheme?, is-else-clause?, primitive?, get-primitive in kernel
- random-int, parse-int as kernel primitives; json-encode, into via IO bridge
Handler migration:
- All IO calls converted to (helper "name" args...) pattern
- request-arg, request-form, state-get, state-set!, now, component-source etc.
- Fixed bare (effect ...) in island bodies leaking disposer functions as text
- Fixed lower-case → lower, ~search-results → ~examples/search-results
Reactive islands:
- sx-hydrate-islands called after client-side navigation swap
- force-dispose-islands-in for outerHTML swaps (clears hydration markers)
- clear-processed! platform primitive for re-hydration
Content restructuring:
- Design, event bridge, named stores, phase 2 consolidated into reactive overview
- Marshes split into overview + 5 example sub-pages
- Nav links use sx-get/sx-target for client-side navigation
Playwright test suite (sx/tests/test_demos.py):
- 83 tests covering hypermedia demos, reactive islands, marshes, spec explorer
- Server-side rendering, handler interactions, island hydration, navigation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove "Overview" nav link (index.sx IS the summary)
- Rename "Demo" → "Examples" in nav and page title
- Remove "Plan" and "Phase 2" from nav (all items done — status table remains in overview)
- Add "Marshes" to nav (was missing, content already existed)
- Add live event bridge demo island (data-sx-emit → signal via on-event)
- Add event bridge section (#14) to examples page
- Keep "demo" route as alias for backward compat
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Evaluator: data-first higher-order forms — ho-swap-args auto-detects
(map coll fn) vs (map fn coll), both work. Threading + HO: (-> data
(map fn)) dispatches through CEK HO machinery via quoted-value splice.
17 new tests in test-cek-advanced.sx.
Fix plan pages: add mother-language, isolated-evaluator, rust-wasm-host
to page-functions.sx plan() — were in defpage but missing from URL router.
Aser error handling: pages.py now catches EvalError separately, renders
visible error banner instead of silently sending empty content. All
except blocks include traceback in logs.
Scope primitives: register collect!/collected/clear-collected!/emitted/
emit!/context in shared/sx/primitives.py so hand-written _aser can
resolve them (fixes ~cssx/flush expansion failure).
New test file: shared/sx/tests/test_aser_errors.py — 19 pytest tests
for error propagation through all aser control flow forms.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
test-continuations-advanced.sx (41 tests):
multi-shot continuations, composition, provide/context basics,
provide across shift, scope/emit basics, scope across shift
test-render-advanced.sx (27 tests):
nested components, dynamic content, list patterns,
component patterns, special elements
Bugs found and documented:
- case in render context returns DOM object (CEK dispatches case
before HTML adapter sees it — use cond instead for render)
- context not visible in shift body (correct: shift body runs
outside the reset/provide boundary)
- Multiple shifts consume reset (correct: each shift needs its own
reset)
Python runner: skip test-continuations-advanced.sx without --full.
JS 815/815 standard, 938/938 full, Python 706/706.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Higher-order forms (map, filter, reduce, some, every?, for-each,
map-indexed) now evaluate their arguments via CEK frames instead
of nested trampoline(eval-expr(...)) calls.
Added HoSetupFrame — staged evaluation of HO form arguments.
When all args are evaluated, ho-setup-dispatch sets up the
iteration frame. This keeps a single linear CEK continuation
chain instead of spawning nested CEK instances.
14 nested eval-expr calls eliminated (39 → 25 remaining).
The remaining 25 are in delegate functions (sf-letrec, sf-scope,
parse-keyword-args, qq-expand, etc.) called infrequently.
All tests unchanged: JS 747/747, Full 864/870, Python 679/679.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cache the style element reference in _cssx-style-el so flush-cssx-to-dom
never creates more than one. Previous code called dom-query on every
flush, which could miss the element during rapid successive calls,
creating duplicates.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
init-client.sx contains browser-only code (dom-listen, collect! cssx).
It was in sx/sx/ which load_sx_dir scans and evaluates server-side,
causing "Undefined symbol: dom-listen". Move to sx/init-client.sx
which is outside the component load path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All signal operations (computed, effect, batch, etc.) now dispatch
function calls through cek-call, which routes SX lambdas via cek-run
and native callables via apply. This replaces the invoke shim.
Key changes:
- cek.sx: add cek-call (defined before reactive-shift-deref), replace
invoke in subscriber disposal and ReactiveResetFrame handler
- signals.sx: replace all 11 invoke calls with cek-call
- js.sx: fix octal escape in js-quote-string (char-from-code 0)
- platform_js.py: fix JS append to match Python (list concat semantics),
add Continuation type guard in PLATFORM_CEK_JS, add scheduleIdle
safety check, module ordering (cek before signals)
- platform_py.py: fix ident-char regex (remove [ ] from valid chars),
module ordering (cek before signals)
- run_js_sx.py: emit PLATFORM_CEK_JS before transpiled spec files
- page-functions.sx: add cek and provide page functions for SX URLs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both plans had nav entries and component files but were missing from
the page-functions.sx case statement, causing 404s on their URLs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
test-cek-reactive.sx: 9 tests across 4 suites — deref pass-through,
signal without reactive-reset, reactive-reset shift with continuation
capture, scope disposal cleanup. run_cek_reactive_tests.py: new runner
loading signals+frames+cek. Both test runners override sx_ref.eval_expr
back to tree-walk so interpreted .sx uses tree-walk internally.
Plan page added to sx-docs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add `scope` special form to eval.sx: (scope name body...) or
(scope name :value v body...) — general dynamic scope primitive
- `provide` becomes sugar: (provide name value body...) calls scope
- Rename provide-push!/provide-pop! to scope-push!/scope-pop! throughout
all adapters (async, dom, html, sx) and platform implementations
- Update boundary.sx: Tier 5 now "Scoped effects" with scope-push!/
scope-pop! as primary, provide-push!/provide-pop! as aliases
- Add scope form handling to async adapter and aser wire format
- Update sx-browser.js, sx_ref.py (bootstrapped output)
- Add scopes.sx docs page, update provide/spreads/demo docs
- Update nav-data, page-functions, docs page definitions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New geography article (provide.sx): four primitives, demos, nested scoping,
adapter comparison, spec explorer links
- Updated spreads article section VI: provide/emit! is now implemented, not planned
- Fixed foundations.sx: ~docs/code-block → ~docs/code (undefined component
was causing the page to silently fail to render)
- Added nav entry and defpage route for provide/emit! article
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The plans were routed in page-functions.sx (GraphSX URL eval) but
missing from the defpage case in docs.sx (server-side slug route).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Documents the three orthogonal primitives (spread, collect!, reactive-spread),
their operation across server/client/morph boundaries, CSSX as use case,
semantic style variables, and the planned provide/context/emit! unification.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add url-to-expr, auto-quote-unknowns, prepare-url-expr to router.sx —
the canonical URL-to-expression pipeline. Dots→spaces, parse, then
auto-quote unknown symbols as strings (slugs). The same spec serves
both server (Python) and client (JS) route handling.
- router.sx: three new pure functions for URL evaluation
- bootstrap_py.py: auto-include router module with html adapter
- platform_js.py: export urlToExpr/autoQuoteUnknowns/prepareUrlExpr
- sx_router.py: replace hand-written auto_quote_slugs with bootstrapped
prepare_url_expr — delete ~50 lines of hardcoded function name sets
- Rebootstrap sx_ref.py (4331 lines) and sx-browser.js
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Component names now reflect filesystem location using / as path separator
and : as namespace separator for shared components:
~sx-header → ~layouts/header
~layout-app-body → ~shared:layout/app-body
~blog-admin-dashboard → ~admin/dashboard
209 files, 4,941 replacements across all services.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All routes moved under /sx/ prefix:
- / redirects to /sx/
- /sx/ serves home page
- /sx/<path:expr> is the catch-all for SX expression URLs
- Bare /(...) and /~... redirect to /sx/(...) and /sx/~...
- All ~600 hrefs, sx-get attrs, defhandler paths, redirect
targets, and blueprint routes updated across 44 files
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New comprehensive documentation for SX URLs at /(applications.(sx-urls))
covering dots-as-spaces, nesting/scoping, relative URLs, keyword ops,
delta values, special forms, hypermedia integration, and GraphSX.
Fix layout tagline: "A" → "The" framework-free reactive hypermedium.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extends router.sx with the full SX URL algebra — structural navigation
(.slug, .., ...), keyword set/delta (.:page.4, .:page.+1), bare-dot
shorthand, and ! special form parsing (!source, !inspect, !diff, !search,
!raw, !json). All pure SX spec, bootstrapped to both Python and JS.
Fixes: index-of -1/nil portability (_index-of-safe wrapper), variadic
(+ a b c) transpilation bug (use nested binary +). Includes 115 passing
tests covering all operations. Also: "The" strapline and essay title.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Update ~sx-doc :path values in docs.sx from old-style paths to SX
expression URLs (fixes client-side rendered page nav resolution)
- Fix stale hrefs in content/pages.py code examples
- Fix tabs push-url in examples.sx
- Add self-defining-medium + sx-urls + sx-protocol to essay/plan cases
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Every URL at sx-web.org now uses bracketed SX expressions — pages AND
API endpoints. defhandler :path values, sx-get/sx-post/sx-delete attrs,
code examples, and Python route decorators all converted.
- Add SxAtomConverter to handlers.py for parameter matching inside
expression URLs (e.g. /(api.(item.<sx:item_id>)))
- Convert ~50 defhandler :path values in ref-api.sx and examples.sx
- Convert ~90 sx-get/sx-post/sx-delete URLs in reference.sx, examples.sx
- Convert ~30 code example URLs in examples-content.sx
- Convert ~30 API URLs in pages.py (Python string code examples)
- Convert ~70 page navigation URLs in pages.py
- Convert 7 Python route decorators in routes.py
- Convert ~10 reactive API URLs in marshes.sx
- Add API redirect patterns to sx_router.py (301 for old paths)
- Remove /api/ skip in app.py redirects (old API paths now redirect)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Components are stored as ~name in the env. The helper was looking up
bare name without the tilde prefix.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ring 2 (bootstrapper): JavaScript translation via js.sx,
Z3/SMT-LIB translation via reader_z3.py. Each define card
now shows SX, Python, JavaScript, and Z3 collapsible panels.
Ring 3 (bridge): Cross-reference index maps function names
to spec files. Each define shows which other spec functions
it references.
Ring 4 (runtime): Test file parsing extracts defsuite/deftest
structure. Fuzzy name matching links tests to functions.
Stats bar shows test count.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1. Extra closing paren in ex-tabs handler
2. tab-content dict values contained (div ...) HTML tags which crash
during register_components since HTML primitives aren't in env
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The <slug> route param doesn't match slashes, so
/language/specs/explore/<slug> needs its own defpage
instead of being handled inside specs-page.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The helper was trying to look up all-spec-items from get_component_env(),
but that only contains defcomp/defmacro — not regular defines. Now the
SX routing layer calls find-spec and passes filename/title/desc directly.
Also adds boundary declaration for spec-explorer-data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- _spec_explorer_data() helper: parses spec files into sections, defines,
effects, params, source blocks, and Python translations via PyEmitter
- specs-explorer.sx: 10 defcomp components for explorer UI — cards with
effect badges, typed param lists, collapsible SX/Python translation panels
- Route at /language/specs/explore/<slug> via docs.sx
- "Explore" link on existing spec detail pages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New plan page at /etc/plans/spec-explorer describing the "fifth ring"
architecture: SX exploring itself through per-function cards showing
source, Python/JS/Z3 translations, platform deps, tests, proofs, and
usage examples.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Specs nav reorganized from flat list into 6 sections with children:
Core (5), Adapters (4), Browser (4), Reactive (1), Host Interface (3),
Extensions (4). Added missing spec items: adapter-async, signals,
boundary, forms, page-helpers, types. Architecture page updated to
match. New essay on ars, techne, and the self-making artifact chain.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New platform_js primitives for direct DOM property/method access and
cross-origin iframe communication. Service worker static cache bumped
to v2 to flush stale assets. Removed experimental video embed from
header island, routes, and home page.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- ~video-player defisland persists across SPA navigations (morph-safe)
- Clicking "reactive" cycles colour (signal) + fetches random YouTube video (sx-get)
- sx-trigger="fetch-video" + dom-first-child check: video keeps playing on repeat clicks
- Close button (x) clears video via /api/clear-video hypermedia endpoint
- Autoplay+mute removes YouTube's red play button overlay
- Header restructured: logo in anchor, tagline outside (no accidental navigation)
- Flex centering on video container
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New hierarchy: Geography (Reactive Islands, Hypermedia Lakes, Marshes,
Isomorphism), Language (Docs, Specs, Bootstrappers, Testing),
Applications (CSSX, Protocols), Etc (Essays, Philosophy, Plans).
All routes updated to match: /reactive/* → /geography/reactive/*,
/docs/* → /language/docs/*, /essays/* → /etc/essays/*, etc.
Updates nav-data.sx, all defpage routes, API endpoints, internal links
across 43 files. Enhanced find-nav-match for nested group resolution.
Also includes: page-helpers-demo sf-total fix (reduce instead of set!),
rebootstrapped sx-browser.js and sx_ref.py, defensive slice/rest guards.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add page-helpers-demo page with defisland ~demo-client-runner (pure SX,
zero JS files) showing spec functions running on both server and client
- Fix _aser_component children serialization: flatten list results from map
instead of serialize(list) which wraps in parens creating ((div ...) ...)
that re-parses as invalid function call. Fixed in adapter-async.sx spec
and async_eval_ref.py
- Switch _eval_slot to use async_eval_ref.py when SX_USE_REF=1 (was
hardcoded to async_eval.py)
- Add Island type support to async_eval_ref.py: import, SSR rendering,
aser dispatch, thread-first, defisland in _ASER_FORMS
- Add server affinity check: components with :affinity :server expand
even when _expand_components is False
- Add diagnostic _aser_stack context to EvalError messages
- New spec files: adapter-async.sx, page-helpers.sx, platform_js.py
- Bootstrappers: page-helpers module support, performance.now() timing
- 0-arity lambda event handler fix in adapter-dom.sx
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
EvalError moved to types.py. All 27 files updated to import eval_expr,
trampoline, call_lambda, etc. directly from shared.sx.ref.sx_ref instead
of through the evaluator.py indirection layer. 320/320 spec tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>