Files
rose-ash/plans/sx-review/core.md
giles 4f766ea4f1 plans: SX review master remediation plan + evidence
Consolidates the three-lane review (core K01-K110, hosts J*/C*/JS*/P*/S*,
conformance F1-F15) into plans/sx-review/:
- PLAN.md — 15 workstreams, phased execution, full per-finding coverage
  ledger (every ~213 finding-instances mapped to a workstream + status)
- RULINGS.md — 40 draft normative rulings (Phase-0 gate)
- core.md / hosts.md / conformance.md — the lane evidence files

dc7aa709 quick-wins batch marked DONE in the ledger; K01 (guard re-raise
hang), S1 (live HTTP crash), K03 (shift-k), and W14 (test gate) flagged as
the highest-value open work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 21:28:41 +00:00

76 KiB
Raw Blame History

SX Language Core Review — spec/ semantics

Reviewer axis: LANGUAGE CORE (spec/evaluator.sx CEK machine, parser.sx, primitives.sx, render.sx, special-forms.sx, eval-rules.sx, stdlib.sx, signals.sx, coroutines.sx, canonical.sx). Note: brief mentions spec/types.sx — it no longer exists; strict-typing machinery lives in evaluator.sx. Status: COMPLETE — all 8 dimension sweeps merged (CEK core, env/scope, HO forms, special forms/macros, parser/serializer/canonical, primitives/stdlib, render modes, strict typing + signals/coroutines/harness).

TOTALS: 104 CONFIRMED findings (3 critical, 26 high, 40 medium, 2 low-medium, 33 low)

  • 5 SUSPECTED. Every CONFIRMED item has a runtime repro (fresh sx_server.exe unless noted). All 3 criticals additionally re-verified on the shipped WASM browser kernel (see CROSS-LANE CHECK).

THEMES a ranker should know:

  1. Nested cek-run instead of CEK frames is one root cause behind ≥4 findings (shift-k double-execution, threading guard/IO break, + suspected macro/let-values/qq boundaries).
  2. Handler still installed while handler runs explains the guard/handler-bind hang family.
  3. Name-before-env dispatch explains the ~60 unshadowable names + HO-not-first-class family.
  4. Global mutable stacks popped only on normal exit explains provide/winder/batch leak family (scope stack, winders, batch-depth — none unwind-safe).
  5. Test-runner-only bindings make whole suites (values, canonical floats, batch, coroutines) green for features the shipped runtime doesn't have; one test passes vacuously because of the very bug it tests (signal-return).
  6. Per-host re-bound platform primitives (parse-number, char-code, escape-string, split, get…) are the drift engine behind parser AST divergence + harness/runtime divergence.

