# 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-continuable` → `42`. 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` (72–89) 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-number` → `parse-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:110–124); 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)` → `"�"`; `(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_script` — `json.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.
