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>
Register cond-scheme? as OCaml primitive — was defined in spec/evaluator.sx
but never exposed to the browser runtime, causing render.sx to crash with
"Undefined symbol: cond-scheme?" on every SX response. This broke URL
updates on navigation (handle-history never ran after the rendering error).
Tree editor render tab now extracts &key params from defcomp/defisland
definitions and shows input fields. Values substitute into the rendered
preview live as you type. Inputs live outside the reactive cond branch
so signal updates don't steal focus.
sx-tools page function accepts &key params (title, etc.) forwarded to
the overview component.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The render tab now correctly renders HTML tag expressions as live DOM
(div, h2, p, ul, etc. with Tailwind classes). Non-HTML expressions
(defcomp, define, etc.) are shown as serialized SX text.
Previous version tried eval-expr which errored silently in the browser.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Named paths let you navigate by structure name instead of opaque indices.
Both formats work in all MCP tools:
- Index: "(0 3 2)"
- Named: "defisland > let > letrec"
The server detects ">" in the path string and calls resolve-named-path
(SX function) which walks the tree matching child names at each level.
New SX functions: resolve-named-path, split-path-string, find-child-by-name.
MCP server: added trim/split primitives, resolve_path dispatcher.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tree editor island now has 4 tabs: tree, context, validate, render.
The render tab evaluates SX source as live HTML — type a (div (h2 "Hello"))
and see it rendered immediately.
MCP server paths changed from JSON arrays [0,2,1] to SX strings "(0 2 1)".
Fixes serialization issues and is more natural for an SX tool. The
json_to_path function now parses SX via sx-parse.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 4: defisland ~sx-tools/tree-editor — interactive tree viewer
embedded in the SX Tools page. Features:
- Textarea with :bind for SX source input
- Parse button to re-parse on demand
- Tree view: annotated tree with path labels, clickable nodes
- Context view: enclosing chain from root to selected node
- Validate view: structural integrity checks (catches missing body etc.)
MCP server fixes: added ident-start?, ident-char?, make-keyword,
escape-string, sx-expr-source — needed by parser.sx when loaded
into the MCP evaluator.
Also: .mcp.json for Claude Code MCP server config, CLAUDE.md protocol
for structural .sx file editing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 1-3 of the SX Tools plan — structural reading, editing, and
MCP server for .sx files.
lib/tree-tools.sx — Pure SX functions for tree comprehension and editing:
Comprehension: annotate-tree, summarise, read-subtree, get-context,
find-all, get-siblings, validate, navigate
Editing: replace-node, insert-child, delete-node, wrap-node, tree-set
Helpers: list-replace, list-insert, list-remove, replace-placeholder
lib/tests/test-tree-tools.sx — 107 tests covering all functions.
hosts/ocaml/bin/mcp_tree.ml — MCP server (stdio JSON-RPC) exposing
11 tools. Loads tree-tools.sx into the OCaml evaluator, parses .sx
files with the native parser, calls SX functions for tree operations.
The MCP server can be configured in Claude Code's settings.json as:
"mcpServers": { "sx-tree": { "command": "path/to/mcp_tree.exe" } }
1429 tests passing (1322 existing + 107 new tree-tools).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two bugs caused code blocks to render empty across the site:
1. ~docs/code component had parameter named `code` which collided with
the HTML <code> tag name. Renamed to `src` and updated all 57
callers. Added font-mono class for explicit monospace.
2. Batched IO dispatch in ocaml_bridge.py only skipped one leading
number (batch ID) but the format has two (epoch + ID):
(io-request EPOCH ID "name" args...). Changed to skip all leading
numbers so the string name is correctly found. This fixes highlight
and other batchable helpers returning empty results.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two paren bugs in home-stepper.sx caused the home page to render blank:
1. Line 222 had one extra ) that prematurely closed the letrec bindings
list — rebuild-preview and do-back became body expressions instead
of bindings, making them undefined in scope.
2. Lines 241-308 were outside the let/letrec scope entirely — the outer
let closed at line 240, so freeze-scope, cookie restore, source
parsing, and the entire div rendering tree had no access to signals
or letrec functions.
Also hardens defisland to wrap multi-expression bodies in (begin ...),
matching the Python-side fix from 9f0c541. Both spec/evaluator.sx and
the OCaml transpiled sx_ref.ml are updated.
Adds SX Tools essay under Applications — the revised plan for structural
tree reading/editing tools for .sx files, motivated by this exact bug.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Brings in sx-pub federated publishing protocol (Phases 1-4):
- DB models, IPFS publishing, federation, anchoring
- Live dashboard UI on the plan page
- Dev infrastructure (docker-compose, dev-pub.sh)
Conflicts: sx-browser.js (kept ocaml-vm rebuild), sx-pub.sx (kept sx-pub version with dashboard)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
defcomp/defisland with multi-expression bodies (let, letrec,
freeze-scope, effect) had only the LAST expression stored as body.
The browser received a truncated defisland missing let/letrec/signal
bindings, causing "Undefined symbol: code-tokens" on hydration.
Fix: body_exprs = expr[body_start:], wrapped in (begin ...) if
multiple. Also clear stale pickle cache on code changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- do-back: uses steps-to-preview + render-to-dom (O(1) render)
instead of replaying every do-step from 0 (O(n) DOM ops + cssx)
- Hydration effect: same rebuild-preview instead of step replay
- Cookie save moved to button on-click only (not per-step)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Promotes reactive-runtime from a plan page to a full applications section
with 7 nav items (ref, foreign FFI, state machines, commands, render loop,
keyed lists, app shell) and its own page function.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
22 JIT closure scoping tests covering:
- Basic closure var in map callback + context switch
- Signal + letrec + map (stepper pattern)
- Nested closures (inner lambda sees outer let var)
- Mutual recursion in letrec (is-even/is-odd)
- set! mutation of closure var after JIT compilation
- defisland with signal + letrec + map
- Deep nesting (for-each inside map inside letrec inside let)
All test the critical invariant: JIT-compiled lambdas must use
their closure's vm_env_ref, not the caller's globals.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests the exact pattern that broke the home stepper: a component
with letrec bindings referenced inside a map callback. The JIT
compiles the callback with closure vars merged into vm_env_ref.
Subsequent renders must use that env, not the caller's globals.
7 tests covering:
- letrec closure var in map callback (fmt function)
- Render, unrelated render, re-render (env not polluted)
- Signal + letrec + map (the stepper pattern)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the VM called a JIT-compiled lambda, it passed vm.globals
(the caller's global env) instead of cl.vm_env_ref (the closure's
captured env that was merged at compile time). Closure-captured
variables like code-tokens from island let/letrec scopes were
invisible at runtime, causing "Undefined symbol" errors that
cascaded to disable render-to-html globally.
Fix: call_closure uses cl.vm_env_ref at both call sites (cached
bytecode and fresh compilation).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
rebuild-preview had one extra close paren that closed the outer
(when container) prematurely, pushing do-back and build-code-dom
out of the letrec scope. Result: "Undefined symbol: build-code-dom".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The click buffer (capture + stopPropagation + replay) caused more
harm than good: synchronous XHR blocks the main thread during kernel
load, so there's no window where clicks can be captured. The buffer
was eating clicks after hydration due to property name mismatches.
Replace with pure CSS: buttons/links/[role=button] inside islands
get cursor:pointer from SSR. No JS needed, works immediately.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Buttons clicked before hydration get a subtle pulse animation
(sx-pending class) showing the click was captured. The animation
is removed when the click is replayed after hydration, or cleared
on boot completion as a fallback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The event buffer script checked _sxBoundislandHydrated (camelCase)
but mark-processed! sets _sxBoundisland-hydrated (with hyphen).
The mismatch meant stopPropagation() fired on EVERY click, killing
all island button handlers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The parser was reporting "Unexpected char: )" with no position info.
Added line number, column, and byte position to all parse errors.
Root cause: bind-sse-swap had one extra close paren that naive paren
counting missed because a "(" exists inside a string literal on L1074
(starts-with? trimmed "("). Parse-aware counting (skipping strings
and comments) correctly identified the imbalance.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- ocaml_sync.py: escape newlines in eval/load_source to prevent
protocol desync (bridge crashed on any multi-line SX)
- Stepper: do-back uses rebuild-preview (O(1) render) instead of
replaying all steps. Hydration effect same. Cookie save on button
click only.
- dom.sx: remove duplicate dom-listen (was shadowing the one at
line 351 that adapter-dom.sx's dom-on wraps)
- orchestration.sx: fix bind-sse-swap close paren count
- safe_eq: Dict equality via __host_handle for DOM node identity
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
adapter-dom.sx defines dom-on as a wrapper around dom-listen (adds
post-render hooks). But dom-listen was never defined — my earlier
dom-on in dom.sx was overwritten by the adapter's version. Rename
to dom-listen so the adapter's dom-on can call it.
This fixes click handlers not firing on island buttons (stepper,
stopwatch, counter, etc.).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Platform:
- sx-platform.js: extract ?v= query from script tag URL, append to
all .sx file XHR requests. Prevents stale cached .sx files.
Stepper performance:
- do-back: use rebuild-preview (pure SX→DOM render) instead of
replaying every do-step from 0. O(1) instead of O(n).
- Hydration effect: same rebuild-preview instead of step replay.
- Cookie save moved from do-step to button on-click handlers only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Clicks on island elements before hydration completes are captured
and replayed after boot finishes:
- shell.sx: inline script (capture phase) buffers clicks on
[data-sx-island] elements that aren't hydrated yet into window._sxQ
- boot.sx: after hydration + process-elements, replays buffered clicks
by calling target.click() on elements still connected to the DOM
This makes SSR islands feel interactive immediately — the user can
click a button while the SX kernel is still loading/hydrating, and
the action fires as soon as the handler is wired up.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server-side create-text-node was returning Nil, causing imperative
text nodes (stopwatch "Start"/"0.0s", imperative counter "0") to
render as empty in SSR HTML. Now returns the text as a String value,
which render-to-html handles via escape-html.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add wasm_hash (MD5 of sx_browser.bc.js) to shell template
- Script tags: /wasm/sx_browser.bc.js?v={hash}, /wasm/sx-platform.js?v={hash}
- Pass wasm_hash through helpers.py and ocaml_bridge.py
- Fix missing close paren in bind-sse-swap (broke SX parsing)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The DOM adapter treated all Dict values as empty (create-fragment).
But DOM nodes (text nodes, elements) from create-text-node/host-call
are wrapped as Dict { __host_handle: N } by js_of_ocaml. Now checks
for __host_handle and passes them through as DOM nodes.
Fixes stopwatch button text ("Start") and timer display ("0.0s")
which were missing because create-text-node results were discarded.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Navigation pipeline now works end-to-end:
- outerHTML swap uses dom-replace-child instead of morph-node (morph has
a CEK continuation issue with nested for-each that needs separate fix)
- swap-dom-nodes returns the new element for outerHTML so post-swap
hydrates the correct (new) DOM, not the detached old element
- sx-render uses marker mode: islands rendered as empty span[data-sx-island]
markers, hydrated by post-swap. Prevents duplicate content from island
body expansion + SX response nav rows.
- dispose-island (singular) called on old island before morph, not just
dispose-islands-in (which only disposes sub-islands)
OCaml runtime:
- safe_eq: Dict equality checks __host_handle for DOM node identity
(js_to_value creates new Dict wrappers per call, breaking physical ==)
- contains?: same host handle check
- to_string: trampoline thunks (fixes <thunk> display)
- as_number: trampoline thunks (fixes arithmetic on leaked thunks)
DOM platform:
- dom-remove, dom-attr-list (name/value pairs), dom-child-list (SX list),
dom-is-active-element?, dom-is-input-element?, dom-is-child-of?, dom-on
All 5 reactive-nav Playwright tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server-rendered dashboard showing live data from sx-pub API:
- Server status (DB, IPFS, actor, domain)
- Actor identity card with public key
- Collections grid with paths
- Published documents with CIDs and sizes
- Recent outbox activity feed
- Followers list
- API endpoint links for direct access
All phases marked as complete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New endpoints:
- POST /pub/anchor — batch unanchored Publish activities into Merkle tree,
pin tree to IPFS, submit root to OpenTimestamps, store OTS proof on IPFS
- GET /pub/verify/<cid> — verify a CID's Merkle proof against anchored tree
Uses existing shared/utils/anchoring.py infrastructure:
- build_merkle_tree (SHA256, deterministic sort)
- get_merkle_proof / verify_merkle_proof (inclusion proofs)
- submit_to_opentimestamps (3 calendar servers with fallback)
Tested: anchored 1 activity, Merkle tree + OTS proof pinned to IPFS,
verification returns :verified true with full proof chain.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New endpoints:
- GET /pub/outbox — paginated activity feed
- POST /pub/inbox — receive Follow/Accept/Publish from remote servers
- POST /pub/follow — follow a remote sx-pub server
- GET /pub/followers — list accepted followers
- GET /pub/following — list who we follow
Federation mechanics:
- HTTP Signature generation (RSA-SHA256) for signed outgoing requests
- HTTP Signature verification for incoming requests
- Auto-accept Follow → store follower → send Accept back
- Accept handling → update following state
- Publish mirroring → pin remote CID to local IPFS
- deliver_to_followers → fan out signed activities to all follower inboxes
- Publish now records activity in outbox for federation delivery
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New endpoints:
- POST /pub/publish — pin SX content to IPFS, store path→CID in DB
- GET /pub/browse/<collection> — list published documents
- GET /pub/doc/<collection>/<slug> — resolve path to CID, fetch from IPFS
- GET /pub/cid/<cid> — direct CID fetch (immutable, cache forever)
New helpers: pub-publish, pub-collection-items, pub-resolve-document, pub-fetch-cid
Tested: published stdlib.sx (6.9KB) → QmQQyR3Ltqi5sFiwZh5dutPbAM4QsEBnw419RyNnTj4fFM
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The opam base image has dune in the switch but not on PATH.
RUN eval $(opam env) doesn't persist across layers. Install dune
explicitly and set PATH so dune is available in build steps.
Also fix run-tests.sh to respect QUICK env var from caller
(was being overwritten to false).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>