Two bugs fixed:
1. Links: bytecode compiler doesn't handle &rest params — treats them as
positional, so (first rest) gets a raw string instead of a list.
Replaced &rest with explicit optional params in all bytecode-compiled
web SX files (dom-query, dom-add-listener, browser-push-state, etc.).
The VM already pads missing args with Nil.
2. Reactive counter: signal-remove-sub! used (filter ...) which returns
immutable List, but signal-add-sub! uses (append!) which only mutates
ListRef. Subscribers silently vanished after first effect re-run.
Fixed by adding remove! primitive that mutates ListRef in-place.
Also:
- Added evalVM API to WASM kernel (compile + run through bytecode VM)
- Added scope tracing (scope-push!/pop!/peek/context instrumentation)
- Added Playwright reactive mode for debugging island signal/DOM state
- Replaced cek-call with direct calls in core-signals.sx effect/computed
- Recompiled all 23 bytecode modules
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tools: vm-trace, bytecode-inspect, deps-check, prim-check,
test_boot.sh, sx-build-all.sh — with usage examples.
Island rules: (do ...) for multi-expression bodies, nested let for
cross-references, (deref (computed ...)) for reactive derived text,
effects in inner let for OCaml SSR compatibility.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
List.find returns the element that matched, but SX some should return
the callback's truthy return value. This caused get-verb-info to return
"get" (the verb string) instead of the {method, url} dict.
Also added _active_vm tracking to VM for future HO primitive optimization,
and reverted get-verb-info to use some (no longer needs for-each workaround).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The some HO form passes callbacks through call_sx_fn which creates a new
VM that can't see the enclosing closure's captured variables (el). Replaced
with for-each + mutation which keeps everything in the same VM scope.
Also fixed destructuring param ((verb ...)) → plain param (verb).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The sx-get links were doing full page refreshes because click handlers
never attached. Root causes: VM frame management bug, missing primitives,
CEK/VM type dispatch mismatch, and silent error swallowing.
Fixes:
- VM frame exhaustion: frames <- [] now properly pops to rest_frames
- length primitive: add alias for len in OCaml primitives
- call_sx_fn: use sx_call directly instead of eval_expr (CEK checks
for type "lambda" but VmClosure reports "function")
- Boot error surfacing: Sx.init() now has try/catch + failure summary
- Callback error surfacing: catch-all handler for non-Eval_error exceptions
- Silent JIT failures: log before CEK fallback instead of swallowing
- vm→env sync: loadModule now calls sync_vm_to_env()
- sx_build_bytecode MCP tool added for bytecode compilation
Tests: 50 new tests across test-vm.sx and test-vm-primitives.sx covering
nested VM calls, frame integrity, CEK bridge, primitive availability,
cross-module symbol resolution, and callback dispatch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- compile-modules.js: Node.js build tool, all 23 .sx files compile to .sxbc.json
- api_load_module with shared globals (beginModuleLoad/endModuleLoad batch API)
- api_compile_module for runtime compilation
- sx-platform.js: bytecode-first loader with source fallback, JSON deserializer
- Deferred JIT enable (setTimeout after boot)
Known issues:
- WASM browser: loadModule loads but functions not accessible (env writeback
issue with interned keys)
- WASM browser: compileModule fails ("Not callable: nil" — compile-module
function from bytecode not working correctly in WASM context)
- Node.js js_of_ocaml: full roundtrip works (compile → load → call)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- sx_browser.ml: add api_load_module (execute pre-compiled bytecode on VM,
copy defines back to env) and api_compile_module (compile SX source to
bytecode via compile-module function)
- compile-modules.js: Node.js build tool that loads the js_of_ocaml kernel,
compiles all 23 .sx platform files to bytecode, writes .sxbc.json files
- Serialization format: type-tagged JSON constants (s/n/b/nil/sym/kw/list/code)
with nested code objects for lambda closures
All 23 files compile successfully (430K total bytecode JSON).
Next: wire up sx-platform.js to load bytecode instead of source.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- orchestration.sx: add nil guard for verb/url before calling do-fetch
(prevents "Expected string, got nil" when verb info dict lacks method)
- sx_browser.ml: restore JIT error logging (Eval_error only, not all
exceptions) so real failures are visible, silence routine fallbacks
- Rebuild WASM bundle with fixes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace update-code-highlight's O(n²) for-each loop (79 nth calls +
79 dom-set-attr FFI crossings) with a single innerHTML set via join/map.
Builds the HTML string in WASM, one FFI call to set innerHTML.
858ms → 431ms (WASM) → 101ms (JIT) → 76ms (innerHTML batch)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Wire up jit_call_hook in sx_browser.ml (same pattern as server)
- Deferred JIT: _jit_enabled flag, enabled after boot-init completes
(prevents "Undefined symbol" errors from compiling during .sx loading)
- enable-jit! native function called by sx-platform.js after boot
- sx-platform.js: async WASM kernel polling + JIT enable after init
- Error logging for JIT compile failures and runtime fallbacks
Performance: 858ms → 431ms (WASM CEK) → 101ms (WASM JIT)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
nav-etc.sx had its own plans-nav-items (35 entries) that loaded after
nav-data.sx (36 entries) and silently overwrote it, hiding sx-host.
All nav items are now solely defined in nav-data.sx.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace WebTransport-only design with three-transport model:
- Browser↔server: WebSocket (works today, no infrastructure changes)
- Browser↔browser: WebRTC data channels (true P2P, NAT traversal)
- Server↔server: HTTP federation (existing sx-pub)
Home nodes relay WebRTC signaling. Reliable channels for chat/components,
unreliable channels for game state/cursor positions. Transport-agnostic
protocol layer — upgrade to WebTransport later with zero SX changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add Debugging & analysis section to tool catalogue (sx_trace, sx_deps,
sx_build_manifest)
- Update harness entry to mention multi-file and setup support
- Update tool count from 35+ to 40+
- Add tool table to CLAUDE.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 2d: shared-signal primitive — local signal writes propagate to
peers via WebTransport rooms. Same deref/reset!/swap! API, networking
invisible to the reactive layer. LWW default, CRDT opt-in.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two new demo pages with live interactive islands:
Cyst (/geography/reactive/examples/cyst):
Parent island with a cyst inside. Parent + button increments parent
count, cyst + button increments cyst count. Cyst state survives
parent re-renders. Shows the isolation boundary in action.
Reactive Expressions (/geography/reactive/examples/reactive-expressions):
Temperature signal with 5 expression types all updating live:
bare deref, str+deref, arithmetic+deref, full conversion string,
conditional. Demonstrates that any expression containing deref
auto-tracks signal dependencies.
Both verified reactive with Playwright clicks on the live server.
Nav entries added to reactive-examples-nav-items.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New defisland ~sx-tools/tool-playground: paste SX source, click a tool
button, see the output. 7 tools run client-side in the browser:
summarise, read-tree, find-all, validate, get-context, serialize,
hypersx.
Each uses the same SX functions the MCP server uses: sx-parse for
parsing, tree walking for annotation/summarise/find, sx-serialize for
round-trip, sx->hypersx for alternative syntax.
Pre-loaded with a sample defcomp. Users can paste any SX and explore
all the comprehension tools interactively.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously only the first parsed expression was inserted. Now all
expressions in new_source are spliced at the given index. e.g.
new_source="(a) (b) (c)" inserts all three as siblings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Added 7 new documentation sections with code examples:
- Cyst: isolated reactive subtrees that survive parent re-renders
- Reactive expressions: auto-wrapping deref-containing exprs in computed
- Live island preview: paste defisland, get working reactive preview
- HyperSX: indentation-based alternative syntax viewer
- Inline test runner: browser-embedded test execution with pass/fail
- Test harness: three-layer test infrastructure (core/reactive/web)
- Core signals: spec-level reactive primitives, zero platform deps
Each section has code examples. The test runner section links to the
live temperature converter demo with 5/5 inline tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New language feature: (cyst [:key id] body...) creates a DOM container
with its own island scope that persists across parent reactive re-renders.
On first render, the body is evaluated in a fresh with-island-scope and
the resulting DOM is cached. On subsequent renders, the cached DOM node
is returned if still connected to the document.
This solves the fundamental problem of nesting reactive islands inside
other islands' render trees — the child island's DOM (with its event
handlers and signal subscriptions) survives when the parent re-renders.
Implementation: *memo-cache* dict keyed by cyst id. render-dom checks
isConnected before returning cached node. Each cyst gets its own
disposer list via with-island-scope.
Usage in sx-tools: defisland render preview now wrapped in (cyst :key
full-name ...). Real mouse clicks work — counter increments, temperature
converts, computed signals update. Verified on both local and live site.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Removed auto-parse on source change (the (reset! parsed ...) at top
of letrec body). Only Parse button triggers parsing now.
- Split defcomp/defisland render: defcomp uses parameter substitution
with bindings signal, defisland uses live component call.
- Removed stopPropagation wrapper (didn't help).
Island previews are reactive — signals, computed, swap! all work.
Known: Playwright's click (which triggers focus change from textarea)
resets the island. Direct JS clicks and PointerEvent dispatches work
correctly. Real islands on demo pages work perfectly with mouse clicks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Added sx_harness_eval to the MCP tools table
- Added spec/harness.sx to the specification files list
- Added full test harness design section (sessions, interceptors, IO log,
assertions, extensibility, platform-specific extensions, CID-based
component/test association)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The island preview refactoring replaced both defcomp and defisland
handling with sx-load-components + component call. This broke parameter
substitution for defcomp (which uses bindings signal for live preview).
Split the cond: defcomp uses the original subst approach (extract params,
read bindings, substitute symbols in body). defisland uses the live
preview via sx-load-components + native render-dom-island.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixed three fundamental issues:
1. cek-try arg passing: handler was called with raw string instead of
(List [String msg]), causing "lambda expects 1 args, got N" errors
2. Silent island hydration failures: hydrate-island now wraps body
render in cek-try, displaying red error box with stack trace instead
of empty div. No more silent failures.
3. swap! thunk leak: apply result wasn't trampolined, storing thunks
as signal values instead of evaluated results
Also fixed: assert= uses = instead of equal? for value comparison,
assert-signal-value uses deref instead of signal-value, HTML entity
decoding in script tag test source via host-call replaceAll.
Temperature converter demo page now shows live test results:
✓ initial celsius is 20
✓ computed fahrenheit = celsius * 1.8 + 32
✓ +5 increments celsius
✓ fahrenheit updates on celsius change
✓ multiple clicks accumulate
1116/1116 OCaml tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- render-html-island wraps body SSR in cek-try (graceful fallback to
empty container when island body has DOM/signal code)
- defcomp placeholder pattern: server renders safe placeholder div
with data-sx-island, browser hydrates the actual island
- cek-try primitive added to both server and browser OCaml kernels
- assert/assert= added to spec/harness.sx for standalone use
- Test source stored in <script type="text/sx-test" data-for="...">
with HTML entity decoding via host-call replaceAll
Temperature converter: 5 tests embedded in demo page. Test runner
hydrates and finds tests but body render is empty — needs debugging
of the specific construct that silently fails in render-to-dom.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Temperature converter tests (6 tests): initial value, computed
fahrenheit derivation, +5/-5 click handlers, reactive propagation,
multiple click accumulation.
New components:
- sx/sx/reactive-islands/test-runner.sx — reusable defisland that
parses test source, runs defsuite/deftest forms via cek-eval, and
displays pass/fail results with re-run button
- sx/sx/reactive-islands/test-temperature.sx — standalone test file
Added cek-try primitive to both browser (sx_browser.ml) and server
(sx_server.ml) for safe test execution with error catching.
Browser bundle now includes harness files (harness.sx,
harness-reactive.sx, harness-web.sx) for inline test execution.
Known: SSR renders test runner body instead of placeholder, causing
arity error on complex str expressions. Needs island SSR handling fix.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
web/harness-reactive.sx — signal testing (no DOM dependency):
assert-signal-value, assert-signal-has-subscribers,
assert-signal-subscriber-count, assert-computed-dep-count,
assert-computed-depends-on, simulate-signal-set!/swap!,
make-test-signal (signal + history tracking), assert-batch-coalesces
web/harness-web.sx — web platform testing (mock DOM, no browser):
mock-element, mock-set-text!, mock-append-child!, mock-set-attr!,
mock-add-listener!, simulate-click, simulate-input, simulate-event,
assert-text, assert-attr, assert-class, assert-no-class,
assert-child-count, assert-event-fired, assert-no-event,
event-fire-count, make-web-harness
Both extend spec/harness.sx. The reactive harness uses spec/signals.sx
directly — works on any host. The web harness provides lightweight DOM
stubs that record operations for assertion, no real browser needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Core reactive primitives (signal, deref, reset!, swap!, computed, effect,
batch, notify-subscribers, dispose-computed, with-island-scope,
register-in-scope) moved to spec/signals.sx — pure SX, zero platform
dependencies. Usable by any host: web, CLI, embedded, server, harness.
Web extensions (marsh scopes, stores, event bridge, resource) remain in
web/signals.sx, which now depends on spec/signals.sx.
Updated all load paths:
- hosts/ocaml/bin/sx_server.ml — loads spec/signals.sx before web/signals.sx
- hosts/ocaml/bin/run_tests.ml — loads both in order
- hosts/ocaml/browser/bundle.sh + sx-platform.js — loads core-signals.sx first
- shared/sx/ocaml_bridge.py — loads spec/signals.sx before web extensions
1116/1116 OCaml tests pass. Browser reactive island preview works.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
spec/harness.sx — spec-level test harness with:
- Mock platform (30+ default IO mocks: fetch, query, DOM, storage, etc.)
- Session management (make-harness, harness-reset!, harness-set!/get)
- IO interception (make-interceptor, install-interceptors)
- IO log queries (io-calls, io-call-count, io-call-nth, io-call-args)
- IO assertions (assert-io-called, assert-no-io, assert-io-count, etc.)
15 harness tests passing on both OCaml (1116/1116) and JS (15/15).
Loaded automatically by both test runners.
MCP tool: sx_harness_eval — evaluate SX with mock IO, returns result + IO trace.
The harness is extensible: new platforms just add entries to the platform dict.
Components can ship with deftest forms that verify IO behavior against mocks.
Tests are independent objects that can be published separately (by CID).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Added contains-deref? predicate to adapter-dom.sx. When rendering a text
expression that contains (deref ...) inside an island scope, the adapter
now wraps it in (reactive-text (computed (fn () (eval-expr expr env)))).
This tracks signal dependencies through arbitrary expressions like
(str (deref celsius) "°C") and (+ (* (deref celsius) 1.8) 32).
Previously only bare (deref sig) was reactive. Now compound expressions
like string interpolation and arithmetic over signals update in real
time. The temperature converter preview in sx-tools is fully reactive:
clicking +5/-5 updates both °C and °F displays live.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
defisland expressions are now rendered as fully reactive live previews.
The component is registered via sx-load-components, then called through
the native render-dom-island adapter path — same code path as real
islands on the page.
Bare (deref sig) expressions are fully reactive — clicking buttons
updates signal values and DOM updates in real time. Compound expressions
like (str (deref c) "°C") render statically (cek-reactive-text only
tracks bare deref). Next: auto-wrap deref-containing expressions in
computed signals for full reactive expression support.
Also cleaned up stale effect and preview-el signal from earlier attempts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
defisland expressions in the render tab are now evaluated through
render-to-dom inside with-island-scope, producing real DOM with signal
bindings. The rendered output persists in a stable container managed by
an effect, outside the reactive render tree.
Known limitation: signal subscriptions don't fire on button clicks yet —
the reactive wiring needs investigation into how deferred (setTimeout)
execution contexts interact with CEK signal tracking. Static rendering
(initial values) works correctly.
Also: removed stale effect that attempted hydrate-island approach.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Python-based MCP server (tools/mcp_services.py) for understanding the
microservice topology via static analysis:
- svc_status: Docker container status
- svc_routes: HTTP route table from blueprint scanning
- svc_calls: inter-service dependency graph (fetch_data/call_action/etc)
- svc_config: environment variables from docker-compose
- svc_models: SQLAlchemy models, columns, relationships
- svc_schema: live defquery/defaction manifest from running services
- alembic_status: migration count per service
- svc_logs/svc_start/svc_stop: service lifecycle
- svc_queries/svc_actions: SX query and action definitions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The tool table was listing only the original 11 tools. Now documents all 31
including smart editing, project-wide search, dev workflow, and git integration.
Also updated the protocol to mention pattern-based editing and sx_write_file.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- sx_changed: list .sx files changed since a ref with structural summaries
- sx_diff_branch: structural diff of all .sx changes vs base ref
- sx_blame: git blame for .sx files, optionally focused on a tree path
- sx_doc_gen: generate component docs from defcomp/defisland signatures
- sx_playwright: run Playwright browser tests with structured results
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New comprehension tools:
- sx_find_across: search pattern across all .sx files in a directory
- sx_comp_list: list all definitions (defcomp/defisland/defmacro/defpage/define)
- sx_comp_usage: find all uses of a component across files
- sx_diff: structural diff between two .sx files (ADDED/REMOVED/CHANGED)
- sx_eval: REPL — evaluate SX expressions in the MCP server env
Smart read_tree enhancements:
- Auto-summarise large files (>200 lines)
- focus param: expand only matching subtrees, collapse rest
- max_depth/max_lines/offset for depth limiting and pagination
Smart editing tools:
- sx_rename_symbol: rename all occurrences of a symbol in a file
- sx_replace_by_pattern: find+replace first/all pattern matches
- sx_insert_near: insert before/after a pattern match (top-level)
- sx_rename_across: rename symbol across all .sx files (with dry_run)
- sx_write_file: create .sx files with parse validation
Development tools:
- sx_pretty_print: reformat .sx files with indentation (also used by all edit tools)
- sx_build: build JS bundle or OCaml binary
- sx_test: run test suites with structured pass/fail results
- sx_format_check: lint for empty bindings, missing bodies, duplicate params
- sx_macroexpand: evaluate expressions with a file's macro definitions loaded
Also: updated hook to block Write on .sx files, added custom explore agent.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
sx->hypersx transform converts parsed SX to a readable indentation-based
format: CSS selector shorthand (div.card#main), signal sugar (@count,
signal(), :=, <-), string interpolation ("Count: {@count}"), and
structural keywords (when, if, let, map, for).
Implemented as pure SX in web/lib/hypersx.sx, loaded in browser via
js_of_ocaml platform. Added as "hypersx" tab in the tree editor.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>