CROSS-LANE CHECK (vs /tmp/sx-review/hosts.md and conformance.md, done 2026-07-03):

  • All 3 criticals re-verified on the shipped WASM browser kernel (js_of_ocaml build browsers actually load; probe harness from the conformance lane): guard re-raise HANGS (node killed at 25s), signal-condition → 42 (same kont drop), shift repro → identical double-execution trace (99 ("r=escaped" "after-k" "r=99")). Kernel-family bugs: native server AND production browser. Conformance F-2 (corpus never runs on WASM) explains why nothing caught them there.
  • Not masked by the JIT: hosts J2/J9 confirm guard-installing lambdas are interpret-only and any raise/call-cc in JIT'd code falls back to the CEK — my criticals are the live path.
  • Three independent double-side-effect mechanisms now on record: my shift-k nested cek-run (critical #3), hosts J1 (-> miscompiled under serving-JIT, steps re-run), hosts J2 (JIT-fallback re-runs whole call). Same user-visible symptom, three distinct fixes.
  • One of my findings corrected (apply — see the finding below); expt int63 wrap corroborated by conformance F-1 (WASM is worse: (expt 2 62) → 0); unshadowable-HO finding extended by hosts J8 (the VM DOES honor local bindings — CEK/VM divergence within one host); render dom/html attr parity independently confirmed as hosts C19; values/eq?/eqv? runner-only bindings corroborated by conformance F-7/F-9.
  • No contradiction on canonical/CIDs: conformance's working cid-from-sx is a native kernel primitive (verified: works with spec/canonical.sx not loaded). My canonical.sx finding concerns the spec guest implementation — production CIDs bypass it. Two parallel CID implementations, only the native one exercised; spec-vs-native canonical-form agreement is untested (conformance F-3 checked native-vs-WASM only).

Verification recipe: sx_harness_eval (MCP) cross-checked against fresh real-runtime processes printf '(epoch 1)\n(eval "...")\n' | timeout 30 hosts/ocaml/_build/default/bin/sx_server.exe. TOOLING CAVEATS found during review (also listed as handoffs): (1) the MCP harness primitive table diverges from the real runtime; (2) sx_harness_eval is NOT a fresh sandbox — state persists and cross-contaminates calls; (3) sx_read_subtree ignores path, sx_read_tree ignores max_lines. All critical/divergent probes were re-verified on fresh sx_server.exe processes.


CONFIRMED findings (most severe first)

[critical] [CONFIRMED×2] Any raise/error inside a guard clause body or handler-bind handler loops forever — handler runs with its own handler frame still installed

  • Location: spec/evaluator.sx, raise-eval case of step-continue (~4547-4573) + kont-unwind-to-handler (236-259); inherited by step-sf-guard (~1693)
  • What: kont-unwind-to-handler returns {:handler match :kont kont} where kont still contains the matched handler frame; the handler is invoked with that kont. A raise inside the handler re-matches the same handler → infinite loop. Not just explicit re-raise: ANY error while a handler/clause body runs ((error ...), a raised different value) hangs instead of propagating. CL/R7RS: handlers run with the enclosing (outer) handler set. guard desugars clause bodies to run INSIDE the handler-bind extent ((__guard-k (cond ...)) — clauses evaluate before the escape), so the memory'd gotcha "(raise e) in a guard clause hangs" is exactly this. Contrast: the no-matching-clause auto-reraise is R7RS-correct ((guard (outer (true outer)) (guard (e ((= e 1) "one")) (raise 2))) → outer catches 2) because the sentinel re-raise happens after the call/cc return, OUTSIDE handler-bind — which is exactly how clause bodies should also run.
  • Repro (bounded CLI, all timeout exit 124): (guard (e (true (raise e))) (raise 42)); (handler-bind (((fn (c) true) (fn (c) (raise c)))) (raise 1)); same with (raise "different") and (error "again").
  • Cross-check: reproduced on the shipped WASM browser kernel (hangs, killed at 25s) — affects production browsers, not just the server. Hosts lane J2/J9: guard-installing lambdas are interpret-only, so the JIT never masks this.
  • Coverage: test-r7rs.sx guard suite + test-conditions.sx cover happy paths only; no test raises from within a handler. test-cek-try-seq.sx "error in error handler propagates" passes because cek-try is a different mechanism.

[critical] [CONFIRMED] signal-return frame key mismatch drops the caller's continuation — continuable signal/raise-continuable returns the handler value as the WHOLE program's result; the covering test passes vacuously

  • Location: spec/evaluator.sx, make-signal-return-frame (line 182, stores saved kont under :f) vs signal-return case of step-continue (~4509-4512, reads (get frame "saved-kont")); mirrored in hosts/ocaml/lib/sx_runtime.ml:210-231 (CekFrame get has no "saved-kont" mapping → Nil)
  • What: the resume kont is always nil, so after the handler returns, its value becomes the terminal value of the entire CEK run — every frame outside the signal site (arithmetic, enclosing lists, asserts) is silently discarded.
  • Repro: (list "outer" (handler-bind (((fn (c) true) (fn (c) 42))) (+ 1 (signal-condition 5))) "end")42; expected ("outer" 43 "end"). Same with raise-continuable42. The shipped test expr (handler-bind (((fn (c) true) (fn (c) (* c 10)))) (+ 1 (signal-condition 5)))50 on both CLI and harness, yet the test asserting 51 PASSES under run_tests.exe — the dropped continuation includes the assert-equal frame itself, so the assertion never executes (vacuous pass).
  • Cross-check: reproduced byte-identically (42) on the shipped WASM browser kernel.
  • Coverage: test-conditions.sx "signal returns handler value to call site" — passing vacuously; the bug defeats its own test.

[critical] [CONFIRMED] Invoking a shift-captured continuation uses a nested cek-run — escaping across that boundary re-executes the outer program tail (double execution, duplicated side effects); raising inside a resumed k can't reach outer handlers

  • Location: spec/evaluator.sx, continue-with-call, continuation? branch (~4708-4716): (let ((result (cek-run (make-cek-value arg env captured)))) (make-cek-value result env kont))
  • What: the nested run's kont ends at the captured frames; (a) a call/cc escape invoked inside the resumed extent rewrites the kont inside the nested run, which then runs the rest of the program to completion, returns that as the value of (k arg), and the outer run executes the program tail again; (b) handler frames in the outer kont are invisible to kont-unwind-to-handler inside the nested run.
  • Repro (a): (do (define log (list)) (define r (call/cc (fn (esc) (reset (do (shift k (do (k 1) (set! log (append log (list "after-k"))) 99)) (esc "escaped") "unreached"))))) (set! log (append log (list (str "r=" r)))) (list r log)) → actual (99 ("r=escaped" "after-k" "r=99")) (tail executed twice); expected ("escaped" ("r=escaped")).
  • Repro (b): (guard (e (true (list "caught" e))) (reset (do (shift k (k 1)) (raise "boom"))))Unhandled exception: "boom"; expected ("caught" "boom").
  • Cross-check: repro (a) reproduced with the identical wrong trace (99 ("r=escaped" "after-k" "r=99")) on the shipped WASM browser kernel. Note: hosts J1/J2 are two FURTHER independent double-side-effect mechanisms (JIT -> miscompile; JIT-fallback re-run) — three distinct fixes needed for "side effects ran twice" reports.
  • Coverage: not covered (test-cek-advanced.sx shift/reset tests never cross the boundary with call/cc or raise).

[high] [CONFIRMED] Caller's immediate frame leaks into interpreted lambda calls — partial dynamic scoping, and the JIT disagrees

  • Location: spec/evaluator.sx, continue-with-call ((local (env-merge (lambda-closure f) env)) ~4739; same in call-lambda ~896) + env_merge in hosts/ocaml/lib/sx_types.ml:390
  • What: when the call-site env is NOT a descendant of the lambda's closure env, env_merge copies the caller's top frame bindings into the lambda's local env. Free variables in the body resolve to the caller's locals — a lexical-scoping violation. Depth-1 only (a binding one frame deeper does not leak). The JIT path disagrees: a VM-compiled body raises "VM undefined" for the same program — behavior flips depending on whether the body got JIT-compiled.
  • Repro: (do (define mg (fn () (fn () (guard (e (true e)) leakedz)))) (define gz (mg)) (let ((leakedz 66)) (gz)))66 (guard forces interpretation); expected undefined-symbol error. Without guard → "VM undefined: leaked" (JIT). Depth-2 variant → Undefined symbol.
  • Coverage: not covered — test-scope.sx "environment-isolation" tests only the lambda→caller direction.

[high] [CONFIRMED] letrec injects its bindings into foreign lambdas' closure envs — permanent global contamination

  • Location: spec/evaluator.sx, sf-letrec (~1370: (env-bind! (lambda-closure val) n (env-get local n)))
  • What: after evaluating inits, letrec binds ALL letrec names into the closure env of every lambda value. make_lambda stores the defining env directly, so a letrec whose value is a pre-existing (e.g. top-level) lambda writes the letrec names into that lambda's closure — the global env — permanently.
  • Repro: (do (define idf (fn (x) x)) (letrec ((zzq idf) (zzn 55)) nil) zzn)55; expected "Undefined symbol: zzn". (This leak also polluted the shared MCP harness image across calls during verification.)
  • Coverage: not covered — test-scope.sx "letrec-edge" only binds lambdas created inside the letrec (extra binds are no-ops there).

[high] [CONFIRMED] Named let leaks its loop name into the enclosing env frame and clobbers same-name bindings

  • Location: spec/evaluator.sx, sf-named-let (~1035: (env-bind! (lambda-closure loop-fn) loop-name loop-fn))
  • What: lambda-closure loop-fn IS the enclosing env (no fresh frame), so the loop name is bound into the surrounding scope: visible after the form, and it clobbers (not shadows) an existing binding of the same name.
  • Repro: (do (let lp ((i 0)) i) (lambda? lp))true (expected unbound). (let ((lp2 5)) (let lp2 ((i 0)) i) lp2) → loop lambda in interpreter, nil under JIT — never the expected 5.
  • Coverage: not covered — test-named-let-sx locks set!-accumulator patterns only.

[high] [CONFIRMED×3] ~60 special-form/HO names are silently unshadowable — define/let/defmacro accepted, call-position dispatch ignores them

  • Location: spec/evaluator.sx step-eval-list (~1801-1958) — head-name match runs before any env lookup; only the _ fallthrough (custom special forms, ~1959) checks (not (env-has? env name))
  • What: list-head dispatch checks built-in special/HO forms BEFORE env lookup. (define bind (fn (a b) "mine")) succeeds (type-of says lambda) but (bind 1 2)1 (special form runs). (define map ...), (let ((map ...)) ...), (defmacro if ...), (defmacro map ...) — all silently ignored in call position while honored in value position. Regular primitives ARE properly shadowable ((define get ...), (define inc ...) → user def wins) — only this name set is hijacked, and custom special forms DO respect user bindings, making built-ins doubly inconsistent.
  • Unshadowable names (extracted from dispatch): if when cond case and or let let* lambda fn define defcomp defisland defmacro defio define-foreign io begin do guard quote quasiquote -> ->> |> as-> set! letrec reset shift deref scope provide peek provide! context bind emit! emitted handler-bind restart-case signal-condition invoke-restart match let-match dynamic-wind map map-indexed filter reduce some every? for-each raise raise-continuable call/cc call-with-current-continuation perform define-library import define-record-type define-protocol implement parameterize syntax-rules define-syntax. Collision-prone short ones: map filter reduce some bind match peek context deref guard io do case.
  • Repro: (do (define map (fn (f xs) "mine")) (map (fn (x) (* x 10)) (list 1 2)))(10 20); (let ((map (fn (a b) 42))) (map 1 2))Error: rest: 1 list arg; (let ((-> (fn (a b) 99))) (-> 1 2))Not callable: nil; (do (defmacro if (a b c) 99) (if true 1 2)) → 1.
  • Coverage: not covered anywhere. Memory gotcha "bind/conj/disj shadowed" confirmed for bind; conj/disj aren't core primitives (guest-worktree lore).

[high] [CONFIRMED] cond grammar is ambiguous — an all-clauses-len-2 heuristic silently switches modes; multi-expr clause bodies are dropped or crash; flat-intent code can silently return the wrong value

  • Location: spec/evaluator.sx, step-sf-cond (a scheme? detection binding selects clause-mode vs flat-pair mode)
  • What: cond supports flat pairs (cond t1 r1 ... :else d) (the only documented syntax — eval-rules.sx:64) plus an undocumented Scheme clause mode auto-detected iff every arg is a 2-element list (or (test => proc)). All verified consequences:
    • Single clause with multi-expr body: (cond ((= 1 1) (set! a 1) (set! b 2))) → nil, neither set! runs — silent total drop of side effects.
    • Multi-expr body + other clauses: (cond ((= 1 1) "a" "b") (:else "no"))Not callable: true — one len≠2 clause anywhere flips the WHOLE cond to flat mode.
    • Silent misinterpretation: (do (define x false) (define y true) (cond (not x) (list 1) (not y) (list 2)))false (clause-mode reads (not x) as test=not, result=x); flat reading gives (1). Wrong answer, no error.
    • Test-only clause (cond (5)) → nil (Scheme: 5); poisons detection: (cond (true "t") (5))Not callable: nil.
    • Trailing odd flat arg silently ignored, never evaluated: (cond (set! a 99)) leaves a unchanged.
  • Coverage: flat tested (test-eval.sx:306-312); clause mode only via cond-arrow suite (test-r7rs.sx:135-145). Ambiguity/multi-expr/test-only uncovered; clause mode entirely undocumented.

[high] [CONFIRMED] (unquote-splicing x) longhand silently no-splices; only splice-unquote is recognized

  • Location: spec/evaluator.sx, qq-expand (checks (symbol-name (first item)) = "splice-unquote" only)
  • What: ,@ sugar parses to splice-unquote and works; the R7RS-standard longhand unquote-splicing fails dispatch, is recursed into as an ordinary list, and is emitted literally — silent zero-splice. (unquote x) longhand works.
  • Repro: (quasiquote (a (unquote-splicing xs)))(a (unquote-splicing xs)); `(a ,@xs) and (splice-unquote xs)(a 1 2).
  • Coverage: not covered — worse, test-macros.sx tests are NAMED "unquote-splicing …" (lines 43-63) while all using ,@ sugar, actively reinforcing the trap. (Confirms memory gotcha; root cause now located.)

[high] [CONFIRMED] dynamic-wind before-thunks never re-run on continuation re-entry; global length-based winder stack corrupts across sibling wind contexts (afters skipped/duplicated)

  • Location: spec/evaluator.sx, continue-with-call callcc-continuation branch (~4702-4707: (do (wind-escape-to w-len) ...)), wind-escape-to (261-271)
  • What: invoking a captured continuation only pops after-thunks down to the captured length of the global *winders* stack. No common-ancestor computation, no before-thunks on entry (R7RS requires before/after along the path between extents). Lengths from unrelated wind contexts collide: resuming a k captured inside wind A while inside wind B (equal depth) unwinds nothing, then A's wind-after frame pops B's winder.
  • Repro 1 (re-entry): capture k inside wind, escape, re-invoke → (2 ("b" "a" "a")); expected (2 ("b" "a" "b" "a")) (before not re-run; after ran twice).
  • Repro 2 (sibling): capture in wind A, re-invoke from inside wind B → (2 ("A-in" "A-out" "B-in" "A-out")); expected B-out + A-in before final A-out. B's after silently never runs (resource-leak class), A's runs twice.
  • Coverage: test-dynamic-wind.sx (8 tests): normal return, raise, one-shot escape only.

[high] [CONFIRMED×2] guard re-raise sentinel is forgeable — a body/clause legitimately returning (list '__guard-reraise__ X) is misinterpreted as a re-raise of X

  • Location: spec/evaluator.sx, step-sf-guard (~1693-1767): sentinel (make-symbol "__guard-reraise__"), detected by structural = on any 2-element list escaping the guard
  • Repro: (guard (e (true (list (quote __guard-reraise__) 42))) (raise 1))Unhandled exception: 42; (guard (e (true "handled")) (list (quote __guard-reraise__) 7))Unhandled exception: 7 — the guard body's return value converted into a raise. Should be an unforgeable/gensym'd token. (Severity judged high by one reviewer, low by another — data-dependent conversion of values into raises; rank accordingly.)
  • Coverage: not covered.

[high] [CONFIRMED] ->/->> non-HO steps run in a nested CEK with empty kont — guard and IO suspension broken through threading

  • Location: spec/evaluator.sx, thread-insert-arg/thread-insert-arg-last (7289) call eval-expr (4828: cek-run with kont (list)); "thread" frame handler (~4074) stays CEK-native only for ho-form-name? heads
  • What: a threaded non-HO step evaluates in a fresh machine that can't see outer guard frames and can't suspend. (a) raise inside a threaded call escapes an enclosing guard; (b) IO/effects inside a threaded step hard-crash instead of suspending. The HO path is CEK-native and correct — same expression works or fails depending on the step's head symbol. Same root pattern as the shift-k critical.
  • Repro: (define boom (fn (x) (raise "T"))) (guard (e (else "caught")) (-> 1 boom))Unhandled exception: "T" (map version → caught). (-> {:op "noop"} (perform))Error: Sx_vm.VmSuspended(_,_) (map version suspends/resumes fine).
  • Coverage: not covered

[high] [CONFIRMED] 2-arg (reduce f coll) silently returns the collection unchanged

  • Location: spec/evaluator.sx, ho-setup-dispatch "reduce" branch (~3671) + ho-swap-args (~3557)
  • What: fn-first 2-arg reduce makes init the collection and coll nil → returns init. Expected: fold with first element as init (Scheme/Clojure) or arity error. Asymmetrically, data-first (reduce coll f) DOES fold — with nil init (works only via nil-coercion in +/str).
  • Repro: (reduce + (list 1 2 3))(1 2 3) (expected 6 or error); (reduce (list 1 2 3) +)6.
  • Coverage: not covered (tests only use 3-arg forms)

[high] [CONFIRMED] ho-swap-args misreads (reduce init f coll) — breaks (-> init (reduce f coll))

  • Location: spec/evaluator.sx, ho-swap-args reduce branch: (list b (nth evaled 2) a)
  • What: with non-callable arg0, (reduce init f coll) treats arg0 as coll and arg2 as init — the threaded scalar seed becomes the "collection" → cryptic host error. The thread handler inserts the threaded value FIRST for HO forms, so any -> reduce with a scalar seed hits this.
  • Repro: (-> 0 (reduce + (list 1 2 3)))Error: rest: 1 list arg (expected 6); same for (reduce 0 + (list 1 2 3)).
  • Coverage: not covered — thread-ho suite only tests (-> coll (reduce + 0)) (test-cek-advanced.sx:673)

[high] [CONFIRMED] Data-first ho-swap-args silently drops all args beyond the second

  • Location: spec/evaluator.sx, ho-swap-args non-reduce branch: (list b a)
  • What: when arg0 is data and arg1 callable, everything after arg1 is discarded — a data-first multi-collection map silently maps over only the first collection; with no lambda-arity enforcement, garbage results, not errors.
  • Repro: (map (list 1 2) (fn (x) (* x 10)) (list 3 4))(10 20); (map (list 1 2) (fn (x y) (+ x y)) (list 30 40))(1 2) (y → nil, (+ 1 nil) = 1).
  • Coverage: not covered

[high] [CONFIRMED] Infinite recursive component hangs the renderer — no depth guard

  • Location: web/adapter-html.sx render-html-component/render-list-to-html; spec/render.sx has no recursion bound
  • What: a self-referencing component with no base case (or data-driven cycle) recurses forever — one render pins the server thread indefinitely. No depth limit or cycle detection.
  • Repro: (do (defcomp ~loop () (div (~loop))) (render-to-html '(~loop) (current-env))) → never returns (killed at 20s). Bounded (~nest :n 3) renders fine.
  • Coverage: not covered (needs a depth limit + test)

[high] [CONFIRMED] append! silently no-ops on all derived lists

  • Location: spec/primitives.sx append! (+ OCaml impl)
  • What: append! mutates only literal (list ...) cells. Lists produced by map, filter, rest, reverse, append are silently unappendable — no error, mutation lost. append! returns the appended value, masking the failure.
  • Repro: (let ((xs (map (fn (x) x) (list 1 2)))) (append! xs 3) xs)(1 2); literal list → (1 2 3).
  • Coverage: test-primitives.sx:339 uses append! only on a literal-list accumulator.

[high] [CONFIRMED] expt silently wraps at 63-bit int; inconsistent with +/* which promote to float

  • Location: spec/primitives.sx expt
  • Repro: (expt 2 62)-4611686018427387904; (expt 2 100)0; but (* 4611686018427387904 4) → float and (+ 9223372036854775807 1) → float. (expt 2.0 100) correct.
  • Coverage: test-math.sx:66-71 — overflow not covered.

[high] [CONFIRMED] MCP harness primitive table diverges from real runtime — invalidates harness-based verification

  • Location: hosts/ocaml/bin/mcp_tree.ml (own primitive table, e.g. bind "contains?" L484, bind "split" L563) vs hosts/ocaml/lib/sx_primitives.ml (sx_server)
  • What: sx_harness_eval runs a parallel implementation of many primitives. Divergences (harness → runtime): (empty? "")/(empty? {}) false → true (test-primitives.sx:89 asserts true — harness contradicts a passing test); (get {:a 1} :a 99) nil even for present key → 1; (get {:a 1} :zz 99) nil → 99; (get (list 10 20) 1) nil → 20; (split "a--b" "--") char-class → substring; (split "abc" "") crash → ("a" "b" "c"); equal? undefined → defined; (contains? {:a 1} :a) true → error; (keyword-name :kw) "" → error. CLAUDE.md mandates harness verification, so this drift silently produces false findings/passes.
  • Coverage: nothing tests harness/runtime parity. (Cross-lane: host tooling — see handoffs — but it's the spec-mandated verification path.)

[high] [CONFIRMED] contains? does not support dicts in the real runtime, contradicting its spec doc

  • Location: spec/primitives.sx contains? (":doc … Dicts: key check"); sx_primitives.ml
  • Repro: (contains? {:a 1} :a)Unhandled exception: "contains?: 2 args" (misleading arity error); lists/strings work.
  • Coverage: list membership only (run_tests.ml:1255); no dict case.

[high] [CONFIRMED] canonical.sx depends on test-runner-only helpers — content addressing fails on ANY number outside run_tests

  • Location: spec/canonical.sx, canonical-number (46-59) calls contains-char? (defined only in run_tests.ml:728 / run_tests.js:85) and trim-right (run_tests.js:87 only — not even OCaml run_tests). Neither exists in sx_primitives.ml, sx_server.ml, or mcp_tree.ml.
  • What: canonical-serialize/content-id on the production server errors on any number. In the OCaml test runner the trim-right branch (floats with trailing zeros) is unreachable-but-passing because tests only canonicalise integers.
  • Repro: fresh sx_server: (load "spec/canonical.sx") (canonical-serialize 42)Undefined symbol: contains-char?; with a shim, (canonical-serialize 0.1)Undefined symbol: trim-right.
  • Coverage: test-canonical.sx covers ints/dict-sorting/CIDs — never a non-.0 float; failure mode invisible to all suites.

[high] [CONFIRMED] Serializer emits dict keys unescaped — non-identifier keys produce unparseable/wrong output; canonical form not a fixed point (CID hazard)

  • Location: spec/parser.sx sx-serialize-dict (emits (str ":" key)); spec/canonical.sx canonical-dict (~79, same pattern)
  • What: dict keys are strings; both serializers print : + raw key. Keys with spaces/parens/non-ident chars produce output that reparses differently or errors. Since canonical-serialize feeds sha3-256, CIDs exist for values whose canonical form violates canonical(parse(canonical(x))) = canonical(x). The native reader accepts string keys {"a b" 1}, so such dicts are creatable from plain source.
  • Repro: dict with key "hello world""{:hello world 2 :k 1}" → reparse errors; {(+ 1 2) 5} → key "(+ 1 2)" → serializes {:(+ 1 2) 5} → garbage.
  • Coverage: "serialize dict round-trips" uses keyword-shaped keys only.

[high] [CONFIRMED] Same source parses to different ASTs across the four ident/number classifier variants

  • Location: hosts/ocaml/lib/sx_parser.ml:36-46 (native), hosts/ocaml/bin/sx_server.ml:1330-1348, hosts/ocaml/bin/mcp_tree.ml:391-410, hosts/javascript/platform.py:2622-2626 — four different ident-start/ident-char tables feeding the one spec grammar
  • What (all verified live): (a,b) → single symbol on native/mcp/JS but (a (unquote b)) on sx_server guest; unicode idents accepted by mcp guest only (forbidden by the production reader); $x/|y| symbols on sx_server guest only; 0x10/0b101/1_000 → numbers 16/5/1000 on native (undocumented C-style acceptance) vs number 0 + symbol x10 on guest/JS (silent token split); inf/nan/-inf are float literals on native (can't be variable names!) vs symbols on guest/JS; 1+/1abc single symbols native vs silent 1 + symbol split guest/JS ((1+ 2) → 3-element list); #t/#f booleans native vs Undefined symbol: reader-macro-get on OCaml guest vs "Unknown reader macro" on JS; {1 2} rejected native vs silently stringified key "1" guest/JS.
  • Coverage: none of these tokens appear in any test file — suites exercise only the intersection.

[high] [CONFIRMED] 1e bare-exponent numbers silently parse to nil in the guest parser

  • Location: spec/parser.sx read-numberparse-number fallthrough (nil emitted as a value, no error)
  • Repro: (sx-parse "1e")(nil); JS parseAll('1e')[null]; native reader yields Symbol "1e" — a third behavior. (foo 1e) becomes (foo nil) silently.
  • Coverage: only valid exponent forms tested.

[high] [CONFIRMED] Guest parser cannot produce rationals on server/tooling hosts — 1/2 throws

  • Location: spec/parser.sx read-number (215-231); sx_server.ml:1325 and mcp_tree.ml:384 override parse-number to always return float, shadowing the Integer-aware sx_primitives version; make-rational rejects (Number,Number)
  • Repro: fresh sx_server: (load "spec/parser.sx") (sx-parse "1/2")make-rational: expected 2 integers. Works only in run_tests env. Native reader parses 1/2 fine.
  • Coverage: test-rationals.sx (62 tests) never uses sx-parse; test-parser.sx has zero rational tests.

[high] [CONFIRMED] Strict mode: HO-form callbacks bypass type checks entirely

  • Location: spec/evaluator.sx step-continue — map/filter/reduce/for-each/some/every/multi-map frames call continue-with-call directly; only the "arg" frame runs strict-check-args (enforcement site 4152-4194). Same in sx_ref.ml:1009.
  • What: with strict on and types declared for f, (f "a") errors but (map f coll)/(filter f coll)/(reduce f init coll)/(for-each f coll)/(every? f coll)/(some f coll) silently pass mistyped elements. Also unchecked: cond => arrow calls, call/cc continuation invocation, exception-handler invocation, signal-subscriber cek-calls.
  • Repro: hh typed (x number): (hh "abc") → type error; (map hh (list "a" "b"))("a" "b") silently.
  • Coverage: test-strict.sx checks direct calls only.

[high] [CONFIRMED] Strict mode: apply bypasses type checks on the target function

  • Location: hosts/ocaml/lib/sx_primitives.ml:1534 / sx_server.ml:1240 — native prim spreads args and calls directly
  • Repro: (apply hh (list "a"))"a" (no error); direct (hh "a") errors.
  • Coverage: not covered.

[high] [CONFIRMED] dispose-computed is a no-op — computed signals leak subscriptions after disposal

  • Location: spec/signals.sx, dispose-computed(signal-remove-sub! dep nil) passes nil as the subscriber; the actual recompute closure is trapped in computed's letrec and unreachable. The island-scope disposer registered by computed is therefore broken (contrast effect, whose dispose-fn works).
  • Repro: computed on a2 (1 run); (dispose-computed c2); (reset! a2 5) → runs=2, value updated. Expected: runs=1, unchanged. Subscriber leak in island teardown.
  • Coverage: no dispose-computed test exists.

[high] [CONFIRMED] Exception inside batch permanently wedges the reactive system

  • Location: spec/signals.sx, batch — increments *batch-depth*, runs thunk with no unwind protection; decrement skipped on throw
  • What: after any error escapes a batch thunk (even if caught outside), *batch-depth* stays >0 — every future notify-subscribers queues forever and never flushes; all reactivity dead. Related: (import (sx signals)) copies value bindings rather than aliasing, so the top-level *batch-depth* reads 0 while the library-internal one is 1 (exported mutable state vars are misleading).
  • Repro: effect on a3 (fired=1); (guard (e (true "caught")) (batch (fn () (error "boom")))) → caught; (reset! a3 2) → fired stays 1. Control test without error flushes correctly.
  • Coverage: not covered.

[high] [CONFIRMED — surfaced by hosts lane, verified here] emit!/emitted state accumulates across evaluator invocations — cross-request contamination on the server

  • Location: spec/evaluator.sx scope/emit frame handlers + the process-global scope stacks (hosts: sx_primitives.ml _scope_stacks)
  • What: (scope (emit! :k 1) (emit! :k 2) (len (emitted :k))) returns 2, then 4, then 6 on successive epoch-server evals — the emit accumulator for a normally-exited scope persists in process-global state and each new scope sees prior invocations' values. On the HTTP server this means one request's emitted values are visible to the next (correctness + information-leak class). Complements the provide/raise leak finding: the scope facility's global stacks are neither unwind-safe NOR invocation-scoped. (My in-eval probe showed no leak within one evaluation — the leak is across evaluator entries.)
  • Repro: three identical (eval "(scope (emit! :k 1) (emit! :k 2) (len (emitted :k)))") epochs on one fresh sx_server → 2, 4, 6. JIT disabled, so not a VM bug.
  • Coverage: scope/emit!/emitted have zero tests (noted previously); cross-invocation behavior untested anywhere.

[medium] [CONFIRMED] provide's dynamic value permanently leaks on non-local exit (raise, shift)

  • Location: spec/evaluator.sx, step-sf-provide (:3344 scope-push!) + "provide" frame handler (:4293, scope-pop! only on normal completion); no pop during raise/guard/shift unwinding
  • What: provide pushes onto a global per-name stack, popped only on normal frame completion. Any non-local exit through the body skips the pop — the value stays on the global stack forever, and context prefers scope-peek, so all later code sees the stale value.
  • Repro: (do (guard (e (true "caught")) (provide "kk" 42 (raise "boom"))) (context "kk"))42 (expected nil). (do (reset (provide "esc" 9 (shift k 77))) (context "esc"))9.
  • Coverage: test-unified-reactive.sx covers provide/context nesting for normal exits only.

[medium] [CONFIRMED] provide! outside any enclosing provide installs a permanent ambient global

  • Location: spec/evaluator.sx, "provide-set" frame handler (:4334-4346: pop-then-push); host scope-pop! on empty stack is a no-op (sx_primitives.ml:1998)
  • Repro: (do (provide! "pk" 7) nil) then, in a later top-level eval, (context "pk")7.
  • Coverage: provide! tests all run inside provide scopes; bare case uncovered.

[medium] [CONFIRMED×2] set! on unbound name silently creates a binding — contradicting both spec docs — and JIT vs interpreter write different global tables (split brain)

  • Location: spec/evaluator.sx step-sf-set! + hosts/ocaml/lib/sx_types.ml env_set_id (:378 root-create fallback) vs sx_vm.ml OP_GLOBAL_SET (:606 writes vm.globals); contradicted docs: spec/eval-rules.sx:112 ("Error if name is not bound"), spec/special-forms.sx:141 ("must already be bound")
  • What: (a) interpreted set! on unbound silently creates a root binding — typo'd set! hides bugs, and directly contradicts both spec documents (test-scope.sx:196 locks the create behavior, so impl-vs-doc conflict must be resolved one way or the other). (b) inside a JIT-compiled lambda the same set! writes the VM's separate vm.globals table — visible to VM code, invisible to interpreted code.
  • Repro: (set! never-defined-var 5) → 5 (readable after). Split brain: (do (define setter (fn () (set! q5 42))) (define reader (fn () q5)) (setter) (reader))"Undefined symbol: q5" (yet q5 reads as 42 inside setter).
  • Coverage: test-scope.sx:196 asserts creation only; visibility split uncovered.

[medium] [CONFIRMED] Quasiquote has no depth tracking — nested quasiquote evaluates inner unquotes early; ,,x errors

  • Location: spec/evaluator.sx, qq-expand (no level parameter)
  • Repro: (let ((x 7)) (quasiquote (a (quasiquote (b (unquote x))))))(a (quasiquote (b 7))) (Scheme: unquote preserved); `(a `(b ,,x))Undefined symbol: unquote.
  • Coverage: test-cek-advanced.sx:486 "nested unquote" is single-level despite its name.

[medium] [CONFIRMED] Quasiquote does not traverse dict literals — ,v inside {...} stays literal

  • Location: spec/evaluator.sx, qq-expand (non-list templates returned as-is)
  • Repro: (let ((v 3)) (quasiquote {:k (unquote v)})){:k (unquote v)}. Inconsistent with dict eval rule ("values are evaluated", eval-rules.sx:40).
  • Coverage: not covered.

[medium] [CONFIRMED] guard clause bodies: multi-expr → crash; multi-expr else → "Undefined symbol: else"

  • Location: spec/evaluator.sx, step-sf-guard — clauses spliced verbatim into a generated cond, inheriting the cond dual-mode defect
  • Repro: (guard (e (true 1 2)) (raise 9))Not callable: nil; (guard (e (else 1 2 3)) (raise 9))Undefined symbol: else. R7RS requires body sequencing. => receiver works.
  • Coverage: only single-expr clause bodies tested.

[medium] [CONFIRMED] defmacro/fn &key params silently misbind — keyword names ignored, off-by-one positional binding

  • Location: spec/evaluator.sx, macro/lambda param binding (&key pairing implemented only for components)
  • Repro: (defmacro mk2 (&key a b) ...): (mk2 :a 10 :b 20) → a=10, b=:b (the keyword itself); (mk2 :b 20 :a 10) → a=20 despite the :b label. Plain (fn (&key a b) ...) treats &key as a positional param name → "expects 3 args, got 4". Accepted without error, misbehaves.
  • Coverage: not covered.

[medium] [CONFIRMED] Splicing a non-list silently wraps it; malformed splice forms pass through literally

  • Location: spec/evaluator.sx, qq-expand
  • Repro: (quasiquote (a (splice-unquote 5)))(a 5) (Scheme: error); (splice-unquote xs ys) (arity 3) → stays literal; (unquote a b) silently drops b.
  • Coverage: not covered.

[medium] [CONFIRMED] do misparses a first form whose head is a list (IIFE) as a Scheme do-loop

  • Location: spec/evaluator.sx, step-eval-list "do" branch (~1843): dispatches to do-loop when (list? (first (first args)))
  • Repro: (do ((fn (x) x) 5) 99) → error "first: expected list, got 5"; expected 99.
  • Coverage: not covered.

[medium] [CONFIRMED] scope's :value parameter is parsed but unreadable — dead feature + dead frame type

  • Location: spec/evaluator.sx, step-sf-scope (:3318) / make-scope-acc-frame (:120); context/peek never consult scope-acc frames. Pre-CEK sf-scope (:1495) did scope-push!; the CEK rewrite dropped it. Frame type "scope" (make-scope-frame :111, handler :4279) is never pushed by any live path.
  • Repro: (scope "v" :value 10 (list (context "v") (peek "v")))(nil nil).
  • Coverage: scope/emit!/emitted have ZERO tests in spec/tests (doc example only, eval-rules.sx:200).

[medium] [CONFIRMED] Host-level errors are uncatchable by guard (only SX-level raise is)

  • Location: spec/evaluator.sx raise/handler machinery vs host primitive errors
  • What: errors from host primitives (rest: 1 list arg, Undefined symbol, arity errors) escape enclosing guard entirely; only guest (raise ...) unwinds to handlers. Guest code cannot write defensive wrappers around primitive misuse.
  • Repro: (guard (e (true "caught")) (undefined-symbol-xyz)) → propagates, guard never fires.
  • Coverage: test-errors.sx/test-conditions.sx exercise guest raise only.

[medium] [CONFIRMED] values/call-with-values bound only inside the test runner — Undefined symbol on every real runtime surface; let-values/define-values unusable

  • Location: spec/evaluator.sx values (2093), call-with-values (1392), sf-let-values (1403), sf-define-values (1437); hosts/ocaml/bin/run_tests.ml:1131 (bind "values" — test env only)
  • Repro: (call-with-values (fn () (values 1 2)) +) on CLI → Undefined symbol: call-with-values; same expr under run_tests → PASS. test-values.sx (22 tests) overstates the shipped runtime.
  • Coverage: green only in the runner environment.

[medium] [CONFIRMED] map/filter/map-indexed are O(n²)

  • Location: spec/evaluator.sx, "map"/"filter" continue handlers (~4364, ~4397): (append results (list value)) per element; map-indexed also recomputes (len new-results) each step
  • Repro: fresh sx_server: 10k → 0.58s, 20k → 2.56s, 40k → 13.6s (≈×4.7 per doubling); 100k map DNF in 120s while (reduce + 0 (in-range 100000)) takes 0.32s. Stack-safe — purely time.
  • Coverage: not covered (no perf tests)

[medium] [CONFIRMED] HO form names are not first-class — value position yields nil with a misleading type

  • Location: spec/evaluator.sx, symbol lookup (~1650) vs special-cased call dispatch
  • Repro: (define f2 map) (f2 (fn (x) x) (list 1 2))Not callable: nil; yet (type-of map)"function".
  • Coverage: not covered

[medium] [CONFIRMED] Cryptic uncatchable errors for bad HO data: dicts, both-args-callable

  • Location: spec/evaluator.sx, seq-to-list (else x) passthrough (~3573) + ho-setup-dispatch
  • Repro: (map (fn (kv) kv) {:a 1 :b 2})rest: 1 list arg; (map (fn (x) 1) (fn (y) 2)) → same. Expected: iterate dict entries or a clear "map: cannot iterate X".
  • Coverage: not covered

[medium] [CONFIRMED] Multi-collection map rejects strings/vectors that single-collection map accepts

  • Location: spec/evaluator.sx, ho-setup-dispatch "map" N-coll branch skips seq-to-list
  • Repro: (map + (vector 1 2) (vector 10 20))first: expected list, got #(1 2); single-collection vector/string map works.
  • Coverage: list multi-map covered (test-r7rs.sx:110124); strings/vectors not

[medium] [CONFIRMED] Threading a lambda literal returns a silently malformed lambda

  • Location: spec/evaluator.sx, thread-insert-arg — splices the value into the params position of (fn ...)
  • Repro: ((-> 5 (fn (y) (+ y 1))) 7)Undefined symbol: y. Should error at thread time.
  • Coverage: not covered

[medium] [CONFIRMED] Attribute names are never escaped/validated — spreading an untrusted-keyed dict injects attributes (XSS class)

  • Location: spec/render.sx, render-attrs (emits key raw) + merge-spread-attrs (copies spread-dict keys verbatim)
  • What: attribute values are escaped; attribute names are concatenated raw. Keys reach render-attrs via the spread operator, so spreading a dict built from user data yields event-handler injection.
  • Repro: (render-attrs {"x onload=alert(1) y" "1"}) x onload=alert(1) y="1". Values confirmed safe.
  • Coverage: not covered

[medium] [CONFIRMED] Five void elements unrenderable — in VOID_ELEMENTS but missing from HTML_TAGS

  • Location: spec/render.sx, VOID_ELEMENTS vs HTML_TAGS
  • Repro: area base embed param track fall through to function-call dispatch: (render-to-html '(base :href "x") ...)Undefined symbol: base.
  • Coverage: void suite tests br/hr/img/input/meta/link/source/col/wbr only

[medium] [CONFIRMED] aser serialises list-valued keyword args as bare unquoted lists → breaks on client re-evaluation

  • Location: web/adapter-sx.sx aser-call
  • Repro: (aser '(~tags :items (list "a" "b")) env)(~tags :items ("a" "b")); re-evaluating the wire form → Not callable: nil. Dicts round-trip fine; only lists break. Should emit (quote (...)) or (list ...).
  • Coverage: test-aser covers lists as children, not as kwarg values

[medium] [CONFIRMED-html / SUSPECTED-dom — independently double-confirmed] render-to-dom disagrees with render-to-html on non-boolean attrs valued true/false (hydration mismatch)

  • Location: web/adapter-dom.sx (attr cond ~357) vs spec/render.sx render-attrs
  • What: for attrs NOT in BOOLEAN_ATTRS, HTML mode stringifies (data-flag="true", data-off="false"), DOM mode omits false and emits true as an empty attr. SSR HTML and hydrated DOM differ. HTML side executed; DOM side code-read (dom adapter not loadable in harness). Cross-check: hosts lane C19 found the same defect independently (same conclusion, same confidence split) — treat as confirmed pending a browser-side execution.
  • Repro: (render-to-html '(div :data-flag true :data-off false) ...)<div data-off="false" data-flag="true">.
  • Coverage: not covered

[medium] [CONFIRMED] String primitives are byte-based; substring can produce invalid UTF-8

  • Location: string-length, substring, upcase/downcase
  • Repro: (string-length "é") → 2, "👍" → 4; (substring "é" 0 1)"<22>"; (upcase "héllo")"HéLLO". Constructors are codepoint-aware (char-from-code 233"é") while measurement is byte-based. Project rule "use UTF-8 chars" makes this a live hazard.
  • Coverage: no codepoint-semantics tests.

[medium] [CONFIRMED] Spec declares primitives that don't exist; runtime has primitives the spec omits

  • Location: spec/primitives.sx
  • What: eq? (L285), eqv? (L292) declared, undefined in both harness and sx_server; into (L722) declared — IO-bridge-only in server; json-encode declared plain but IO-bridge-only; sort exists in runtime but NOT in spec; header (L27-35) claims ~40 functions "moved to stdlib.sx" but stdlib.sx contains only format.
  • Repro: (eq? 1 1)Undefined symbol: eq?; (sort (list 3 1 2))(1 2 3).
  • Coverage: drift untested.

[medium] [CONFIRMED] Division-by-zero inconsistency: / returns inf silently, mod/quotient leak raw OCaml exception

  • Repro: (/ 1 0)inf; (mod 7 0)/(quotient 7 0) → unstructured host Division_by_zero.
  • Coverage: not covered.

[medium] [CONFIRMED] / doc contradicts behavior: ":returns float" but exact results snap to int

  • Repro: (integer? (/ 6 3)) → true. (/ 1 3) → float, never rational despite make-rational.
  • Coverage: behavior covered green — the doc is wrong.

[medium] [CONFIRMED] sort takes no comparator

  • Repro: (sort (list 3 1 2) (fn (a b) (> a b)))Unhandled exception: "sort: 1 list". Natural ascending on numbers/strings only.
  • Coverage: not covered.

[medium] [CONFIRMED] Strict type errors are uncatchable by guard (host/spec error-channel divergence)

  • Location: sx_ref.ml strict_check_args (:516, raises Eval_error outside the CEK raise-eval machinery); the spec expresses it as (error ...) which would use the ordinary condition channel
  • What: (guard (e (true ...)) (typed-call bad-arg)) does not catch — the type error escapes to top level, while user (error "boom") IS caught by the same guard. Programs cannot recover from type errors. Same channel problem as the general host-errors-uncatchable finding, but here spec and host disagree about which channel it should be.
  • Repro: (guard (e (true (str "CAUGHT: " e))) (s1 "bad")) → protocol-level type error; (guard (e (true ...)) (error "boom")) → caught.
  • Coverage: test-strict.sx asserts at the runner level; the guard channel untested.

[medium] [CONFIRMED] Strict mode: unknown type names silently match everything

  • Location: spec/evaluator.sx value-matches-type?_ fallback returns true for any unknown non-"?"-suffixed string; set-prim-param-types! does no validation
  • Repro: gg typed (x "integer"): (gg "abc")"abc" (typo silently disables checking); "frobnicate?" matches all values.
  • Coverage: not covered.

[medium] [CONFIRMED] Strict mode: "keyword" type is dead; components are untypeable

  • Location: value-matches-type? vs eval-rules.sx keyword rule (keywords evaluate to strings)
  • What: (a) evaluated keyword args arrive as strings, so a "keyword"-typed param always fails on (f :foo) and passes plain strings via "string"; (b) type-of a component is "component", which fails "lambda", and "component" isn't a match branch — falls to the catch-all and accepts everything. No way to require a component.
  • Repro: (pk :foo) → "expected keyword … got string (foo)"; c7 typed "component": (c7 42) passes.
  • Coverage: no keyword/lambda/component type tests.

[medium] [CONFIRMED] Strict mode: component &key calls misalign with positional type specs

  • Location: strict-check-args positional indexing vs component keyword calling convention
  • Repro: ~tc typed (a number): (~tc :n 5) → "expected number for param a, got string (n)" — the keyword marker itself is checked as arg 0. Typing components via this machinery is impossible.
  • Coverage: not covered.

[medium] [CONFIRMED] Signals: reset!/computed change-detection is dead for numbers and strings

  • Location: spec/signals.sx reset!, swap!, computed(when (not (identical? old value)) ...); identical? is physical equality: (identical? 5 5) → false
  • What: setting a signal to its current value still notifies; computeds recomputing to an equal number/string still cascade — spurious re-runs throughout the reactive graph.
  • Repro: effect on (signal 5) (runs=1); (reset! a7 5) → runs=2. Expected 1.
  • Coverage: not covered.

[medium] [CONFIRMED] Signals: diamond dependency glitch — no glitch-freedom

  • Location: spec/signals.sx — notify/flush propagate depth-first synchronously; batch dedups only direct subscribers of directly-mutated signals and decrements depth before cascades
  • What: a → b,c → d: one change to a recomputes d twice; the first recompute observes new-b with stale-c (inconsistent intermediate state).
  • Repro: initial d runs=1; (reset! a 2) → d runs=3, final value correct.
  • Coverage: not covered.

[medium] [CONFIRMED] Datum comment #; cannot precede ) or end input — all three parsers

  • Location: spec/parser.sx read-expr #; branch (discard-then-read-next); sx_parser.ml:167-171 same structure
  • Repro: (sx-parse "(a #;b)")Unexpected character: ); (sx-parse "1 2 #;3")Unexpected end of input. Standard Lisp: (a #;b) = (a).
  • Coverage: three datum-comment tests, all mid-list.

[medium] [CONFIRMED] Char values never compare equal — = lacks a Char case

  • Location: hosts/ocaml/lib/sx_primitives.ml safe_eq (749-804): no Char,Char arm → falls to _ -> false
  • Repro: (= (make-char 32) (make-char 32))false. parse(serialize(char)) ≠ char for every char; char-keyed memoization silently fails.
  • Coverage: test-chars.sx compares via char->integer/predicates; no =-on-chars test.

[medium] [CONFIRMED] #\a char literals crash the guest parser on the mcp-tree host (Int/Float primitive drift)

  • Location: mcp_tree.ml:378 (char-code returns float) vs sx_primitives.ml:2811 (make-char requires Integer)
  • Repro: mcp harness (sx-parse "#\a")make-char: expected integer codepoint; sx_server OK. Same shadowing family as parse-number.
  • Coverage: no char-literal-via-sx-parse tests.

[medium] [CONFIRMED] Multibyte character literals broken everywhere; serialized chars ≥128 don't reparse; unknown char names silently truncate

  • Location: sx_parser.ml:153-159 (byte-level Char.code); spec/parser.sx read-char-literal (byte-level); serializer emits #\ + raw char
  • Repro: native '#\éParse_error "Unexpected char: \169"; (sx-serialize (make-char 233))"#\é" which no parser reads back; #\spade#\s silently (both implementations).
  • Coverage: no non-ASCII char literals tested.

[medium] [CONFIRMED] \uXXXX escape: invalid input crashes raw (OCaml) or silently corrupts (JS); no astral/surrogate-pair support

  • Location: spec/parser.sx read-string \u branch (no hex validation, no bounds check, -1 from failed digit lookup); sx_parser.ml:70-77
  • What: valid BMP works on all three parsers (the "never use \uXXXX" project rule is style, not brokenness). Invalid hex: guest → raw Invalid_argument (negative codepoint); native → uncaught Failure("int_of_string"); JS → silently yields garbage string. Surrogates: OCaml raises, JS produces lone surrogate. Truncated "\u41" → guest reads past the closing quote (Expected string, got nil). Astral unrepresentable.
  • Coverage: zero \u tests in any suite.

[medium] [CONFIRMED] Unknown string escapes diverge: native keeps the backslash, guest/JS drop it

  • Location: sx_parser.ml:79 (_ -> add '\\'; add esc) vs spec/parser.sx read-string :else esc
  • Repro: "a\qb" is 4 chars through the native reader, 3 chars through guest/JS — same source file, different data depending on which parser read it. \b/\f unsupported both (silent literal); native additionally accepts undocumented \/ and \`.
  • Coverage: only \n \t " tested.

[medium] [CONFIRMED] #name extensible reader-macro dispatch is unimplemented on OCaml hosts

  • Location: spec/parser.sx:459 (reader-macro-get); registry exists only in hosts/javascript/platform.py:2639-2640
  • Repro: mcp harness (sx-parse "#t")Undefined symbol: reader-macro-get (instead of the intended "Unknown reader macro" error). The sole production evaluator cannot register reader macros at all.
  • Coverage: reader-macro suite tests only #; #| #'.

[low-medium] [CONFIRMED] case: :else/else matches in ANY position, shadowing later valid clauses

  • Location: spec/evaluator.sx, step-sf-case / is-else-clause?
  • Repro: (case 1 :else "e" 1 "one") → "e".
  • Coverage: not covered.

[low-medium] [CONFIRMED] case: evaluated datums, keyword/string punning, Scheme clause syntax crashes misleadingly

  • Location: spec/evaluator.sx, step-sf-case; documented flat in eval-rules.sx:70 (but rule text doesn't say vals are evaluated)
  • What (verified): vals evaluated sequentially until match (side effects only for pre-match vals), scrutinee once, comparison structural = (lists match), duplicates first-wins, no-match+no-else → nil; keywords evaluate to strings so (case "k" :k "kw") matches. Scheme datum-list clauses crash: (case "a" (("a") 1) (else 2))Not callable: ("a"). Flat form is intended (test-cek.sx:130-138); the unstated eval semantics + hostile diagnostic are the issues.
  • Coverage: happy paths only.

[low] [CONFIRMED×2] letrec is parallel (not letrec*) and reference-before-init silently yields nil

  • Location: spec/evaluator.sx, sf-letrec (~1366-1469: all inits evaluated before any name bound; names pre-bound nil)
  • Repro: (letrec ((a b) (b 1)) a)nil (R7RS: error); (letrec ((a 1) (b (+ a 1))) b)1 (nil-coerced by +; letrec* would give 2). Masks initialization-order bugs.
  • Coverage: only well-formed lambda recursion tested.

[low] [CONFIRMED] Documentation contradicts implementation: let IS sequential and multi-expression bodies ARE implicit begin

  • Location: spec/evaluator.sx step-sf-let (:3133 — let and let* dispatch identically, shared local frame) vs CLAUDE.md "SX Island Authoring Rules" (claims parallel let, last-expr-only bodies, "reactive text needs deref computed", "effects go in inner let")
  • What: (let ((a 1) (b a)) b) → 1; (let ((x 5) (x (* x 2))) x) → 10; let/when/fn multi-expr bodies evaluate every form (side effects verified). Sequential let is explicitly tested intent (test-scope.sx:45). The CLAUDE.md gotchas describe a different evaluator (likely the OCaml SSR island path) — doc drift that misleads every SX author. Also (let ((f (fn () a2)) (a2 5)) (f)) → 5: binding-init lambdas capture the let frame itself (letrec-like — beyond even letrec* semantics; worth documenting).
  • Coverage: sequential let tested; the doc is what's wrong.

[low] [CONFIRMED] Component &key argument false is coerced to nil

  • Location: spec/evaluator.sx, component branch: (env-bind! local p (or (dict-get kwargs p) nil))
  • Repro: (do (defcomp ~t1 (&key flag) (if (nil? flag) "NIL" "VAL")) (~t1 :flag false))"NIL". Components can't distinguish :flag false from omitted.
  • Coverage: invisible to test-defcomp.sx (only used in conditionals).

[low] [CONFIRMED] Trailing keyword argument without a value silently accepted

  • Location: spec/evaluator.sx, parse-keyword-args (:935)
  • Repro: (do (defcomp ~c4 (&key a) (list a)) (~c4 :a))(nil); expected kwarg error.
  • Coverage: not covered.

[low] [CONFIRMED] defmacro is unhygienic (classic capture) while the test suite is named "macro-hygiene"

  • Repro: (defmacro my-or2 (a b) (let ((t ,a)) (if t t ,b))); (let ((t 5)) (my-or2 false t))false`. CL-style defmacro — judged intended (gensym available, unique, tested); but test-macros.sx "macro-hygiene" suite (line 208) tests only the leak-OUT direction, overstating the guarantee.

[low] [CONFIRMED] match has no guard clauses — Racket-style (pattern (when cond)) silently read as a structural pattern

  • Repro: (match 9 ((x (when (> x 5))) "big") (_ "small")) → "small" (silent structural fail → fall through). Supported features work; non-match raises properly. let-match is dict-destructuring only; list patterns give a confusing "no clause matched".
  • Coverage: supported features covered; guard-clause rejection not.

[low] [CONFIRMED] Components not recognized by ho-fn?; map-with-component yields silent zeros

  • Location: spec/evaluator.sx, ho-fn? (3554) — no component check
  • Repro: (defcomp ~c2 (x) (* x 2)); (map ~c2 (list 1 2 3))(0 0 0); (map (list 1 2 3) ~c2)rest: 1 list arg.
  • Coverage: not covered

[low] [CONFIRMED] |> alias is dead code — parser rejects |

  • Location: spec/evaluator.sx step-eval-list ("|>" ...) (1906); tokenizer
  • Repro: (|> (list 1 2 3) ...)Parse_error("Unexpected char: |"). Branch unreachable.

[low] [CONFIRMED] Keywords-as-getters unsupported in HO fn position and -> chains, with misleading errors

  • Repro: (map :name (list {:name 1}))Not callable: "name"; (-> {:a {:b 42}} :a :b)Not callable: nil.
  • Coverage: not covered

[low] [CONFIRMED] Zero/one-arg HO calls return empty results silently

  • Repro: (map)(); (map (fn (x) x))(); (reduce +)nil; (-> (list 1 2 3) map)() (plausible typo silently discards data).
  • Coverage: not covered

[low] [CONFIRMED] Boolean-attr truthiness footguns: string "false" and 0 emit the bare attribute

  • Location: spec/render.sx, render-attrs (SX truthiness)
  • Repro: (input :disabled "false")<input disabled />; (input :disabled 0) same. Aligns with SX truthiness but surprising when values come from data.
  • Coverage: true/false booleans tested; string/number values not

[low] [CONFIRMED] is-render-expr? exported but dead; html: tags and hyphenated custom elements error despite being "recognised"

  • Location: spec/render.sx, is-render-expr? — zero callers
  • Repro: (render-to-html '(html:my-tag :foo "bar") ...)Undefined symbol: html:my-tag; (aser '(custom-widget :foo "bar" "child") ...)Undefined symbol: custom-widget.
  • Coverage: not covered

[low] [CONFIRMED] <script>/<style> content is HTML-escaped like text — corrupts legitimate inline JS/CSS

  • Location: web/adapter-html.sx render-html-element
  • Repro: (script "if (a < b && c) { x=\"y\"; }") → entities inside script (broken JS); (style ".a > .b {}").a &gt; .b {}. Blocks </script> breakout (good) but breaks real inline code; raw! is the workaround.
  • Coverage: only script attrs tested, never content

[low] [CONFIRMED] Comparison/equality strictly binary; = is deep structural equality conflating exactness

  • Repro: (< 1 2 3)/(= 1) → unstructured arity error (matches spec, deviates from Scheme); (= {:a 1} {:a 1}) → true; (= 1 1.0) → true (dedup-key hazard).

[low] [CONFIRMED] Rounding half-away-from-zero, not banker's; inexact->exact rounds; (sqrt -1) → nan

  • Repro: (round 2.5) → 3 (R7RS: 2); (inexact->exact 1.5) → 2 (locked by test-numeric-tower.sx:115 — intended but R7RS-divergent); (sqrt -1) → nan silently.

[low] [CONFIRMED] Float/nil rendering inconsistencies across str/format/render

  • Repro: (str 1.0)"1" (float/int distinction lost — also (div 1.0) renders 1); (str nil)"" but (format "~a" nil)"()"; (format "~d" 3.7)"3" (silent truncation).

[low] [CONFIRMED] Inconsistent nil/empty tolerance across list ops

  • Repro: (first nil) → nil, (rest nil)(), (nth (list 1 2) 5) → nil silently — but (last nil), (reverse nil), (nth nil 0) all raise.

[low] [CONFIRMED] keys returns strings in reverse insertion order

  • Repro: (keys {:a 1 :b 2 :c 3})("c" "b" "a"). Determinism footgun for serialization/content-addressing.

[low] [CONFIRMED] keyword-name unusable on evaluated keywords

  • Repro: (keyword-name :kw) → error (:kw self-evaluates to "kw"); only (keyword-name ':kw) works.

[low] [CONFIRMED] string->number: no rational/whitespace parsing

  • Repro: "1/2" → nil (despite make-rational), " 5 " → nil, "1e3" → 1000, garbage → nil (good).

[medium] [CONFIRMED — CORRECTED after cross-lane check] apply does not spread AT ALL on the native production surface

  • Location: continue-with-call native-call path / apply primitive
  • What: originally reported as "leading-args form missing, two-arg form works" — WRONG. Re-verified on fresh sx_server: (apply + (list 1 2))Unhandled exception: "Expected number, got list: ". The list is passed as a single argument, never spread — (apply str (list 1 2 3))"(1 2 3)" (str of the list itself). The earlier "works" observation came from a test-runner/harness environment with its own apply. Conformance lane F-3 independently found this AND that the WASM kernel spreads the 2-arg form (→ 6) while native errors — the same kernel family disagrees with itself on apply.
  • Repro: (apply + (list 1 2)) → error; (apply + (list 1 2 3)) → error; (apply str (list 1 2 3))"(1 2 3)" (fresh sx_server, verified 2026-07-03).
  • Coverage: not covered on the production surface (runner env has a different apply — see the values/call-with-values finding for the same pattern).

[low] [CONFIRMED] Strict checks are name-keyed at the call site — trivially evaded, and shadowers inherit checks

  • Repro: (let ((zz hh)) (zz "a")) → unchecked; computed heads ((mk) "bad") → unchecked; conversely a user fn shadowing a typed name gets the declared checks applied to it. First-class function flow is entirely unchecked.
  • Coverage: not covered.

[low] [CONFIRMED] set-prim-param-types! replaces wholesale; no validation; malformed specs fail cryptically and uncatchably

  • What: second call wipes all earlier declarations (no merge); nonexistent prim names accepted silently; {"positional" "oops"} errors at call time with "Expected list, got string" (uncatchable, doesn't name the spec as culprit); {"name" "not-a-dict"} silently checks nothing; declaring types for HO-form names never fires (HO dispatch intercepts before the arg frame).
  • Coverage: only the nil-reset path tested.

[low] [CONFIRMED] Too-few args never error and their declared types are silently skipped

  • What: user lambdas nil-fill missing params ((f2 1)(1 nil) with b typed number, no error); strict-check-args guards idx < len(args) so unsupplied params skip checking. Too-many args DO error. foreign-check-args has the mirror asymmetry (extra args unchecked; code-level).
  • Coverage: not covered.

[low] [CONFIRMED] (:as type) parameter annotations are never enforced — even in strict mode

  • Location: eval-rules.sx documents (:as type) in the lambda rule; spec/signals.sx uses them pervasively ((s :as signal))
  • Repro: (define tf (fn ((x :as number)) x)) (tf "not-a-number") → returns the string, strict on or off. The natural per-param channel is decorative; strict mode reads only the global name-keyed dict.
  • Coverage: not covered.

[low] [CONFIRMED] Strict-machinery paper cuts

  • Return types unsupported anywhere (params only). Rest-arg errors index from 0 within the rest section ("rest arg 0" is overall arg 2). set-strict! is one global OCaml ref — not per-env, not captured by continuations; toggling mid-program retroactively affects existing lambdas. Dead shadowed duplicates _strict_ref/_prim_param_types_ref at sx_ref.ml:18-19 (transpiler cruft, no desync). Host surface inconsistency: sx_server binds set-strict!/set-prim-param-types! but not value-matches-type?; the harness binds none. Positive: error message quality is good (names function, param, expected, actual, value).

[low] [CONFIRMED] batch unusable on the server host; coroutines module inert outside the test runner

  • What: batch calls (batch-begin!) on non-client hosts; batch-begin!/batch-end! are bound only in run_tests.ml:564 — on sx_server (batch ...)Undefined symbol: batch-begin! (which, per the wedge finding, also leaves *batch-depth* stuck). Separately, spec/coroutines.sx lacks the trailing (import (sx coroutines)) re-export that signals.sx/harness.sx have — loading it binds nothing globally; tests work only via explicit import + run_tests-only cek-* hooks.
  • Coverage: not covered.

[low] [CONFIRMED] effect stale cleanup double-invocation

  • Location: spec/signals.sx effect/run-effect — cleanup-fn invoked at each re-run start but never cleared; only overwritten when a run returns a new callable
  • Repro: effect returns cleanup only when v=0: after two resets, cleanup-calls = 2. Expected 1.
  • Coverage: not covered.

[low] [CONFIRMED] Guest parse errors carry no source locations; native has line/col on only 2 of ~8 error types

  • Location: spec/parser.sx (all error sites location-free); sx_parser.ml (locations only for "Unexpected end of input"/"Unexpected char"; unterminated string/list/dict etc. location-free)
  • Repro: (sx-parse "(a (b)") → just "Unterminated list". Also test-source-locations.sx tests a parser-combinator library, NOT spec/parser.sx, and its cols are 0-based vs native 1-based.
  • Coverage: no reader-location tests exist.

[low] [CONFIRMED] Dict literal edges: odd form count → misleading error; duplicate keys silently last-win

  • Repro: {:a}Unexpected character: } (no mention of pairing); {:a 1 :a 3}{:a 3} silently (both parsers).
  • Coverage: not covered.

[low] [CONFIRMED] #|...| is a raw string to the first |, not a block comment; #|a|# leaves a dangling #

  • Repro: (sx-parse "#|hello world|")("hello world"). Documented, but a Scheme-expectation trap with no test for the |# suffix case.

[low] [CONFIRMED] Keyword edge tokens: : parses as keyword with empty name; ::a is a keyword named ":a"

  • Coverage: numeric-suffix/consecutive keywords tested; :/:: not.

[low] [CONFIRMED] Harness contract nits

  • A throwing mock leaves no IO-log entry (append happens after the mock returns) — failed calls invisible to assert-io-called. (assert cond) one-arg form works only via the evaluator-wide nil-fill of missing params.

[low] [CONFIRMED] CLAUDE.md points at a deleted canonical spec (shared/sx/ref/*.sx)

  • What: CLAUDE.md instructs reading shared/sx/ref/eval.sx/parser.sx/primitives.sx/render.sx as "authoritative SX semantics"; the directory contains only BOUNDARY.md + Python cache. Live spec is spec/*.sx. Together with the island-authoring-rules drift (let/body semantics above), the project docs actively mislead on core semantics.

SUSPECTED findings (reasoning only, not reproduced)

[medium] [SUSPECTED] More nested-eval boundaries: expand-macro, sf-let-values, sf-define-values, qq-expand unquotes all evaluate via (trampoline (eval-expr ...)) instead of CEK frames

  • Location: spec/evaluator.sx, expand-macro (1548-1580), sf-let-values (1411-1417), sf-define-values (1443-1445), qq-expand unquote eval
  • Reasoning: same structural pattern as the three CONFIRMED nested-run bugs (shift-k invoke, threading, signal) — continuation capture, perform/IO suspension, or raise-to-outer-handler inside a macro body, let-values initializer, or unquote crosses a nested trampoline the outer kont cannot see. let-values untestable at runtime (values missing — see medium finding); macro-expansion capture is expansion-time and rare.
  • Coverage: not covered.

[low] [SUSPECTED] env_merge is_descendant depth cap (>100) silently flips scoping semantics

  • Location: hosts/ocaml/lib/sx_types.ml:394 (if depth > 100 then false)
  • Reasoning: call-site env chains deeper than 100 frames false-negative the descendant check, activating the caller-frame-copy branch (the dynamic-scoping leak above) in code that was previously purely lexical. Rare (needs ~100 nested closure/let layers), silent flip. Code-read only.
  • Coverage: not covered.

[medium] [SUSPECTED] Canonical serialization is not cross-host deterministic — CIDs can differ between OCaml and JS

  • Location: spec/canonical.sx (canonical-number uses host str; string case uses host escape-string)
  • Reasoning + partial confirmation: OCaml (canonical-serialize 1e-7)"1e-07" (verified live) while JS String(1e-7)"1e-7" (code-read) — different canonical text → different sha3 CIDs for the same value. Also: sx_server escapes \r (sx_server.ml:1275), JS platform does not (platform.py:2628); integers beyond 2^53 exact on OCaml, unrepresentable in JS. Full cross-host CID comparison not run.
  • Coverage: test-canonical.sx never canonicalises exponent-form floats, CR strings, or big ints. (Dict-key sorting IS implemented and idempotence holds for tested classes.)

[medium] [SUSPECTED] Coroutine performing a non-yield effect is permanently wedged

  • Location: spec/coroutines.sx, coroutine-handle-result — for a suspension with op ≠ "coroutine-yield" it does (perform request): forwards outward but discards both the answer and the coroutine's suspension; state stays "running" and coroutine-resume has no "running" branch → "unexpected state: running"
  • Reasoning: code-level; not reproducible outside run_tests (needs cek-step-loop/cek-resume hooks bound only in run_tests.ml:951-955). Correct forwarding would cek-resume the suspension with the outer answer in a loop.
  • Coverage: test-coroutines.sx (27 tests) has zero perform usage.

[low] [SUSPECTED] VM/JIT execution path has no strict checking

  • Location: sx_vm.ml — zero callers of strict_check_args (repo-wide grep: only sx_ref.ml)
  • Reasoning: any call executed as compiled bytecode bypasses checks. Could not confirm live — lazy JIT never engaged in CLI probes (bytecode-inspect after 300 calls: "no compiled bytecode").
  • Coverage: not covered.

Checked, NOT reproducible (negative results correcting project memory)

  • "Short helper names (name/dyad) hang the runtime": does NOT reproduce — (define name …)/(define dyad …) work. The guard case is the unshadowable-name finding (error, not hang).
  • "split is char-class not substring": harness/guest-worktree only. Real sx_server (split "a--b" "--")("a" "b") substring, keeps empties. Multi-char delimiter untested in spec/tests — worth a pinning test.
  • "let is parallel / bodies evaluate only last expr / effects need inner let" (CLAUDE.md island rules): all false for the spec evaluator — let is sequential, bodies are implicit begin (tested intent). Likely describes the separate OCaml SSR island path → doc fix + cross-lane check.

Clean areas verified

CEK core: TCO through all special forms (named-let 200k, mutual 100k, non-tail 100k heap-safe); call/cc escape/multi-shot/independence; shift/reset delimiting + multi-shot composable k; shift without reset → clean error; escape from HO callbacks; multi-shot resume INTO map frames (no accumulator leakage); raise through dynamic-wind one-shot (after exactly once, 50k-frame unwind); (and)/(or)/(begin)/(cond)/if-no-else edge values; cond =>; head-position exprs; parameterize; restart-case/invoke-restart.

Env/scope: closure sharing + isolation both directions; define local-in-lambda vs top-level redefine; set! write-through 1-2 levels; (let ((x x)) x) → outer; letrec mutual recursion (lambda case); emit!/emitted ordering/extent/nesting/TCO-survival/no-leak (correct but ZERO test coverage — gap worth closing given the scope/provide bugs); provide/context/peek normal-flow nesting (well covered); component &rest/kwarg interleaving; component set! does not write back to caller; primitive shadowing works for genuine primitives.

HO forms: HoSetupFrame stages both args exactly once, left-to-right, both orders; map over list-of-functions picks sane reading; guest raise mid-map caught cleanly; some/every?/filter/ for-each/map-indexed semantics sane (0/"" truthy — internally consistent); no double-eval in threading (quoted-value splice protects data both paths); as->; ->> normalizes via swap; nested map-in-map; reduce 100k in 0.3s; multi-map zips to shortest (covered).

Special forms: when/begin/do sequencing; and/or/if falsiness fully consistent (only false/nil falsy); short-circuit verified; defmacro recursive expansion, &rest + ,@ templates, ~name heads in qq; guard happy paths incl. R7RS auto-reraise; ->/set! interplay; eval-rules.sx accurate except set!-error claim, cond clause mode, case evaluated-vals; unless intentionally userland.

Render: text + attr-value escaping correct; raw!/SxExpr single-escape guarantee (no double- escape); registered void elements self-close, drop children silently; boolean-attr registry (23) correct for true/false/nil; numbers/booleans/nil as children; aser wire semantics (components unexpanded, control flow evaluated, string/dict args round-trip incl. quotes/unicode); recursive- with-base-case components; fragment/nil/string/number component returns; &rest spliced flat.

Primitives: quotient/remainder/modulo signs R7RS-correct; substring clamping; replace; trim/index-of/starts-with?/ends-with?; assoc/dissoc/merge/has-key?; range/flatten/chunk-every; rationals (normalization, contagion, zero-denominator errors); vectors/sets/ports/chars/string- buffers basics; dict-set! vs assoc; truthiness consistent; format directives; max/min zero-arg errors clean. Not probed (dedicated green suites): zip-pairs, bitwise, bytevectors, regexp.

Parser/serializer: basic escapes correct + exact round-trips (quotes/backslashes/newlines/ multibyte strings); quote sugar nesting incl. before )/EOF; 10k-deep nesting + 10k-char tokens parse fine (heap frames, no hangs on any adversarial input — every failure errors rather than loops); serializer round-trips for number/keyword/symbol/list/nested-dict(ident keys)/bool/nil; nil vs () vs {} distinct; canonical-dict key sorting + idempotence (tested classes); -0.0 → "0"; negative numbers vs - symbol; 5./1e10/-1.5e-3; comments at EOF; dotted pairs cleanly rejected on all hosts; keyword AST round-trip.

Strict typing: value-matches-type? core semantics correct (number/string/boolean/nil/list/ dict; empty list not a dict; nullability exclusively via "type?" suffix — consistent; floats+ints both "number"; quoted symbols; lambdas). ->/->> threading IS strict-checked (re-dispatches a real call form). Recovery after a strict error works. Error messages high quality.

Signals: effect does not re-run on unrelated signals; effect's dispose-fn unsubscribes correctly; batch dedups multiple resets of one signal (when it works — see wedge finding).

Harness (spec/harness.sx): interceptors log args/result/op correctly; arity fan-out 0-3 + apply; custom-platform merge over defaults; assertion messages descriptive.


Handoffs to other lanes

  • HOSTS: hosts/ocaml/bin/mcp_tree.ml maintains its own primitive table, drifted from sx_primitives.ml (empty?/get/split/contains?/equal?/keyword-name differ — details in the harness-divergence finding). Also: sx_harness_eval is a shared persistent image, not a fresh sandbox; sx_read_subtree ignores path; sx_read_tree ignores max_lines.
  • HOSTS/CONFORMANCE — JIT vs interpreter divergence: three confirmed behavior flips between VM-compiled and interpreted paths: (1) set!-unbound writes vm.globals vs root env (split brain); (2) env_merge caller-frame leak exists only interpreted ("VM undefined" under JIT); (3) named-let leaked loop name reads as lambda interpreted / nil under JIT. Parity suite has no coverage.
  • HOSTS (Python shell): aser output embedded into <script> via json.dumps in shared/sx/helpers.py sx_streaming_resolve_scriptjson.dumps doesn't escape / or <; check whether serialized SX can ever contain </script> (aser HTML-escapes text children, but attr/raw paths unverified).
  • CONFORMANCE: run_tests.ml injects bindings absent from the real runtime — values/ call-with-values (test-values.sx), contains-char?/trim-right (canonical.sx), batch-begin!/batch-end! (signals), cek-step-loop/cek-resume (coroutines). Whole suites are green only in-runner; test-env vs runtime-env parity needs a systematic sweep.
  • CONFORMANCE — parser fleet: three parser implementations (native OCaml reader, spec guest parser over per-host primitive bindings, JS transpiled spec) with four ident/number classifier tables that were never reconciled (details in the AST-divergence finding). Guest-parser platform primitives (parse-number, char-code, contains-char?, trim-right, reader-macro-get, escape-string) drift per host because each host re-binds them ad hoc. Suites only exercise the intersection — that's why everything stays 1080/1080 green.
  • HOSTS (JS): JS parser silently corrupts invalid \uXXXX escapes (garbage string, no error) where OCaml raises; JS reader-macro-get registry exists but OCaml's doesn't.
  • DOCS: CLAUDE.md island-authoring rules describe non-spec semantics (parallel let, last-expr bodies); CLAUDE.md canonical-reference section points at deleted files.
  • TOOLING incident log: mid-review another session polluted the shared MCP image (inc redefined to a constant, breaking guest parsing with spurious "Unterminated" errors); the parser agent restored it. Underlines the harness-not-fresh finding — harness state is shared across concurrent sessions.