- sx_browser.ml: restore VmSuspended handler in api_call_fn with
make_js_callFn_suspension for IO suspension chains (wait, fetch)
- runtime.sx: delete host-get stub that shadowed platform native —
hs-toggle-class! now uses real FFI host-get for classList access
All three live demo examples work:
Toggle Color — classList.toggle on click
Bounce — add .animate-bounce, wait 1s suspend, remove
Count Clicks — increment @data-count, put into innerHTML
4/4 bytecode regression tests pass (was 0/4 without VmSuspended).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three bugs fixed:
1. host-get in sx-platform.js: return true for function-valued properties
so dom-get-attr/dom-set-attr guards pass (functions can't cross WASM boundary)
2. hs-runtime.sx: renamed host-get→hs-host-get and dom-query→hs-dom-query to
stop shadowing platform natives when loaded as .sx source
3. compile-modules.js: HS dependency chain (integration→runtime→compiler→parser→tokenizer)
so lazy loading pulls in all deps. Non-library modules load as .sx source
for CEK env visibility.
Result: 8/8 elements activate, hs-on attaches listeners. Click handler needs
IO suspension support (VmSuspended in sx_browser.ml) to fire — next step.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- orchestration.sx: add hs-boot-subtree! call to process-elements
- integration.sx: remove load-library! calls (browser loads via manifest)
- sx_vm.ml: add __resolve-symbol hook to OP_GLOBAL_GET for lazy loading
- compile-modules.js: add HS modules as lazy_deps in manifest
HS compilation works in browser (tokenize→parse→compile verified).
Activation pipeline partially working — hs-activate! needs debugging
(dom-get-data/dom-set-data interaction with WASM host-get on functions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reset to last known-good state (908f4f80) where links, stepper, and
islands all work, then recovered all hyperscript implementation,
conformance tests, behavioral tests, Playwright specs, site sandbox,
IO-aware server loading, and upstream test suite from f271c88a.
Excludes runtime changes (VM resolve hook, VmSuspended browser handler,
sx_ref.ml guard recovery) that need careful re-integration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All .sxbc recompiled with fixed sx_vm.ml. 32/32 WASM tests, 4/4
bytecode regression tests. hs-repeat-times correctly does 6 io-sleep
suspensions in bytecode mode.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Compiler match for type-check-strict was still using old name type-check!
- Deploy updated HS source files to shared/static/wasm/sx/
- Sandbox runner validates 16/16 hard cases pass with cek-eval
(no runtime let-binding hacks needed in WASM context)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The when form's continuation for the second body expression was lost
across perform/cek_resume cycles. Wrapping (thunk) and (do-repeat)
in an explicit (do ...) gives when a single body, and do's own
continuation handles the sequencing correctly.
Sandbox confirms: 6/6 io-sleep suspensions now chain through
host-callback → _driveAsync → resume_vm (was 1/6 before fix).
Also fix sandbox async timing: _asyncPending counter tracks in-flight
IO chains so page.evaluate waits for all resumes to complete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The /sx/ prefix mismatch: defpage declares paths like /language/docs/<slug>
but browser URLs are /sx/(language.(doc.slug)). find-matching-route used
starts-with? "/(", missing the /sx/ prefix entirely.
Fix: find-matching-route now uses (index-of path "/(") to detect the SX
URL portion regardless of prefix. Works for /sx/, /myapp/, any prefix.
No hardcoded paths.
Also fixed deps-satisfied?: nil deps (unknown) now returns false instead
of true, preventing client-side eval of pages with unresolved components.
Correctly falls back to server fetch.
Verified with Playwright: clicking "Getting Started" on the docs page now
shows "sx:route deps miss for docs-page" → "sx:route server fetch" instead
of the old "sx:route no match (51 routes)".
2 new router tests for prefix stripping. 2914/2914 total, zero failures.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Provide subscribers stored in global *provide-subscribers* dict (keyed
by name) instead of on provide frames. Fixes subscriber loss when
frames are reconstructed, and enables cross-cek_run notification.
Batch integration: batch-begin!/batch-end! primitives manage
*provide-batch-depth*. fire-provide-subscribers defers to queue when
depth > 0, batch-end! flushes deduped. signals.sx batch calls both.
context now prefers scope-peek over frame value — scope stack is the
source of truth since provide! always updates it (even in nested
cek_run where provide frames aren't on the kont).
2754/2768 OCaml (14 pre-existing). 32/32 WASM.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: context called inside lambdas (e.g. swap!) went through
nested cek_run with empty kont, so provide frames weren't found and
never tracked to *bind-tracking*.
Three changes in evaluator.sx:
- step-sf-context: track context names (not frames) to *bind-tracking*
— names work across cek_run boundaries via scope-peek fallback
- bind continue: resolve tracked names to frames via kont-find-provide
on rest-k before registering subscribers
- subscriber: use empty kont instead of kont-extract-provides — old
approach created provide frames whose continue handlers called
scope-pop!, corrupting the scope stack
2752/2768 OCaml tests pass (all 7 bind subscriber tests fixed).
32/32 WASM native tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- cek_run patched to handle import suspensions via _import_hook.
define-library (import ...) now resolves cleanly on the server.
IO suspension errors: 190 → 0. JIT failures: ~50 → 0.
- _import_hook wired in sx_server.ml to load .sx files on demand.
- compile-modules.js syncs source .sx files to dist/sx/ before
compiling — eliminates stale bytecode from out-of-date copies.
- WASM binary rebuilt with all fixes.
- 2658/2658 tests pass (8 new — previously failing import tests).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the VM or CEK hits an undefined symbol, it checks a symbol→library
index (built from manifest exports at boot), loads the library that
exports it, and returns the value. Execution continues as if the module
was always loaded. No import statements, no load-library! calls, no
Suspense boundaries — just call the function.
This is the same mechanism as IO suspension for data fetching. The
programmer doesn't distinguish between calling a local function and
calling one that needs its module fetched first. The runtime treats
code as just another resource.
Implementation:
- _symbol_resolve_hook in sx_types.ml — called by env_get_id (CEK path)
and vm_global_get (VM path) when a symbol isn't found
- Symbol→library index built from manifest exports in sx-platform.js
- __resolve-symbol native calls __sxLoadLibrary, module loads, symbol
appears in globals, execution resumes
- compile-modules.js extracts export lists into module-manifest.json
- Playground page demonstrates: (freeze-scope) triggers freeze.sxbc
download transparently on first use
2650/2650 tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The bytecode compiler now handles let-match (both variants):
- Variant 1: (let-match name expr {:k v} body...) — named binding + destructure
- Variant 2: (let-match {:k v} expr body...) — pattern-only destructure
Desugars to sequential let + get calls — no new opcodes needed.
This was the last blocker for SPA navigation. The bytecoded orchestration
and router modules used let-match which compiled to CALL_PRIM "let-match"
(undefined at runtime). Now desugared at compile time.
Also synced dist/sx/ sources with web/ and recompiled all 26 .sxbc modules.
2650/2650 tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bytecode compiler now emits OP_PERFORM for (import ...) and compiles
(define-library ...) bodies. The VM stores the import request in
globals["__io_request"] and stops the run loop — no exceptions needed.
vm-execute-module returns a suspension dict, vm-resume-module continues.
Browser: sx_browser.ml detects suspension dicts from execute_module and
returns JS {suspended, op, request, resume} objects. The sx-platform.js
while loop handles cascading suspensions via handleImportSuspension.
13 modules load via .sxbc bytecode in 226ms (manifest-driven), both
islands hydrate, all handlers wired. 2650/2650 tests pass including
6 new vm-import-suspension tests.
Also: consolidated sx-platform-2.js → sx-platform.js, fixed
vm-execute-module missing code-from-value call, fixed bootstrap.py
protocol registry transpiler issues.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The transpiled VM (sx_vm_ref.ml, from lib/vm.sx) is now the ACTIVE
bytecode execution engine. sx_server.ml and sx_browser.ml call
Sx_vm_ref.execute_module instead of Sx_vm.execute_module.
Results:
- OCaml tests: 2644 passed, 0 failed
- WASM tests: 32 passed, 0 failed
- Browser: zero errors, zero warnings, islands hydrate
- Server: pages render, JIT compiles, all routes work
The VM logic now lives in ONE place: lib/vm.sx (SX).
OCaml gets it via transpilation (bootstrap_vm.py).
JS/browser gets it via bytecode compilation (compile-modules.js).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All 26 browser modules recompiled with define-library/import forms.
Compilation works without vm-compile-adapter (JIT pre-compilation
hangs with library wrappers in some JIT paths — skipped for now,
CEK compilation is ~34s total).
Key fixes:
- eval command: import-aware loop that handles define-library/import
locally without touching the Python bridge pipe (avoids deadlock)
- compile-modules.js: skip vm-compile-adapter, bump timeout
2621/2621 OCaml tests passing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Core SX has zero IO — platforms extend __io-registry via (defio name
:category :data/:code/:effect ...). The server web platform declares 44
operations in web/io.sx. batchable_helpers now derived from registry
(:batchable true) instead of hardcoded list. Startup validation warns if
bound IO ops lack registry entries. Browser gets empty registry, ready
for step 5 (IO suspension).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 1 Step 1 of the architecture roadmap. The old cssx.sx
(cssx-resolve, cssx-process-token, cssx-template, old tw function)
is superseded by the ~tw component system in tw.sx.
- Delete shared/sx/templates/cssx.sx
- Remove cssx.sx from all load lists (sx_server.ml, run_tests.ml,
mcp_tree.ml, compile-modules.js, bundle.sh, sx-build-all.sh)
- Replace (tw "tokens") inline style calls with (~tw :tokens "tokens")
in layouts.sx and not-found.sx
- Remove _css-hash / init-css-tracking / SX-Css header plumbing
(dead code — ~tw/flush + flush-collected-styles handle CSS now)
- Remove sx-css-classes param and meta tag from shell template
- Update stale data-cssx references to data-sx-css in tests
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The text/sx AJAX response path (handle-sx-response) never called
hoist-head-elements, so <style> elements stayed in #sx-content instead
of moving to <head>. Additionally, CSS rules collected during client-side
island hydration were never flushed to the DOM.
- Add hoist-head-elements call to handle-sx-response (matching
handle-html-response which already had it)
- Add flush-collected-styles helper that drains collected CSS rules
into a <style data-sx-css> element in <head>
- Call flush after island hydration in post-swap, boot-init, and
run-post-render-hooks to catch reactive re-renders
- Unify on data-sx-css attribute (existing convention) in ~tw/flush
and shell template, removing the ad-hoc data-cssx attribute
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reverted render-dom-lake SSR reuse — it broke OOB swaps (claimed
old lake elements during morph, stale content in copyright). The
framework's morphing handles lake updates correctly already.
Stepper: lake passes nil on client (prevents raw SX flash), effect
always calls rebuild-preview (no initial-render flag needed). Server
renders the expression for SSR; client rebuilds via render-to-dom
after boot when ~tw is available.
Removed initial-render dict flag — unnecessary complexity.
Copyright route not updating is a pre-existing issue: render-dom-island
renders the header island inline during OOB content rendering (sets
island-hydrated mark), but the copyright lake content doesn't reflect
the new path. Separate investigation needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The change to re-render children when non-empty caused the stepper's
lake to show raw SX again (the ~tw expressions in steps-to-preview
were rendered as text). Reverted to the simpler approach: reuse the
existing SSR element and preserve its content entirely.
The copyright route not updating during SPA nav is a pre-existing
issue (path is an island parameter, not a reactive signal).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After reusing an SSR lake element, set data-sx-lake-claimed attribute.
Subsequent dom-query uses :not([data-sx-lake-claimed]) to skip already-
reused elements, preventing SPA navigation from picking up stale lakes
from previous pages.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three fixes:
1. Framework: render-dom-lake preserves SSR elements during hydration.
When client-side render-to-dom encounters a lake with an existing
DOM element (from SSR), it reuses that element instead of creating
a new one. This prevents the SSR HTML from being replaced with
unresolvable raw SX expressions (~tw calls).
2. Stepper: skip rebuild-preview on initial hydration. Uses a non-
reactive dict flag (not a signal) to avoid triggering the effect
twice. On first run, just initializes the DOM stack from the
existing SSR content by computing open-element depth from step
types and walking lastElementChild.
3. Stepper: rebuild-preview computes correct DOM stack after re-render.
Same depth computation + DOM walk approach. This fixes the bug where
do-step after do-back would append elements to the wrong parent
(e.g. "sx" span outside h1).
Also: increased code view font-size from 0.5rem to 0.85rem.
Playwright tests:
- lake never shows raw SX during hydration (mutation observer)
- back 6 + forward 6 keeps all 4 spans inside h1
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The stepper's split-tag and steps-to-preview used (list) with append!,
but in the WASM kernel (list) creates immutable List values — append!
returns a new list without mutating, so children accumulate nowhere.
Changed all accumulator initializations to (mutable-list):
- split-tag: cch, cat, spreads
- steps-to-preview: bc-loop inner children, initial call
- result and tokens lists in the parsing setup
Also includes WASM rebuild with append! primitive and &rest fixes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The text/sx AJAX response path (handle-sx-response) never called
hoist-head-elements, so <style> elements stayed in #sx-content instead
of moving to <head>. Additionally, CSS rules collected during client-side
island hydration were never flushed to the DOM.
- Add hoist-head-elements call to handle-sx-response (matching
handle-html-response which already had it)
- Add flush-collected-styles helper that drains collected CSS rules
into a <style data-sx-css> element in <head>
- Call flush after island hydration in post-swap, boot-init, and
run-post-render-hooks to catch reactive re-renders
- Unify on data-sx-css attribute (existing convention) in ~tw/flush
and shell template, removing the ad-hoc data-cssx attribute
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. parse-trigger-spec: strip [condition] from event names, store as
"filter" modifier
2. bind-event: native SX filter for key=='X' patterns (extracts key
char and checks event.key + not-input guard)
3. bind-event from: modifier: resolve "body"/"document"/"window" to
direct DOM references instead of dom-query
4. sx-platform-2.js: global keyboard dispatch — WASM host-callbacks
on document/body don't fire, so keyboard triggers with from:body
are handled from JS, calling execute-request via K.eval
5. bind-inline-handlers: map afterSwap/beforeRequest to sx: prefix,
eval JS bodies via Function constructor
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. parse-trigger-spec: strip [condition] from event names, store as
"filter" modifier (e.g. keyup[key=='s'] → event="keyup", filter=...)
2. bind-event: evaluate filter conditions via JS Function constructor
when filter modifier is present
3. bind-inline-handlers: map afterSwap/beforeRequest etc. to sx:*
event names (matching what the engine dispatches)
4. bind-inline-handlers: detect JS syntax in body (contains ".") and
eval via Function instead of SX parse — enables this.reset() etc.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The poll trigger in bind-triggers called set-interval but discarded the
interval ID, so polls continued firing after the element was removed from
the DOM. Now the callback checks el.isConnected each tick and self-clears
when the element is gone (HTMX-style cleanup).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tag names from dom-tag-name are lowercase (not uppercase) in the WASM
kernel — fix FORM/INPUT/SELECT/TEXTAREA comparisons in get-default-trigger.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
From other session: adapter-html/sx/dom fixes, orchestration
improvements, examples-content refactoring, SPA navigation test
updates, WASM copies synced.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
highlight.sx now returns a list of (span :class "..." "text") elements
instead of a string. The rendering pipeline handles the rest:
- Server render-to-html: produces <span class="...">text</span>
- Client render-to-dom: produces DOM span elements
- Aser: serializes spans as SX for client rendering
Key fixes:
- hl-span uses (make-keyword "class") not :class (keywords evaluate
to strings in list context)
- render-sx-tokens returns flat list of spans (no wrapper)
- hl-escape is identity (no escaping needed for tree values)
- highlight.sx added to browser bundle + platform loader
- ~docs/code renders src directly as child of pre
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: sx_primitives.ml registered "effect" as a native no-op (for SSR).
The bytecode compiler's (primitive? "effect") returned true, so it emitted
OP_CALL_PRIM instead of OP_GLOBAL_GET + OP_CALL. The VM's CALL_PRIM handler
found the native Nil-returning stub and never called the real effect function
from core-signals.sx.
Fix: Remove effect and register-in-scope from the primitives table. The server
overrides them via env_bind in sx_server.ml (after compilation), which doesn't
affect primitive? checks.
Also: VM CALL_PRIM now falls back to cek_call for non-NativeFn values (safety
net for any other functions that get misclassified).
15/15 source mode, 15/15 bytecode mode.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three root causes for reactive attribute updates not propagating in WASM:
1. `context` CEK special form only searched kont provide frames, missing
`scope-push!` entries in the native scope_stacks hashtable. Unified by
adding scope_stacks fallback to step_sf_context.
2. `flush-subscribers` used bare `(sub)` call which failed to invoke
complex closures in for-each HO callbacks. Changed to `(cek-call sub nil)`.
3. Test eagerly evaluated `(deref s)` before render-to-dom saw it.
Fixed tests to use quoted expressions matching real browser boot.
WASM native: 10/10, WASM shell: 26/26.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Revert (when (client?) ...) guard in signals.sx — it broke JS tests
since client? is false in Node.js too.
Instead, rebind effect and register-in-scope as no-ops in sx_server.ml
AFTER all .sx files load. The SX definition from signals.sx is replaced
only in the OCaml SSR context. JS tests and WASM browser keep the real
effect implementation.
Remove redundant browser primitive stubs from sx_primitives.ml — only
resource SSR stub needed (effect override moved to server setup).
JS tests: 1582/1585 (3 VM closure interop remain)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- signals.sx: guard effect body with (when (client?) ...) so effects
are no-op during SSR — only 2 stubs needed (effect, register-in-scope)
- sx_primitives.ml: add resource SSR stub (returns signal {loading: true}),
remove 27 unnecessary browser primitive stubs
- sx_server.ml: native component-source that looks up Component/Island
from env and pretty-prints the definition (replaces broken Python helper)
- reactive-islands/index.sx: Examples section with all 15 live demos
inline + highlighted source via component-source
- reactive-islands/demo.sx: replace 14 hardcoded highlight strings with
(component-source "~name") calls for always-current source
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
dom-id returns "" for elements without an id attribute, and "" is
truthy in SX. This caused morph-children to build spurious entries
in old-by-id keyed by "", then match unrelated children via the
empty key — advancing oi past id-keyed children like #main-content
and skipping them in cleanup.
Three changes in morph-children (engine.sx):
- old-by-id reduce: normalize empty dom-id to nil so id-less
elements are excluded from the lookup dict
- match-id binding: normalize empty dom-id to nil so new children
without ids don't spuriously match old children
- Case 2 skip condition: use (not (empty? ...)) instead of bare
(dom-id old-child) to avoid treating "" as a real id
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
signal-add-sub! used (append! subscribers f) which returns a new list
for immutable List but discards the result — after signal-remove-sub!
replaces the subscribers list via dict-set!, re-adding subscribers
silently fails. Counter island only worked once (0→1 then stuck).
Fix: use (dict-set! s "subscribers" (append ...)) to explicitly update
the dict field, matching signal-remove-sub!'s pattern.
Build pipeline fixes:
- sx-build-all.sh now bundles spec→dist and recompiles .sxbc bytecode
- compile-modules.js syncs .sx source files alongside .sxbc to wasm/sx/
- Per-file cache busting: wasm, platform JS, and sxbc each get own hash
- bundle.sh adds cssx.sx to dist
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three bugs in the DOM morph algorithm (web/engine.sx):
1. Empty-id keying: dom-id returns "" (not nil) for elements without an
id attribute, and "" is truthy in SX. Every id-less element was stored
under key "" in old-by-id, causing all new children to match the same
old element via the keyed branch — collapsing all children into one.
Fix: guard with (and id (not (empty? id))) in map building and matching.
2. Cleanup bug: the oi-cursor cleanup (range oi len) removed keyed elements
that were matched and moved from positions >= oi, and failed to remove
unmatched elements at positions < oi. Fix: track consumed indices in a
dict and remove all unconsumed elements regardless of position.
3. Island attr sync: morph-node delegated to morph-island-children without
first syncing the island element's own attributes (e.g. data-sx-state).
Fix: call sync-attrs before morph-island-children.
Also: pass explicit `true` to all dom-clone calls (deep clone parameter).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
JIT compiler:
- Fix jit_compile_lambda: resolve `compile` via symbol lookup in env
instead of embedding VmClosure in AST (CEK dispatches differently)
- Register eval-defcomp/eval-defisland/eval-defmacro runtime helpers
in browser kernel for bytecoded defcomp forms
- Disable broken .sxbc.json path (missing arity in nested code blocks),
use .sxbc text format only
- Mark JIT-failed closures as sentinel to stop retrying
CSSX in browser:
- Add cssx.sx symlink + cssx.sxbc to browser web stack
- Add flush-cssx! to orchestration.sx post-swap for SPA nav
- Add cssx.sx to compile-modules.js and mcp_tree.ml bytecode lists
SPA navigation:
- Fix double-fetch: check e.defaultPrevented in click delegation
(bind-event already handled the click)
- Fix layout destruction: change nav links from outerHTML to innerHTML
swap (outerHTML destroyed #main-panel when response lacked it)
- Guard JS popstate handler when SX engine is booted
- Rename sx-platform.js → sx-platform-2.js to bust immutable cache
Playwright tests:
- Add trackErrors() helper to all test specs
- Add SPA DOM comparison test (SPA nav vs fresh load)
- Add single-fetch + no-duplicate-elements test
- Improve MCP tool output: show failure details and error messages
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: _env_bind_hook mirrored ALL env_bind calls (including
lambda parameter bindings) to the shared VM globals table. Factory
functions like make-page-fn that return closures capturing different
values for the same param names (default-name, prefix, suffix) would
have the last call's values overwrite all previous closures' captured
state in globals. OP_GLOBAL_GET reads globals first, so all closures
returned the last factory call's values.
Fix: only sync root-env bindings (parent=None) to VM globals. Lambda
parameter bindings stay in their local env, found via vm_closure_env
fallback in OP_GLOBAL_GET.
Also in this commit:
- OP_CLOSURE propagates parent vm_closure_env to child closures
- Remove JIT globals injection (closure vars found via env chain)
- sx_server.ml: SX-Request header → returns text/sx (aser only)
- sx_server.ml: diagnostic endpoint GET /sx/_debug/{env,eval,route}
- sx_server.ml: page helper stubs for deep page rendering
- sx_server.ml: skip client-libs/ dir (browser-only definitions)
- adapter-html.sx: unknown components → HTML comment (not error)
- sx-platform.js: .sxbc fallback loader for bytecode modules
- Delete sx_http.ml (standalone HTTP server, unused)
- Delete stale .sxbc.json files (arity=0 bug, replaced by .sxbc)
- 7 new closure isolation tests in test-closure-isolation.sx
- mcp_tree.ml: emit arity + upvalue-count in .sxbc.json output
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>