Files
rose-ash/plans/sx-review/hosts.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

77 KiB
Raw Blame History

SX Hosts Review — findings (hosts axis)

Reviewer lane: per-target host implementations + FFI seam. Targets: hosts/ocaml (kernel, VM/JIT, epoch protocol, server env, HTTP path), hosts/javascript, hosts/python, hosts/native, WASM browser kernel, web/ adapters.

Status: COMPLETE — all 7 verification agents done (OCaml VM/JIT, sx_server epoch/env/HTTP, JS host, Python host, web-FFI seam, cross-host parity, native/WASM). OCaml run_tests baseline + --jit differential both done. The highest-severity findings were reproduced live and self-verified by the lead reviewer (marked "self-verified").

Live HTTP-path verification (follow-up): booted sx_server --http on free ports (3 bounded runs, own PIDs only — never pkill) and drove concurrent load. This UPGRADED three suspected HTTP-race items to CONFIRMED — S1 (multi-Domain render pool crashed intermittently under concurrent load; empty responses, no exception, OOM ruled out at ~292 MB peak RSS), S4 (routing failures served as HTTP 200 and cached: cold 2.02s → warm 0.0005s), and S5 (cache key ignores cookies/query — three different session= cookies returned one identical cached body). S2/S3 stay SUSPECTED: their code paths were verified live but the stock static-docs app has no request-varying full-page render to exhibit a visible symptom. Logs: /tmp/sx-review/http-server{,2,3}.log.

Finding tally: ~65 CONFIRMED (incl. S1/S4/S5 upgraded via live repro) + ~13 SUSPECTED (incl. S2/S3) + 2 positive/informational. Full detail below.

TOP-LINE (most severe; CONFIRMED unless noted):

  • Production serving-JIT silently miscompiles (J1): (-> …) in argument position and user-macro args (J3) yield wrong values; JIT-fallback double-applies side effects (J2). The http server registers the JIT hook UNCONDITIONALLY (sx_server.ml:4163) — live on sx.rose-ash.com.
  • One malformed line kills the whole sx_server process (C1) — unguarded command-channel parse; a non-ASCII byte does the same (C1b).
  • JS bundle is hollow (C0a): render/engine/router/signals/all adapters transpile to 0 bytes because the transpiler doesn't recurse into define-library; JS conformance is 2490 failing (C0b).
  • Python↔OCaml boundary + parser diverge: NUL/escape corruption (C25), boundary validation is a permanent no-op (C24), bridge desync is unrecoverable (S-bridge).
  • Cross-host primitive parity is broken in ~30 confirmed ways (P1P12): OCaml lossy float wire, mixed-number sort, into needs a bridge; JS lenient-coercion cluster + missing equal?/eq?.
  • HTTP serving path breaks under concurrency (live-verified on a booted server): the multi-Domain render pool crashed intermittently under concurrent load — a genuine data race on unsynchronized cross-Domain Hashtbls, OOM ruled out (S1, CONFIRMED); routing failures are served as HTTP 200 and cached indefinitely (S4, CONFIRMED); the response cache key ignores cookies/query, so it would serve one user's response to another on any cookie/auth-varying app (S5, CONFIRMED). Still SUSPECTED: per-request globals read by queued workers (S2) and expand-components? removed on the shared env by AJAX renders (S3) — both code paths verified live, but need a request-varying app (not the static docs site) to exhibit a visible symptom.

Baseline (no JIT): run_tests.exe5762 passed, 274 failed. 273 failures are hs-* hyperscript suites (in-progress guest project — baseline caveat, not a host bug; note the can-map-an-array / "map with block" line prints with a blank suite label due to C9 but belongs to a hyperscript compat suite). The single genuine non-hyperscript host failure is C2 (r7rs string->number shadow). --jit run: 5760p/276f, with 2 deterministic JIT-only divergences (J9). Logs: /tmp/sx-review/ocaml-run_tests-baseline.log, /tmp/sx-review/ocaml-run_tests-jit.log

Numbering note: findings carry mnemonic prefixes (J* serving-JIT, C* OCaml/kernel/protocol, JS* JavaScript host, P* cross-host parity, S* suspected). Order within CONFIRMED is roughly severity-desc but read the whole section — prefixes were assigned as agents reported, not in rank order.


CONFIRMED (most severe first)

Serving-JIT correctness cluster (J1J8). The OCaml http_mode (the production HTTP server behind sx.rose-ash.com) registers the JIT hook unconditionally (sx_server.ml:4163) — it is NOT gated by SX_SERVING_JIT (that env var only gates the epoch/persistent serving mode, sx_server.ml:5011). So every named top-level lambda invoked during page rendering is JIT-compiled on first call, and the divergences below are reachable in production for any rendered lambda that hits these patterns. The default run_tests --jit uses a different, low-hit-rate JIT path and surfaced only 2 divergent tests (see J9); the serving-JIT path is where the silent-wrong-value bugs live. All J1J8 reproduced live at the kernel level.

IMPORTANT reconciliation: there is a belief on record (project memory / a comment at sx_server.ml:5001-5011) that "serving-JIT is OPT-IN via SX_SERVING_JIT=1, default OFF." That gate applies ONLY to the epoch/persistent serving mode. The actual HTTP server (http_mode) calls register_jit_hook env UNCONDITIONALLY at sx_server.ml:4163 with no env-var check. So anyone who concludes "JIT is off in production, therefore J1J8 don't apply" is wrong — the http server that serves sx.rose-ash.com runs with JIT on. Confirmed by reading the http_mode entry and by the live boot logs ([jit] … compile in …s lines during every page render).

J1. -> threading miscompiles under serving-JIT — silent wrong value + duplicated side effects

  • severity: critical
  • confidence: CONFIRMED (reproduced live, self-verified)
  • where: lib/compiler.sx:934-951 (compile-thread-step); stale twin hosts/ocaml/lib/sx_compiler.ml:178
  • what: For (-> v f g) with non-empty rest-forms, compile-thread-step compiles call-expr (pushing a value that's never popped) then recurses with call-expr re-embedded, compiling it AGAIN. Each non-final step is evaluated once per remaining step (side effects duplicated) and leaves a stack residue. When the -> is argument ≥2 of a primitive call, CALL_PRIM pops the residue instead of the sibling arg → silently wrong value. In arg position of a user call the slot misaligns → "not callable" → self-heals via CEK fallback.
  • repro: with lib/compiler.sx loaded, serving-JIT on: (+ 1 (-> 2 inc inc)) on CEK → 5; JIT'd (define (t1) (+ 1 (-> 2 inc inc))) called → 7 (self-verified: (t1 ×5)(7 7 7 7 7); same source with JIT off → 5). bytecode-inspect shows the residue CONST/CALL_PRIM that's never popped.

J2. JIT-fallback re-runs the entire call on the CEK → side effects execute twice (persist/counter/emit double-apply)

  • severity: high
  • confidence: CONFIRMED (reproduced live, self-verified)
  • where: sx_server.ml:1630-1638, 1674-1680 (catch-all | e -> … l_compiled <- jit_failed; None); sx_vm.ml:461-464, 490-493 (Component/Island with _ -> cek_call_or_suspend re-run)
  • what: Any exception during VM execution of a JIT'd lambda (data-first HO form, macro call, raise/error, call/cc, undefined global) marks it jit-failed and re-runs the WHOLE call under the CEK. Mutations performed before the failure point are applied twice. A code comment claims idempotence "for the host's durable reads" — but it is NOT idempotent for writes (persist appends, counters, emits). The Component/Island path catches even VmSuspended, so a component that performed IO can have that IO re-issued.
  • repro (self-verified): (do (define fb-count 0) (define (fb) (set! fb-count (+ fb-count 1)) (map (list 1 2) (fn (x) x))) (fb) fb-count) → serving-JIT: 2 after ONE call; JIT off: 1.

J3. JIT'd functions eagerly evaluate arguments of user-macro calls before falling back

  • severity: high
  • confidence: CONFIRMED (reproduced live, self-verified)
  • where: lib/compiler.sx compile-list/compile-call (no macro-expansion pass); sx_vm.ml:497 (vm_call has no Macro case)
  • what: The compiler treats a user-macro call as an ordinary call — compiles and evaluates ALL arguments, then vm_call raises "not callable: " and the hook re-runs on the CEK. Code a macro guards from evaluation executes once in the VM.
  • repro (self-verified): my-unless macro that expands true → skip body; JIT'd (um) that does (my-unless true (set! hit (+ hit 1))) → serving-JIT: hit = 1; JIT off: hit = 0.

J4. VM component calls misparse positional string args equal to a param name

  • severity: high
  • confidence: CONFIRMED (agent-reproduced)
  • where: sx_vm.ml:144-156 (parse_keyword_args), fed by compile-expr lowering :kw to its string name
  • what: The compiler erases the keyword/string distinction (keywords become their string names in the constant pool), so parse_keyword_args treats ANY string matching a declared param as a keyword marker and consumes the next value → silent wrong props/children, no error, no fallback.
  • repro: (defcomp ~box2 (&key title &rest children) (list "BOX" title children)); (~box2 :title "T" "title" "x") CEK → ("BOX" "T" ("title" "x")); JIT'd → ("BOX" "x" ()).

J5. Specialized opcodes freeze primitive semantics at compile time — redefinition ignored by JIT'd code

  • severity: medium
  • confidence: CONFIRMED (agent-reproduced)
  • where: lib/compiler.sx:1061-1077 (specializes + - * / = < > cons 2-arg, not len first rest 1-arg → opcodes 160-172); sx_vm.ml:804-914
  • what: CALL_PRIM resolves through vm.globals at run time (redefinitions respected), but the 12 specialized opcodes inline the original semantics. A function JIT'd before (define + …) keeps the old + forever while the CEK sees the new one → CEK/JIT divergence.
  • repro: (define (q) (+ 1 2)) JIT'd → 3; (define + (fn (a b) 999)); (q) → 3 (stale OP_ADD) vs fresh CEK → 999.

J6. Redefining a primitive the compiler itself uses poisons subsequent JIT compilation

  • severity: medium
  • confidence: CONFIRMED (agent-reproduced)
  • where: sx_vm.ml:732-766 (CALL_PRIM globals-first lookup) + the compiler running as VM bytecode
  • what: The SX compiler's own bytecode calls +, first, nth… through CALL_PRIM → globals. After a user redefines one, every subsequent JIT compile computes with the user's fn — observed as corrupted pools/offsets ("CONST index 999 out of bounds"). Currently every hit errors → CEK fallback (correct), but a redefinition returning plausible numbers could emit structurally-valid wrong bytecode silently.

J7. Data-first HO forms (map coll fn) / -> coll (map fn) always fail under the VM → permanent deopt (+ J2 double-effect on first call)

  • severity: medium (perf/coverage; correctness preserved by fallback, but triggers J2)
  • confidence: CONFIRMED (agent-reproduced)
  • where: sx_primitives.ml:1584-1637 (map/filter/reduce/some/every?/for-each accept fn-first only); CEK ho-swap-args has no VM counterpart
  • what: Every JIT'd function using the documented data-first arg order (or threading a collection into an HO form) errors on first VM call and runs on the CEK forever. Results correct, but never benefits from JIT and the first call incurs the J2 double-side-effect hazard.

J8. Local bindings shadow HO-form names in the VM but not the CEK

  • severity: medium-low
  • confidence: CONFIRMED (agent-reproduced)
  • where: compiler compile-call scope resolution (locals win) vs CEK HO-form dispatch (form wins)
  • what: (let ((map (fn (a b) 42))) (map 1 2)) → JIT'd: 42; CEK inline: error "rest: 1 list arg" (dispatches as HO map). Divergent semantics for the 7 HO-form names.

J9. run_tests --jit diverges from interpreter on 2 hyperscript scoping tests (deterministic)

  • severity: medium
  • confidence: CONFIRMED (self-verified, 3× each mode, deterministic)
  • where: default run_tests --jit path (guard-installing lambdas interpret-only); hs-upstream-core/scoping suite
  • what: hs-upstream-core/scoping > behavior scoping is isolated from other behaviors and … from the core element scope PASS interpreter-only but FAIL under --jit with an empty result (Expected 20, got ). Deterministic across 3 runs each mode. This is the low-hit-rate default-JIT path (distinct from serving-JIT J1J8), so it's a second, independent JIT/CEK divergence surface. (Baseline 5762p/274f → --jit 5760p/276f; the hs-runtime-e2e "def function" entry fails in BOTH and is not a JIT regression.)

J10. Stale/incomplete Sx_compiler stub still bound as compile; JIT hook registered even if lib/compiler.sx fails to load

  • severity: medium-low
  • confidence: CONFIRMED (binding + staleness) / SUSPECTED (reachability)
  • where: sx_server.ml:1315 (bind), comment at :5014 ("incomplete stub"); hosts/ocaml/lib/sx_compiler.ml (May-7, no POP after LOCAL_SET, no rest-arity/guard/match/scope)
  • what: If lib/compiler.sx fails to load in serving-JIT mode the code warns but still registers the JIT hook, so jit_compile_lambda would compile with the stub whose missing LOCAL_SET/POP discipline can silently corrupt stacks (let-in-argument-position) rather than merely erroring.

J11. JIT debug/aux paths diverge from the real VM (vm-trace, code_from_value locals-scan, resume-error handler, threshold)

  • severity: low (tooling / edge)
  • confidence: CONFIRMED (by reading; J-threshold self-observed)
  • where: sx_vm.ml:1436-1632 (trace_run), :230-252 (code_from_value), :990-1005 (resume handler); sx_server.ml:1640-1656 (hook ignores jit_threshold)
  • what: (a) vm-trace resolves CALL_PRIM via get_primitive BEFORE globals (real run does opposite → user redefs invisible in trace), pushes Nil for non-NativeFn callees, inline ops 160-175 handle only Number, OP_EQ uses OCaml structural = → traces can show different values than execution. (b) code_from_value's max-local scan mis-walks operands for opcodes 8/33/35/144/51 (masked by +16 headroom). (c) resume-path error handler delivers String msg instead of the raised condition value (only reachable via extension opcodes / hand-built bytecode). (d) the CEK-side hook ignores jit_threshold=4 and compiles on the very FIRST call (why J1's t1 was already wrong on call #1); lambdas defined inside let-wrapped library modules never JIT at all (e.g. all of lib/highlight.sx).

J12. (verification, positive) perform/resume stack-misalignment from repro_jit_resume.ml is FIXED

  • confidence: CONFIRMED — all 9 repro cases (direct/multiframe/map-callback/pending_cek/reduce/ nested-map perform) produce expected values; the "drop all-but-first in map/for-each" hazard from the memory note does NOT reproduce at this level. Also verified matching (no divergence): closures with set! on captured locals, &rest under/over-application, define in do/let, named let, letrec, TCO + non-tail at 100k, quasiquote unquote/splice incl nested, dict literals, cond/case/when multi-expr, and/or, string/arith edge prims, keyword equality, apply, guard (correctly JIT-excluded), call/cc fallback, map over rest/filter/append lists, CEK↔VM global visibility both ways.

C0a. JS transpiler is blind to define-library — the browser/CI bundle is hollow (render/engine/router/signals/all adapters = 0 bytes)

  • severity: critical
  • confidence: CONFIRMED (reproduced live, self-verified — fresh build)
  • where: hosts/javascript/platform.py:15-35 (extract_defines), bootstrap.py:205-226
  • what: extract_defines only extracts top-level (define …) / register-special-form!. The spec+web files were since wrapped in R7RS (define-library … (begin …)) (spec/render.sx, web/adapter-html/-dom/-sx.sx, web/engine.sx, web/router.sx, spec/signals.sx, web/deps.sx, web/page-helpers.sx, web/orchestration.sx, web/web-signals.sx, lib/freeze.sx), so those files now transpile to ZERO bytes. The bundle still references the missing functions (renderToHtml in its own public API; dom-* guarded by typeof checks that silently skip), so rendering, signals, router, engine are simply ABSENT. spec/content.sx no longer exists and is silently skipped. This is the JS host's single biggest defect and reframes its status.
  • repro (self-verified): python3 hosts/javascript/cli.py --output <scratch>/verify-js.js then per-section byte counts → render/adapter-html/adapter-sx/adapter-dom/engine/orchestration/deps/ page-helpers/router/signals/freeze/content all = 6 bytes (marker only); evaluator 140KB, signals-web 99KB, boot 14KB, web-forms 8KB, parser 12KB survive (not define-library-wrapped).

C0b. node run_tests.js fails 2490/5086 — the JS conformance/CI gate is structurally red

  • severity: critical
  • confidence: CONFIRMED (reproduced live, self-verified)
  • where: hosts/javascript/run_tests.js; .gitea/run-ci-tests.sh (CI gates on this)
  • what: node hosts/javascript/run_tests.js2596 passed, 2490 failed (exit 1); --full → 2453p/3203f. A fresh rebuild produces identical failures — structural, not stale artifact. Same corpus on OCaml → 5762p/274f. Failure signature: 1190× Undefined symbol: dom-set-inner-html, 538× hs-to-sx*, 229× renderToHtml is not defined, 13× makeRtd is not defined — the hollow bundle (C0a) plus a runner that never learned to load the newer lib deps (parser-combinators, gql, hyperscript) the OCaml runner loads. Memory's "957/957 standard, 1080/1080 full" is stale. The JS host is alive as a CI/test host (sx-build-all.sh, run-tests.sh, .gitea CI) but DEAD as a served artifact (the live shell injects wasm/sx_browser.bc.wasm.js, not scripts/sx-browser.js) — so this drift breaks CI/conformance, not visitors.
  • repro (self-verified): timeout 400 node hosts/javascript/run_tests.jsResults: 2596 passed, 2490 failed.

C1. Malformed command line crashes the whole persistent/site sx_server process

  • severity: critical
  • confidence: CONFIRMED (reproduced live)
  • where: hosts/ocaml/bin/sx_server.ml:5048 (persistent mode) and :4950 (site_mode)
  • what: The dispatch loop calls Sx_parser.parse_all line outside any try; the enclosing handler only catches End_of_file, so Sx_types.Parse_error from one malformed line kills the entire process (the shared command channel used by bridges/conformance runners). site_mode has the identical unguarded parse.
  • repro: printf '(epoch 2)\n(eval "(+ 1 2"\n(epoch 3)\n(eval "99")\n' | timeout 60 hosts/ocaml/_build/default/bin/sx_server.exeFatal error: exception Sx_types.Parse_error("Unterminated list"); subsequent commands never run. Same with a plain-garbage line (not an s-expr ]]] {{{).

C10. Served browser JIT compiler is one fix behind lib/compiler.sx — the HO-loop desugar parity fix never shipped to the browser

  • severity: high
  • confidence: CONFIRMED (drift) / SUSPECTED (user impact)
  • where: shared/static/wasm/sx/compiler.sx + compiler.sxbc vs lib/compiler.sx:390-443
  • what: Commit 921db09f (2026-06-30, "jit: HO-loop desugar + --jit == CEK parity") added map/filter/reduce/for-each lambda-inlining to lib/compiler.sx, but the copy served to the browser (content from 59ac51a8, 2026-06-29) lacks all 54 lines; the .sxbc was rebuilt later (1e2ff387, Jul 2) but from the stale source. The browser JIT therefore misses the exact parity fix for the known "map/for-each drops all-but-first" bug class fixed server-side.
  • repro: diff lib/compiler.sx shared/static/wasm/sx/compiler.sx | head390,443d389; grep -c "_hml" lib/compiler.sx shared/static/wasm/sx/compiler.sx → 6 vs 0.

C11. Client reads module-manifest.sx that nothing generates anymore — stale lazy-dep graph

  • severity: high
  • confidence: CONFIRMED
  • where: consumer hosts/ocaml/browser/sx-platform.js:575 (fetches sx/module-manifest.sx); generator hosts/ocaml/browser/compile-modules.js:504 (writes module-manifest.json only)
  • what: loadWebStack() drives client module loading from module-manifest.sx, but the generator now only emits .json; no .sx writer exists. The served .sx manifest is from 2026-04-16 while modules are from Jul 1-2. Concretely _entry :lazy-deps is missing hs-worker and hs-prolog, so those plugins never lazy-load in the browser; any future dep/export change is invisible to the client.
  • repro: grep '_entry' shared/static/wasm/sx/module-manifest.sx → lazy-deps ends at hs-integration hs-htmx; module-manifest.json includes hs-worker, hs-prolog.

C1b. Non-ASCII byte on the top-level command channel also crashes the sx_server process (2nd instance of C1)

  • severity: critical
  • confidence: CONFIRMED (reproduced live, self-verified)
  • where: hosts/ocaml/lib/sx_parser.ml:38 (is_symbol_char is ASCII-only) + the unguarded command-channel parse (C1, sx_server.ml:5048)
  • what: A non-ASCII byte reaching the kernel's top-level command reader raises an uncaught Parse_error that kills the subprocess — same unguarded-parse root cause as C1, but reachable from any client that forwards a unicode-named symbol. The Python parser accepts and serializes bare unicode symbols (shared/sx/parser.py:158 accepts €-￿), so a Python-bridge caller can trigger this. Inside (eval "…") it's a catchable error; on the command channel it's fatal.
  • repro: printf '(epoch 1)\n(eval (quote café))\n(epoch 2)\n(eval "99")\n' | timeout 30 hosts/ocaml/_build/default/bin/sx_server.exeFatal error: exception Sx_types.Parse_error("Unexpected char: \195 …"); epoch-2 never runs.

JS1. define-record-type broken on JS: makeRtd platform constructor missing

  • severity: high
  • confidence: CONFIRMED (agent-reproduced)
  • where: spec/evaluator.sx:2584 calls (make-rtd …); no makeRtd in hosts/javascript/platform.py
  • what: The transpiled evaluator's record support calls makeRtd, which the JS platform never defines. Any define-record-type throws; test-cross-lang-types.sx (18 deftests) aborts.
  • repro: (define-record-type point (make-point x y) point? (x point-x) (y point-y)) → JS ERROR: makeRtd is not defined; OCaml (point-x (make-point 3 4)) → 3.

JS2. host-callback / host-await never fire for SX lambdas (wrong type tag)

  • severity: high
  • confidence: CONFIRMED (agent-reproduced)
  • where: hosts/javascript/platform.py:2332-2342 (host-callback), 2354-2361 (host-await)
  • what: Both check fn._type === "lambda", but Lambda instances are tagged _lambda === true (platform.py:889); the check never matches, so SX lambdas wrapped as JS callbacks become silent no-op functions. Any DOM event/promise callback registered through these does nothing.
  • repro: JS (let ((cb (host-callback (fn (x) (* x 2))))) (cb 21)) → nil; OCaml → 42.

JS3. JS arithmetic silently drops extra arguments (arity not checked)

  • severity: high
  • confidence: CONFIRMED (agent-reproduced)
  • where: hosts/javascript/platform.py:1021 (-), 1045 (/), 1086-1101 (< > <= >=)
  • what: Non-rational - returns args[0]-args[1] regardless of arity; / is strictly 2-arg; comparisons ignore args past the 2nd. OCaml errors on wrong arity, so buggy guest code passes silently on JS with wrong values.
  • repro: JS (- 5 1 2) → 4 (OCaml 2); (/ 24 2 3) → 12 (OCaml errors); (< 1 2 3) → true (OCaml errors).

JS4. Parser rejects . as a symbol — whole hyperscript-parser test file aborts

  • severity: high
  • confidence: CONFIRMED (agent-reproduced)
  • where: hosts/javascript/platform.py:2622 (_identStartRe lacks .)
  • what: . is a valid symbol on OCaml but "Unexpected character: ." on JS. test-hyperscript-parser.sx uses (quote .), so the file fails to parse and all 93 deftests are skipped (counted as 1 failure).
  • repro: JS (str (quote .)) → ERROR; OCaml → ".", type-of → "symbol". (Handoff: spec must rule whether . is a legal symbol.)

JS5. run_tests.js harness undercounts + shadows semantics

  • severity: medium
  • confidence: CONFIRMED (agent-reproduced)
  • where: hosts/javascript/run_tests.js:457-467, 78-79, 88-96, 126
  • what: (1) Each test file is wrapped in one try/catch — the first throw abandons the rest of the file and counts as ONE failure (~111 deftests silently skipped this run). (2) env-bind! and env-set! are defined identically (e[k]=v) — no scope-chain walk for env-set!, contradicting the platform's own envSet (platform.py:2098) and the spec bind/set distinction, so tests exercising that semantics test the wrong thing. (3) sha3-256 is a fake 32-bit hash padded to 64 hex — cross-host content-address comparison is meaningless. (4) lib/harness load errors are logged but never counted.

JS6. Transpiled (str …) doesn't skip nil (emission vs runtime disagree)

  • severity: medium
  • confidence: CONFIRMED (agent, emission rule read + runtime verified opposite)
  • where: hosts/javascript/transpiler.sx js-emit-list str clause vs runtime platform.py:1138-1144
  • what: The transpiler emits String(a)+String(b) for str, so transpiled spec code concatenates String(NIL) → "nil", while the CEK runtime skips nils. Identical expressions disagree between transpiled internals and guest code.

JS7. Transpiler has no quasiquote emission; quoted dicts emit garbage

  • severity: low
  • confidence: CONFIRMED (code read)
  • where: hosts/javascript/transpiler.sx js-emit-quote (no dict clause → [object Object]); no quasiquote case in js-emit-list
  • what: Only a hazard if a transpiled spec file uses top-level quasiquote or quoted dict literals; runtime quasiquote via CEK is correct on both hosts.

JS8. Stale JS host metadata + stale checked-in sx-full-test.js

  • severity: low
  • confidence: CONFIRMED (agent)
  • where: bootstrap.py:2-11 docstring (refs deleted js.sx/run_js_sx.py/platform_js.py/shared/sx/ref/*), bootstrap.py:287 (bad default output path), cli.py:17 (_PROJECT one level too high → /root on sys.path), manifest.py (reports the hollow bundle as healthy); shared/static/scripts/sx-full-test.js (Apr 26, predates open-input-string → 35 --full failures)

C24. Python↔OCaml boundary validation is permanently a no-op (imports a nonexistent module)

  • severity: high
  • confidence: CONFIRMED (reproduced live, self-verified)
  • where: shared/sx/boundary.py:34
  • what: _load_declarations() does from .ref.boundary_parser import … but shared/sx/ref/boundary_parser.py does not exist (real file is hosts/python/boundary_parser.py). The ModuleNotFoundError is swallowed by a bare except Exception and NOT cached, so every validate_primitive/validate_io/validate_helper/validate_boundary_value silently returns without checking — even under SX_BOUNDARY_STRICT=1. The whole SX-boundary enforcement subsystem is dead. NOTE: shared/sx/ is load-bearing in production (Quart services render SX via the bridge), so this is not dead-host code.
  • repro: SX_BOUNDARY_STRICT=1 python3 -c "from shared.sx import boundary; boundary.validate_primitive('definitely-not-a-primitive-xyz')"pure: 0 and NO ERROR RAISED.

C25. Parser string-escape divergence corrupts data across the Python↔OCaml boundary (\0, \x, unknown escapes)

  • severity: high
  • confidence: CONFIRMED (reproduced live, self-verified)
  • where: shared/sx/parser.py:108 (_ESCAPE_MAP) / :471 (serialize) vs hosts/ocaml/lib/sx_parser.ml:61-79
  • what: Python maps \0→NUL and drops the backslash on unknown escapes (\xx); OCaml treats \0 as literal backslash+zero and preserves the backslash on unknown escapes (\x\x). Python serialize() emits \0 for a NUL char, so a 3-char Python string round-trips to a 4-char OCaml string — silent corruption on any path that serializes a NUL-bearing string and re-parses it in the kernel. </script also diverges (Python <\/script vs OCaml differently).
  • repro: python3 -c "from shared.sx.parser import serialize; print(repr(serialize('a\x00b')))"'"a\\0b"'; kernel (len "a\0b")4 (should be 3).

C26. Python parser accepts bare unicode symbols the OCaml kernel rejects → error or crash

  • severity: high
  • confidence: CONFIRMED (reproduced live, self-verified — see C1b)
  • where: shared/sx/parser.py:158 (SYMBOL regex accepts €-￿) / :462 (serialize bare) vs sx_parser.ml:38 (ASCII-only)
  • what: The Python tokenizer accepts unicode letters in symbol names and serializes them bare; OCaml's symbol charset is ASCII-only and raises Parse_error on the first non-ASCII byte. In an (eval "…") string it's catchable; on the command channel it's a fatal subprocess kill (C1b).
  • repro: python3 -c "from shared.sx.parser import serialize; from shared.sx.types import Symbol; print(serialize(Symbol('café')))"café (bare); kernel crashes on it.

C27. Dict key ordering not preserved across hosts (wire/golden/cache-key mismatch)

  • severity: medium
  • confidence: CONFIRMED (reproduced live, self-verified)
  • where: shared/sx/parser.py:394-413 / :488-493 vs OCaml kernel dict serialization
  • what: Python dict preserves insertion order; the OCaml kernel reorders/reverses. Any cache key, golden fixture, or wire payload relying on stable dict serialization mismatches between the Python-serialized form and the kernel-serialized form.
  • repro: Python serialize(parse('{:a 1 :b "two"}')){:a 1 :b "two"}; kernel (quote {:a 1 :b "two"}){:b "two" :a 1}.

C28. Two distinct SxExpr classes; serialize() double-quotes the types.py one (nested wire corruption)

  • severity: medium
  • confidence: CONFIRMED (agent-reproduced)
  • where: shared/sx/parser.py:71 and shared/sx/types.py:408 (two unrelated class SxExpr(str)); parser.py:441 isinstance checks only the parser one
  • what: parser.SxExpr and types.SxExpr are unrelated classes. serialize() emits pre-built SX unquoted only for parser.SxExpr; a types.SxExpr falls through to the plain-string branch and is double-quoted/escaped. handlers.py:182 returns SxExpr(result_sx) (the types one), so any path nesting a handler result into an AST and re-serializing wraps the wire fragment in quotes.
  • repro: serialize([Symbol('div'), types.SxExpr('(~inner :a 1)')])(div "(~inner :a 1)") (wrong) vs parser.SxExpr → (div (~inner :a 1)) (right).

C29. Reader-macro auto-resolution broken (OcamlSync.start() doesn't exist)

  • severity: medium
  • confidence: CONFIRMED (agent-reproduced)
  • where: shared/sx/parser.py:57 (_sync.start()); shared/sx/ocaml_sync.py (class has _ensure, no start)
  • what: _resolve_sx_reader_macro (auto-resolving #name reader macros like #z3) calls OcamlSync().start(), but OcamlSync exposes _ensure(). The AttributeError is caught by except Exception: return None, so every auto-resolve silently returns None → ParseError("Unknown reader macro: #…"). The feature is non-functional.
  • repro: python3 -c "from shared.sx.ocaml_sync import OcamlSync; print(hasattr(OcamlSync(),'start'))"False.

C30. Python standalone host (hosts/python/) is a non-functional stale target

  • severity: medium
  • confidence: CONFIRMED (agent-reproduced)
  • where: hosts/python/bootstrap.py, platform.py, transpiler.sx
  • what: hosts/python/ masquerades as a supported host but its bootstrapper emits syntactically invalid Python (if sx_truthy((len(*winders*) > …)) — SyntaxError) and can't produce a working evaluator. Nothing imports it except its own internal bootstrap→platform. The hosts/python/tests/ dir referenced by memory/RESTRUCTURE_PLAN.md does not exist. Transpiler also warns its spec has drifted (eval.sx dispatches forms not in special-forms.sx: provide, scope, define-record-type …).
  • repro: python3 hosts/python/bootstrap.py > /tmp/t.py; python3 -c "import t" → SyntaxError at line 1564.

C31. shared/sx/tests/ is 14/33 files broken; test_ocaml_bridge.py has 5 live failures

  • severity: medium
  • confidence: CONFIRMED (agent-reproduced)
  • where: shared/sx/tests/ (dangling shared.sx.ref.sx_ref imports); test_ocaml_bridge.py
  • what: 14/33 test files fail collection (import the deleted shared.sx.ref.sx_ref). test_ocaml_bridge.py has 5 failures: test_parse_response_ok_number (the (ok N) epoch-ambiguity below) and 4 test_render_* that still use flat (let (x v) body) binding syntax the kernel now rejects (real Python-test/kernel drift).

C2. lib/r7rs.sx shadows string->number, dropping the spec's optional radix — spec test fails on the OCaml runner

  • severity: medium
  • confidence: CONFIRMED (baseline test failure + root cause isolated + differential vs bare server)
  • what: spec/primitives.sx:1104 declares string->number with &rest (radix :as number), and the native prim (hosts/ocaml/lib/sx_primitives.ml:486) accepts 12 args. But lib/r7rs.sx:83 redefines it as (fn (s) ...) (1 param) and re-exports to the global namespace ("backward compatibility" import at end of file). Every environment that loads r7rs.sx (run_tests does, at run_tests.ml:3711) loses the radix arg: (string->number "ff" 16)string->number expects 1 args, got 2. Note the adjacent number->string wrapper (r7rs.sx:76) does this correctly by delegating to the captured native prim for the radix case — string->number just wasn't given the same treatment. The shadow also changes 1-arg semantics (parse-int/parse-float vs the native parser).
  • repro: baseline log line 4179 FAIL: > string->number: string->number expects 1 args, got 2 (spec/tests/test-math.sx:112). Bare server WITHOUT r7rs loaded: printf '(epoch 1)\n(eval "(string->number \"ff\" 16)")\n' | timeout 30 hosts/ocaml/_build/default/bin/sx_server.exe255 (correct).

C12. compile-modules.js SOURCE_MAP has 5 dead source paths that silently no-op

  • severity: medium
  • confidence: CONFIRMED
  • where: hosts/ocaml/browser/compile-modules.js:38-49
  • what: signals.sx→web/signals.sx, hypersx.sx→web/hypersx.sx, tw-layout/tw-type/tw.sx→web/tw-*.sx all point at files that no longer exist (moved to web/web-signals.sx, web/lib/hypersx.sx, shared/sx/templates/tw-*.sx). fs.existsSync guards make the skips silent, so edits to the real sources are never synced by this script — the rot vector that plausibly produced the C10 compiler drift.
  • repro: for f in web/signals.sx web/hypersx.sx web/tw-layout.sx web/tw-type.sx web/tw.sx; do [ -f $f ] || echo MISSING $f; done → all 5 MISSING.

C13. test_platform.js broken — references deleted web/signals.sx (browser platform test tier unrunnable)

  • severity: medium
  • confidence: CONFIRMED
  • where: hosts/ocaml/browser/test_platform.js:65
  • what: The "full WASM + platform stack" Node test crashes at load: web/signals.sx was renamed to web/web-signals.sx and the file list was never updated.
  • repro: timeout 60 node hosts/ocaml/browser/test_platform.jsError: ENOENT … web/signals.sx.

C14. hosts/ocaml/browser/dist/ is an 8-week-stale duplicate bundle that sx-build-all.sh still feeds

  • severity: medium
  • confidence: CONFIRMED
  • where: hosts/ocaml/browser/dist/ (72 files, May 7) vs shared/static/wasm/ (101 files, Jul 1-2); scripts/sx-build-all.sh:19-32
  • what: Runtime serving is exclusively from shared/static/wasm/ (shared/sx/helpers.py:1080, sx_server.ml:3349/4525), but bundle.sh/compile-modules.js default to dist/ and sx-build-all.sh still syncs into and compiles from it — a full run can compile .sxbc from May-7-era sources for anything its sync list misses. Two half-overlapping bundle dirs are the root cause of C10/C11.
  • repro: ls hosts/ocaml/browser/dist/sx | wc -l → 72 vs 101; dist mtimes 2026-05-07.

C15. Git-tracked stale kernel artifacts in hosts/ocaml/shared/static/wasm (5MB dead blob)

  • severity: medium
  • confidence: CONFIRMED
  • where: hosts/ocaml/shared/static/wasm/{sx_browser.bc.js (5.0MB Apr 9), sx_browser.bc.wasm.js, sx-platform.js (Apr 16)}
  • what: A wrong-cwd copy of the deployed wasm dir, committed to git, months stale (missing newer kernel exports like resetStepCount/setStepLimit); nothing references it.
  • repro: git ls-files hosts/ocaml/shared/ → 3 files.

C19. HTML vs DOM adapter disagree on non-boolean attributes with raw boolean values (SSR/hydration mismatch)

  • severity: medium
  • confidence: CONFIRMED (HTML side run; DOM side unambiguous in source)
  • where: spec/render.sx:render-attrs (~263-283) vs web/adapter-dom.sx:render-dom-element (~356-366)
  • what: For an attr NOT in BOOLEAN_ATTRS with value true: HTML emits attr="true", DOM emits attr="". With value false: HTML emits attr="false", DOM omits the attribute. Any data-*/aria attr driven by a boolean diverges between SSR and client render → hydration mismatch + needless sync-attrs morph churn.
  • repro: sx_harness_eval (render-to-html (quote (div :data-x false :data-y true :data-z nil)))<div data-y="true" data-x="false"></div>; DOM branch produces data-y="" and drops data-x.

C20. CSRF token attached to cross-origin fetches; computed cross-origin flag is dead

  • severity: medium
  • confidence: CONFIRMED (code path) / SUSPECTED (browser-level exploitability)
  • where: web/orchestration.sx:do-fetch (line 183 header injection, 207-208 cross-origin flag); consumer web/lib/boot-helpers.sx:fetch-request (354-390)
  • what: do-fetch adds X-CSRFToken unconditionally (no origin gate) and computes a cross-origin field that fetch-request never reads (nor sets credentials/mode). Net: CSRF token leaks to third-party endpoints; the cross-origin classification is inert. No test exercises CSRF header injection or origin gating.

C21. Test harness IO model bypasses the real perform/suspend path (structural coverage gap)

  • severity: medium
  • confidence: CONFIRMED
  • where: spec/harness.sx default-platform (line 34), make-interceptor (52), install-interceptors (55)
  • what: The harness binds IO ops as plain synchronous NativeFns, never as perform-suspending primitives — so no harness test can exercise CEK suspend/cek-resume or the VM inline-settle path. The known HO+perform element-drop class (S10) is structurally invisible to the harness: (map (fn (u) (fetch u)) …) under a mock returns all elements because fetch is a direct call.
  • repro: sx_harness_eval of (map (fn (u) (get (fetch u) "status")) (list "a" "b" "c")) with fetch mock → (200 200 200), all logged — real serving path is the one at risk.

C22. Harness interceptor logs IO call only after the mock returns — throwing mocks invisible

  • severity: low
  • confidence: CONFIRMED
  • where: spec/harness.sx:make-interceptor (line 52)
  • what: append! to the IO log happens after mock-fn returns; a raising mock leaves no log entry, so assert-io-called/count falsely report "never invoked" on error-path tests. (Positive side-finding: host IO errors DO surface as catchable SX errors.)
  • repro: try-catch around (fetch "http://a") with mock raising "boom-io" → caught, but IO log shows "(no IO calls)".

C23. adapter-dom test suite tests membership predicates only — zero render-output tests

  • severity: low
  • confidence: CONFIRMED
  • where: web/tests/test-adapter-dom.sx (18 deftests)
  • what: All 18 tests assert *-is-a-render-form?/RENDER_HTML_FORMS membership; none test actual render-to-dom output (boolean attrs, on-*/bind/ref/key, reactive attrs, void elements, hydration cursor). The 1512-line DOM adapter holding C19 + hydration handoff is the thinnest-tested adapter relative to size.

C3. Dead stale-io-response guard in the epoch loop (13-char literal vs 14-char substring)

  • severity: low
  • confidence: CONFIRMED (reproduced live)
  • where: hosts/ocaml/bin/sx_server.ml:5043-5046
  • what: String.sub line 0 14 = "(io-response " compares a 14-byte substring to a 13-byte literal — never true, guard never fires. A stray (io-response …) line falls through to dispatch and emits an extra (error N "Unknown command …") reply instead of being silently discarded — an unexpected extra response for the client.
  • repro: printf '(epoch 1)\n(io-response 1 42)\n(eval "5")\n' | timeout 60 …/sx_server.exe(error 1 "Unknown command: (io-response 1 42)") then the eval reply.

C4. Malformed (epoch) / (epoch foo) doesn't update the epoch — responses tagged stale

  • severity: low
  • confidence: CONFIRMED (reproduced live)
  • where: hosts/ocaml/bin/sx_server.ml:5051-5054
  • what: Only (epoch <number>) updates current_epoch; malformed epoch markers fall through as unknown commands and the old epoch keeps tagging subsequent responses. A client whose epoch line was mangled will discard every following response as stale → hang.
  • repro: printf '(epoch)\n(eval "2")\n(epoch foo)\n(eval "3")\n' | … → all replies tagged epoch 0.

C5. No monotonic-epoch enforcement despite protocol comment claiming it

  • severity: low
  • confidence: CONFIRMED (reproduced live)
  • where: hosts/ocaml/bin/sx_server.ml:5051-5054, :4952-4955 (comment at :259-262)
  • what: current_epoch := n unconditionally; decreasing/repeated epochs accepted silently, so client bugs/reordering mis-tag responses instead of being detected.
  • repro: printf '(epoch 9)\n(epoch 3)\n(eval "42")\n' | …(ok-len 3 2).

C6. Two commands on one line → both dropped with a single error (client desync)

  • severity: low
  • confidence: CONFIRMED (reproduced live)
  • where: hosts/ocaml/bin/sx_server.ml:5055-5056
  • what: A line parsing to >1 expr returns one (error … "Expected single command, got 2") and executes neither — a pipelining client gets a response-count desync. Related: a command with no preceding (epoch N) is answered under the previous epoch tag.

C7. vm-trace with compiler not loaded errors as "Not callable: nil"

  • severity: low
  • confidence: CONFIRMED (reproduced live)
  • where: hosts/ocaml/bin/sx_server.ml:2193-2201 (vm-trace dispatch)
  • what: If lib/compiler.sx isn't loaded (e.g. load failed because it is cwd-relative and the server was started from hosts/ocaml), (vm-trace "…") fails with the opaque (error N "Not callable: nil") instead of "compiler not loaded". Minor DX.
  • repro: from hosts/ocaml: printf '(epoch 1)\n(load "lib/compiler.sx")\n(epoch 2)\n(vm-trace "(+ 1 2)")\n' | timeout 60 _build/default/bin/sx_server.exe(error 1 "File error: …") then (error 2 "Not callable: nil").

C8. Stray triplicated source tree hosts/ocaml/hosts/ocaml/hosts/ocaml/

  • severity: low
  • confidence: CONFIRMED
  • what: A nested duplicate of the OCaml tree (154 KB Apr-9 copy of sx_server.ml etc.) sits inside hosts/ocaml. Not built by dune, but a grep/edit-wrong-file trap.

C16. hosts/native is an orphaned but healthy PoC — builds green, referenced by nothing

  • severity: low
  • confidence: CONFIRMED
  • where: hosts/native/ (own dune-workspace; lib_sx → ../../hosts/ocaml/lib symlink)
  • what: SDL2/Cairo native SX renderer, single commit f0d8db9b (2026-04-09), unreferenced by scripts/CI/docs. NOT bit-rotted: compiles against current sx library and its smoke test passes (parse → 25-node render tree → layout → Cairo paint → PNG). Because it has its own dune-workspace, the main build never compiles it, so silent breakage would go unnoticed. Positive: it shares the evaluator by tracked symlink — no fork drift possible.
  • repro: cd hosts/native && eval $(opam env) && timeout 280 dune build ./test/test_render.exe → exit 0; run → === All OK! ===.

C17. Tracked-but-dead browser files: sx-platform-2.js and 23 *.sxbc.json

  • severity: low
  • confidence: CONFIRMED (dead refs) / SUSPECTED (.sxbc.json fully unused)
  • where: shared/static/wasm/sx-platform-2.js (Apr 16); shared/static/wasm/sx/*.sxbc.json
  • what: sx-platform-2.js referenced by nothing. .sxbc.json written by mcp_tree.ml:1211 and copied by compile-modules.js:355, but the client loads the .sxbc SX-text format — no runtime reader found. All ~2.5 months stale vs their .sxbc twins.

C18. Untracked repo-root clutter: spa-debug.js, scripts/sx-sessions-restore.sh

  • severity: low
  • confidence: CONFIRMED
  • what: spa-debug.js is a leftover Playwright compare of direct-load vs boosted SPA nav of relate-picker (Jun 30 debugging); sx-sessions-restore.sh is an ops script embedding session UUIDs + claude --dangerously-skip-permissions invocations. Commit, relocate, or remove.

C9. run_tests spec suites print with empty suite label

  • severity: low
  • confidence: CONFIRMED (baseline log)
  • what: spec test suites (e.g. test-math.sx) report as FAIL: > string->number with a blank suite name in run_tests output — harness labeling defect, makes triage harder.

CROSS-HOST PARITY (CONFIRMED divergences — same expr, different result per host)

Enumeration (900-name universe): spec declares 202 primitives; OCaml resolves 536, JS 668, Python 112. Only 97 names common to all three. Spec primitives MISSING: OCaml 9 (eq? eqv? escape format format-date parse-datetime pluralize strip-tags sx-parse), JS 6 (downcase eq? eqv? equal? json-encode upcase), Python 108 (whole modules — see PY block). Differential battery files in scratchpad: battery*.json, parity-js.js.

P1. OCaml float serialization is lossy — SX-text wire round-trip destroys precision

  • severity: high
  • confidence: CONFIRMED (self-verified)
  • where: hosts/ocaml/lib/sx_types.ml:415-423 (format_number uses %g = 6 sig digits for fractional floats)
  • what: All fractional floats print with 6 sig digits; JS prints shortest-round-trip. Any value crossing the SX text wire (aser, epoch protocol, persisted SX) from OCaml silently loses precision. Integral floats ≥1e16 use %.17g (lossless) — inconsistent within the same host. This is the PRODUCTION evaluator, so it affects live wire payloads.
  • repro (self-verified): OCaml (number->string (/ 1 3))"0.333333"; (+ 0.1 0.2)"0.3"; (= (parse-number (number->string (/ 1 3))) (/ 1 3))false. JS same → true / 0.3333333333333333.

P2. OCaml sort breaks on mixed int/float lists (polymorphic compare on constructor tag)

  • severity: high
  • confidence: CONFIRMED (self-verified)
  • where: hosts/ocaml/lib/sx_primitives.ml:1290-1292 (List.sort compare l)
  • what: OCaml polymorphic compare orders Integer before Number by variant tag, so numerically mixed lists sort wrongly and silently.
  • repro (self-verified): OCaml (sort (list 1.5 10 2))(2 10 1.5); JS → (1.5 2 10).

P3. into is not native on the OCaml kernel — routed over the IO helper bridge, fails standalone

  • severity: medium
  • confidence: CONFIRMED (self-verified)
  • where: OCaml resolves into but execution emits (io-request N "helper" "into" …)
  • what: A spec core.dict primitive requires a live Python-side helper on OCaml; JS/Python have it native. Any pure-OCaml context (tests, CLI, and notably the eval/load-source command paths that don't service helper IO) can't use into.
  • repro (self-verified): OCaml (into {} (list (list :a 1))) → io-request then error … "IO bridge: stdin closed while waiting for io-response"; JS → {:a 1}.

P4. Exact-integer semantics diverge (OCaml int63 Integer vs JS float64)

  • severity: high
  • confidence: CONFIRMED (agent)
  • where: hosts/ocaml/lib/sx_types.ml:47-48 (Integer distinct from Number); JS has only doubles
  • what: Arithmetic differs beyond 2^53; literals above int63 silently become floats on OCaml with different text form; exactness is host-dependent and not wire-preserved (both serialize 1.0 as "1", which OCaml re-reads as exact Integer).
  • repro: (+ 9007199254740992 1) → OCaml 9007199254740993, JS …992; (* 111111111 111111111) → OCaml 12345678987654321, JS …320; (float? 1.0) → OCaml true, JS false.

P5. JS = is not deep on dicts (spec: alias for equal?); equal?/eq?/eqv? missing from JS bundle

  • severity: high
  • confidence: CONFIRMED (agent + JS agent)
  • where: hosts/javascript/platform.py:841 (sxEq falls through to false for plain-object dicts); no PRIMITIVES["equal?"/"eq?"/"eqv?"] in the JS bundle (run_tests.js:126 patches them only into the test env)
  • what: = deep-compares lists but reference-compares dicts on JS; OCaml deep-compares both. And the spec equality trio is absent from the browser bundle — harness assertions (spec/harness.sx:82,85,88 call equal?) and any component using equal? crash client-side. OCaml also lacks eq?/eqv? (has non-spec identical? instead).
  • repro: (= {:a 1} {:a 1}) → OCaml true, JS false; (equal? (list 1 2) (list 1 2)) → OCaml true, JS ERROR.

P6. String length/indexing units differ across hosts (UTF-8 bytes vs UTF-16 code units)

  • severity: high
  • confidence: CONFIRMED (agent)
  • where: OCaml String.length (bytes) vs JS .length (UTF-16, platform.py:1386)
  • what: Neither counts codepoints; any substring/index arithmetic on non-ASCII text diverges cross-host (and both differ from user-perceived length).
  • repro: (len "héllo") → OCaml 6, JS 5; (len "🎉") → OCaml 4, JS 2.

P7. JS lenient-coercion cluster vs OCaml strictness (silent wrong values on the client)

  • severity: medium (aggregate; several individually medium/high)
  • confidence: CONFIRMED (agent + JS agent)
  • where: hosts/javascript/platform.py (str 1138, range 1380, len 1386, cons 1391, get 1385, parse-int 1500, split 1148, round 1058, slice 1167, max/min 1065)
  • what: JS coerces where OCaml errors/strict → divergent results the client renders silently: (str (list 1 2)) JS "1,2" (OCaml "(1 2)"); (str {:a 1}) JS "[object Object]" (OCaml "{:a 1}"); (range 3) JS () (OCaml (0 1 2)); (len nil) JS 2 (OCaml 0); (cons 1 nil) JS (1 nil) (OCaml (1)); (get {:a nil} :a 42) JS nil (OCaml 42); (get "abc" 1) JS "b" (OCaml nil); (parse-int "abc") JS 0 (OCaml nil); (parse-int "42px") JS 42 (OCaml nil); (split "abc" "") JS ("abc") (OCaml ("a" "b" "c")); (round -2.5) JS -2 (OCaml -3); (slice "hello" -3) JS "llo" (OCaml "hello"); (max) JS -Infinity (OCaml errors); (+ 1 "2") JS "12" (OCaml 3); (mod 10 0) JS NaN (OCaml Division_by_zero).

P8. nil/list-strictness family diverges, some arms violating the spec on OCaml's side

  • severity: medium
  • confidence: CONFIRMED (agent)
  • where: OCaml sx_primitives.ml strict arms vs JS lenient (platform.py:1391-1392)
  • what: (merge nil {:a 1}) → OCaml ERROR (spec says skip nil → OCaml BUG) vs JS {:a 1}; (contains? {:a 1} :a) → OCaml ERROR "contains?: 2 args" (spec: dict key check → OCaml BUG) vs JS true; (append nil (list 1)) → OCaml (1) vs JS ERROR; (cons 1 2) → OCaml ERROR vs JS (1 2); (reverse "abc") → OCaml ERROR vs JS "cba".

P9. Dict key ordering differs per host (Hashtbl vs insertion order) — SX text wire not canonical

  • severity: medium
  • confidence: CONFIRMED (agent; also self-verified in the Python block C27)
  • where: OCaml dicts are Hashtbl (arbitrary bucket order); JS objects preserve insertion; Python preserves insertion
  • what: keys/vals iteration order and serialized dict text differ per host — anything comparing or hashing serialized SX text cross-host diverges. CBOR/CID layer is safe (sx_cbor.ml:67 sorts keys).
  • repro: (keys {:z 1 :a 2 :m 3}) → OCaml ("m" "z" "a"), JS ("z" "a" "m").

P10. NaN/Infinity are not portable wire tokens; JS can't re-read its own output

  • severity: medium
  • confidence: CONFIRMED (agent)
  • where: sx_types.ml:416-418 prints nan/inf/-inf (OCaml re-reads all forms) vs JS prints Infinity/NaN (resolves neither)
  • repro: (/ 1 0) → OCaml inf, JS Infinity; eval Infinity → OCaml inf, JS ERROR undefined symbol.

P11. upper/lower Unicode behavior differs; upcase/downcase aliases missing on JS; round half-mode differs

  • severity: medium
  • confidence: CONFIRMED (agent)
  • where: sx_primitives.ml:890-897 (uppercase_ascii) vs platform.py:1145 (full Unicode); JS lacks upcase/downcase; round half-mode
  • repro: (upper "héllo") → OCaml "HéLLO", JS "HÉLLO"; (upcase "x") → OCaml "X", JS ERROR; (round -2.5) → OCaml -3, JS -2.

P12. OCaml zip-pairs chunks pairs; spec + lib/stdlib.sx define a sliding window (kernel diverges from its own spec)

  • severity: medium
  • confidence: CONFIRMED (JS agent handoff)
  • where: OCaml native zip-pairs vs spec/primitives.sx:663 + lib/stdlib.sx:207 (JS matches spec)
  • what: OCaml native zip-pairs returns ((1 2)(3 4)) but the spec + stdlib define sliding window ((1 2)(2 3)(3 4)). Handoff to core, but the OCaml host is the one diverging from the spec.

PY (Python host). Standalone hosts/python/ cannot evaluate the current spec at all

  • severity: critical (as a supported target) — see C30 for the standalone-host summary
  • confidence: CONFIRMED (agent)
  • where: hosts/python/bootstrap.py:119 (_mangle), generated lines 1564/3482/3498; spec match form
  • what: Four independent breaks in the Python bootstrapper: (1) _mangle RENAMES lacks *winders* → SyntaxError; (2) no rule for -> in names → def string_>symbol invalid; (3) the match special form is emitted as an eager function call → undefined match, and even shimmed it evaluates all branches eagerly (core dispatch unrunnable); (4) references missing natives make-char/aser-call/aser-fragment. Python resolves only 112 of the 900-name universe and lacks 8 primitive modules (math/bitwise/bytevectors/vectors/regexp/sets/rational/hash-table = 108 spec primitives). This overlaps/extends C30. NOTE: shared/sx/ Python (parser+bridge) IS load-bearing in production; the standalone evaluator host is what's dead.
  • repro: python3 hosts/python/bootstrap.py > /tmp/x.py && python3 -c "import x" → SyntaxError line 1564; after mangle-patch, (+ 1 2)name 'match' is not defined.

(informational) ok-len framing is byte-accurate and desync-resistant

  • confidence: CONFIRMED — length prefixes are byte counts; newlines/framing patterns in payloads are escaped; multibyte UTF-8 counted correctly. (Positive result.)

(informational) WASM browser kernel boots green; kernel is single-source with server

  • confidence: CONFIRMED — test_boot.sh → WASM boot: OK; test_wasm.sh → 29/29; test_kernel.js → 24/24; deployed kernel artifacts byte-identical to _build outputs. Browser links the identical sx OCaml library as sx_server (no evaluator fork); only sx_browser.ml FFI glue is browser-specific. (Positive result.)

SUSPECTED (reasoning only) — NOTE: S1/S4/S5 UPGRADED to CONFIRMED via live HTTP repro

Follow-up live testing (bounded sx_server --http on free ports, 3 runs) upgraded three of the HTTP-race items and clarified two more:

  • S1 CONFIRMED — observed a live intermittent crash (empty responses, no exception, OOM ruled out at ~292 MB peak) under concurrent multi-Domain load.
  • S4 CONFIRMED — routing-failure page served as HTTP 200 and cached (cold 2.02s → warm 0.0005s).
  • S5 CONFIRMED — cache key ignores cookies (3 different session= → identical cached body) and query.
  • S2/S3 remain SUSPECTED — mechanism verified live in the code path, but no visible symptom is triggerable in the stock static-docs app (no full-page route renders request args / no expansion-sensitive component to observe). They become observable on request-varying apps. Their entries below carry updated per-finding confidence lines.

S1. HTTP mode: shared mutable state raced across parallel render-worker Domains — LIVE CRASH OBSERVED

  • severity: high
  • confidence: CONFIRMED (live crash reproduced once; race is real by construction) — was SUSPECTED
  • where: hosts/ocaml/bin/sx_server.ml:4717 (Domain.spawn × max 4 (recommended_domain_count)), :4312/:4677 (Hashtbl.replace response_cache from workers + main), :4654 (main reads it), :2666-2670 (env.bindings bind+remove), :345 (_stream_mutex created but NEVER locked)
  • what: http_mode spawns 4 real Domain.spawn workers rendering full-page requests on the single shared env, concurrently with the accept-domain doing AJAX renders. There is no lock around rendering. Workers + main mutate process-global Stdlib.Hashtbls (response_cache, env.bindings) concurrently; OCaml 5 Stdlib.Hashtbl is NOT domain-safe (memory corruption on rehash → segfault, no catchable exception). The comment at :4204 claiming cache writes happen "only during single-threaded startup" is false (workers write at runtime); the :343-345 comment admits "concurrent CEK evaluations corrupt shared state".
  • LIVE REPRO (self-verified). Booted sx_server --http <port> on a free port (3 runs). Under a burst of ~300 concurrent AJAX + ~150 concurrent full-page requests to distinct uncached paths (all 4 worker Domains + main thread writing response_cache/env.bindings simultaneously), run 2 CRASHED: the process vanished mid-render — no OCaml exception logged (the try/with at :4315 would have printed [render] Error; none present), no "Binary changed" restart, and a concurrent poll of a known-good cached page began returning EMPTY bodies (md5 of "") for 59/60 reads as the server died. OOM ruled out: run 3 under an even heavier burst survived with peak RSS ~292 MB (baseline ~200 MB) — memory never approached exhaustion. The crash is INTERMITTENT (run 1 clean, run 2 crashed, run 3 survived) — the classic signature of a data race, not a deterministic fault. Net: the production HTTP server can crash under concurrent load; I couldn't capture a core dump (apport core_pattern, restricted) to pin the exact fault address, so the "Hashtbl-rehash-segfault" mechanism is inferred from the crash profile + the unsynchronized-access code, not a stack trace. Logs: /tmp/sx-review/http-server{,2,3}.log.

S2. HTTP mode: per-request state is process-global → cross-request contamination

  • severity: high
  • confidence: SUSPECTED (mechanism confirmed live; visible symptom not triggerable in the stock docs app)
  • where: sx_server.ml:4335-4354 (main domain sets _req_method/_req_query/_req_headers/_req_body
    • Hashtbl.clear/replace _request_cookies), :4703 (full-page render QUEUED to a worker with only (fd, path, headers) — NOT the request context), consumed by request-* primitives at :3772-3829
  • what: I confirmed live that full-page misses are queued to worker Domains carrying only the path, while the worker's render reads the process-global _req_query/_req_body/_request_cookies that the main loop overwrites on the very next request. So a queued render can read a subsequent request's query/body/cookies (plus a plain main-writes-while-worker-reads data race). I could NOT produce a visible crossed-value symptom because the sx-docs app has no full-page route that renders (request-arg …)/get-cookie/body into its output — the docs pages are static. The contamination is therefore structurally present and live-reachable but only becomes observable for an app whose full-page renders vary by request args (e.g. any of the real Quart-replacement domains). Kept SUSPECTED honestly: mechanism verified, symptom not exhibited on the stock app.

S3. expand-components? global binding removed mid-flight by AJAX render

  • severity: medium
  • confidence: SUSPECTED (bind/remove verified live in code path; visible symptom not isolated)
  • where: sx_server.ml:4157 (bound globally = true for ALL renders), :2666/:2670 (AJAX branch binds then Hashtbl.remove env.bindings "expand-components?")
  • what: http_mode binds expand-components? once globally so full-page worker renders expand components. The AJAX branch of http_render_page binds and then REMOVES it on the SAME shared env — so after any AJAX request the global binding is gone, and a concurrent (or subsequent) full-page aser render can observe it vanished (components silently not expanded), plus the remove races worker lookups. Confirmed the bind/remove-on-shared-env code path executes during live AJAX requests (the log shows "[sx-http] … (SX) aser=" for every AJAX hit); did not isolate a visibly-unexpanded component in output because the docs pages' aser output is SSR-expanded through a different path. Mechanism confirmed; symptom not pinned.

S4. Response cache stores soft error pages (routing failures cached as HTTP 200) — LIVE CONFIRMED

  • severity: medium
  • confidence: CONFIRMED (self-verified live) — was SUSPECTED
  • where: sx_server.ml:2635-2658 (error_page_ast returned as Some body), cached at :4312/:4677
  • what: A routing failure renders error_page_ast returned as a normal Some body and served with HTTP 200; the worker/ajax paths cache any Some body. So the not-found/error page is cached and served from cache to every subsequent visitor until restart. (Hard 500s from raised exceptions at :4315 are NOT cached; soft error pages ARE.) The "transient cross-service failure" sub-case (a fetch error rendered into the page, then cached) is reasoned from the same code path but needs the Python bridge to trigger, so it stays inferred.
  • repro (self-verified): GET /sx/<random-nonexistent-path> → HTTP 200 containing the "404"/not-found page; a 2nd GET of the same path returned byte-identical bytes served from cache — cold render 2.02s vs warm 0.0005s (~4000×), and the server log shows only ONE render line for that path.
  • severity: medium
  • confidence: CONFIRMED (self-verified live) — was SUSPECTED
  • where: sx_server.ml:4296/:4651-4654 (cache_key = path, +ajax:/htmx: prefix), :4649 (only sx-home-stepper bypasses the cache), :4331-4332 (query stripped before keying)
  • what: The cache key is the path only; cookies (except the hardcoded sx-home-stepper) and the query string are absent from it. Any page whose output varied by cookie/session/auth or query would be cached under a shared key and served across users. Benign for the current public docs (pages don't vary by cookie), but a real footgun for the cookie/auth-dependent Quart-replacement domains.
  • repro (self-verified): GET /sx/(geography.(hypermedia)) with three different Cookie: headers (different session= values) → three byte-identical responses from one cache entry; same path with ?u=1 vs ?u=2 → byte-identical (query stripped from key).

S8. Browser env missing spec platform constructors + a small primitive tail — latent break for shared modules

  • severity: medium
  • where: sx_browser.ml:626+ (91 server-only binds vs 22 browser-only; lists in scratchpad server-binds.txt / browser-binds.txt)
  • what: make-lambda/make-component/make-macro/make-thunk, now, json-encode, parse-safe, pretty-print, into are bound only in sx_server's env, absent client-side. Currently harmless (spec/evaluator.sx isn't shipped to the browser; the only consumer of the latter group is web/io.sx, not bundled) — but any shared .sx module that starts using them silently breaks in the browser only.

S9. Aser/SPA boosted-nav component expansion still fragile — hinges on boot-manifest eager-load fed by stale artifacts

  • severity: medium
  • where: web/adapter-sx.sx; sx-platform.js:649-660 (page-manifest boot array); lib/host/sx/relate-picker.sx; bundle.sh:74-77
  • what: The April blocker was fixed (ac65666f) and Jun-29 commits added server-embedded data-sx-manifest eager-loading, but the untracked Jun-30 spa-debug.js shows boosted-nav expansion of the picker was still being debugged the next day — and both the stale served compiler (C10) and stale module-manifest.sx (C11) sit directly on this code path.

S10. VM inline-resolve of IO inside HO-primitive callbacks cannot suspend/resume (async-IO correctness cliff)

  • severity: high
  • confidence: SUSPECTED (documented + code-confirmed; not reproducible through the harness — see C21)
  • where: hosts/ocaml/lib/sx_vm.ml:call_closure_reuse (~349-375); _cek_io_resolver installed at sx_server.ml:4166
  • what: When perform fires inside a JIT/VM HO-primitive callback (map/filter/reduce/for-each), the native OCaml loop sits between perform and resume, so it can't unwind-and-resume; it resolves IO inline via a local settle loop. The in-code comment states the alternative "would drop the remaining elements — corrupting the stack so the next CALL_PRIM sees wrong args." This works only because durable-KV reads are synchronous; a genuinely async IO op inside an HO callback on the serving path is not correctly supported. This is the same hazard class as the memory note about serving JIT dropping all-but-first in map/for-each.

S11. Server page routing evaluates the URL as SX — any env-bound function is URL-invokable

  • severity: medium
  • confidence: SUSPECTED
  • where: web/request-handler.sx (sx-url-to-expr, sx-auto-quote, sx-eval-page/call-page); driven by sx_server.ml:http_render_page:2620 (sx-handle-request)
  • what: sx-url-to-expr strips /sx/, splits on ., joins with spaces; call-page parses and invokes it. sx-auto-quote only quotes unbound symbols, so a bound symbol as list head is called with attacker-supplied (recursively evaluated) args. A path with parens (a legal path char) can reach (env-get env "some-fn") + apply for any render-env binding. cek-try swallows errors (→ nil/error page) limiting impact, but this is an SSR eval surface worth whitelisting to page:-prefixed bindings. Couldn't confirm whether the OCaml front door pre-sanitizes the path.

S12. Island hydration replaceChildren-then-render → hydration cursor reuses no SSR DOM

  • severity: low
  • confidence: SUSPECTED
  • where: web/boot.sx:hydrate-island (~333-350)
  • what: hydrate-island calls (host-call el "replaceChildren") (empties the element) then renders the body in sx-hydrating mode. But the hydration matcher reads childNodes.item(idx) on the just-emptied element, so it reuses nothing — effectively clear-and-fresh-render. If intended, the sx-hydrating scope push is dead work; if not, element-rooted islands never reuse SSR DOM. Not runnable through the DOM path to confirm.

S13. SSR/client parity relies on island body being pure over serialized kwargs (no dev-mode check)

  • severity: low
  • confidence: SUSPECTED
  • where: web/adapter-html.sx:render-html-island + serialize-island-state (608) vs web/boot.sx:hydrate-island
  • what: Only kwargs are serialized into data-sx-state; a body reading request-scoped/ non-deterministic IO (now, request-arg, random) or scope/context values not in kwargs produces SSR HTML differing from the client render → hydration mismatch. Inherent to the design, but there's no guard or dev-mode mismatch check and no test for a non-pure body.

S14. Deep (≥2-level) nested-list children flatten differently between HTML and aser

  • severity: low
  • confidence: SUSPECTED
  • where: spec/render.sx render-list path (recursive) vs web/adapter-sx.sx:aser-call/aser-fragment (one-level flatten)
  • what: HTML recursively flattens arbitrarily nested child lists to text; aser flattens one level and serializes deeper lists structurally. Single-level map output agrees (tested), but 2+-level raw nesting could serialize differently between modes. No test covers depth ≥ 2.

S-bridge. Python↔OCaml bridge desync on coroutine cancellation is unrecoverable (dead _restart, no timeouts)

  • severity: high
  • confidence: SUSPECTED (bridge-side reasoning; kernel-side stale-message mechanism self-verified)
  • where: shared/sx/ocaml_bridge.py:99 (_restart, 0 callers), :613 (_read_until_ok), :139 (async with self._lock)
  • what: render/eval/aser hold self._lock while blocked in _read_until_ok awaiting a kernel io-response. If the awaiting coroutine is cancelled (client disconnect / timeout), the async with releases the lock while the kernel is still blocked in read_io_response for the OLD epoch. The next coroutine acquires the lock and sends (epoch N+1)\n(command); the kernel — still inside read_io_response — consumes both lines as "stale messages" and keeps blocking, so the new command never runs and the new coroutine hangs on _readline forever. No asyncio.wait_for wraps bridge calls, and _restart() pipe-recovery is never invoked, so the bridge can't self-heal — it stays wedged until the process is killed. Under a shared multi-Domain HTTP server (S1/S2) this is a real liveness hazard.
  • repro (self-verified, kernel-side mechanism): printf '(epoch 1)\n(eval "(helper \"w\" 1)")\n(epoch 2)\n(eval "(+ 1 2)")\n' | timeout 20 hosts/ocaml/_build/default/bin/sx_server.exe(io-request 1 "helper" "w" nil), then [io] discarding stale message (…) ×2 eating the epoch-2 command, then the epoch-2 eval never runs.

S-bridge2. _parse_response treats a numeric result value as an epoch (ambiguity)

  • severity: low
  • confidence: CONFIRMED (agent)
  • where: shared/sx/ocaml_bridge.py:915, shared/sx/ocaml_sync.py:115
  • what: (ok EPOCH VALUE) parsing strips a leading number as the epoch, but the guard line[4:-1].isdigit() classifies (ok 3) as epoch-only-no-value and returns ('ok', None) instead of ('ok','3'). Latent (kernel always emits (ok EPOCH VALUE)), but the digit-sniffing epoch strip is fragile for any value that begins with a digit. test_ocaml_bridge.py has 5 live failures incl this.

S6. io_request-based primitives under eval can consume subsequent command lines

  • severity: low
  • where: sx_server.ml:617-669 (query/action/helper → blocking read_io_response), eval dispatch at :1816
  • what: query/action/helper block reading the next stdin line as an IO response; under a client that doesn't speak the IO sub-protocol, the next queued command is consumed → desync/hang.

S7. Multiple eval entry points with divergent IO semantics for the same source

  • severity: low
  • where: eval (:1816-1849, import+persist inline), load-source (:1772-1783, no IO handling), load (:1755, full eval_expr_io), http render (:2575, import→nil)
  • what: The same source behaves differently depending on the wrapping command — the known "unify eval/JIT paths" TODO surfacing as behavioral drift.

Notes / baseline caveats

  • 272/274 baseline run_tests failures are the in-progress hyperscript (hs-*) suites — guest-language project, not host defects. Undefined symbols: hs-is-set? (16), eval-hs-error (18), hs-ref-eq (4), host-hs-normalize-exc (2), plus DOM-mock assertion failures.
  • run_tests JIT is opt-in (--jit, run_tests.ml:4035). Baseline ran interpreter-only ([jit] calls=3045835 hit=0 miss=0 skip=3045835). A --jit differential run is in progress to diff pass/fail sets — results pending.
  • sx_scope.ml does not exist on this branch (only in sibling worktrees); memory/docs referencing it are stale for architecture. Scope prims live in sx_primitives.ml as process-globals (see S1/handoffs).

Handoffs to other lanes

  • core: served browser JIT compiler missing 921db09f's HO-loop desugar (see C10) — the browser-side --jit == CEK parity fix needs a re-sync + recompile pipeline fix.
  • conformance: module-manifest format split (C11) — regenerate .sx or point sx-platform.js at .json; until then hs-worker/hs-prolog never lazy-load client-side.
  • conformance: hosts/native has a working test nothing runs; test_platform.js needs web/signals.sxweb/web-signals.sx to restore the browser platform test tier.
  • hygiene: unify dist/ vs shared/static/wasm/ behind one sync script; delete the ignored hosts/ocaml/hosts/ nest, tracked stale hosts/ocaml/shared/static/wasm blobs, sx-platform-2.js, and (if reader-less) *.sxbc.json.
  • core (spec/render.sx parity): render-attrs renders non-BOOLEAN_ATTRS boolean-valued attrs as attr="true"/attr="false" while adapter-dom emits attr=""/omits — pick one contract and align all four adapters; add conformance cases (see C19).
  • conformance (spec/harness.sx): (1) log the IO call before invoking the mock so throwing mocks are recorded (C22); (2) add a harness mode that routes IO through real perform/cek-resume so the HO+perform element-drop cliff (S10) becomes testable (C21).
  • host (sx_vm.ml): add a targeted conformance test performing IO inside a JIT-compiled HO callback over a function-produced list — the inline-settle path is a correctness cliff for any future genuinely-async seam IO op (S10).
  • core: _scope_stacks / _request_cookies / _req_* being process-global means scope/provide/emit! and request primitives are not isolable per concurrent flow; spec scope semantics assume single-flow evaluation. Needs a per-flow story if the OCaml HTTP server stays multi-Domain.
  • core: eval vs load-source vs load vs http-render resolve import/durable IO differently — converge on one IO-resolution policy.
  • core/eval: scope/emit! appears to LEAK across invocations — (scope (emit! :k 1) (emit! :k 2) (len (emitted :k))) returns 2, 4, 6… cumulatively across epoch-server calls, with JIT disabled too (so not a VM bug — looks like a scope-stack state leak in scope-in-frames or the epoch env's scope stack). Flagged by the VM/JIT agent.
  • core/docs: let is SEQUENTIAL (let*) in BOTH CEK and VM ((let ((x 2) (y x)) y) → 2 with outer x=1) — contradicts the documented "let is parallel" authoring rule. Engines agree; docs don't.
  • core/spec: nested quasiquote evaluates inner unquotes at depth 2 on both OCaml and JS (`(a `(b ,(c))) errors) — nonstandard but consistent; needs a spec ruling.
  • core/spec: = deep-on-dicts, eq?/eqv?/equal?, upcase/downcase, json-encode, 1-arg range, empty-sep split, parse-int failure, get stored-nil-vs-default, (max), round half-negative, string-length units, exactness-across-wire — all need normative rulings; then each host aligned. OCaml wire float printing (%g) should be shortest-round-trip; OCaml sort numeric-compare Integer/Number; OCaml into/contains?-on-dict/merge-skip-nil made native+conformant; OCaml zip-pairs reconciled with its own spec.
  • conformance: NO differential CEK-vs-JIT parity suite exists — all four silent-wrong-value JIT bugs (J1 ->, J3 macro-arg, J4 component-kwargs, J5 stale opcodes) pass the existing tests. A harness that runs each conformance expr both interpreted and through a forced-JIT wrapper would catch them. Also add a cross-host differential battery (scratchpad battery*.json) — the spec corpus currently runs green only against OCaml; the JS bundle is 2490 failing and the Python standalone host can't load.
  • conformance: add a protocol-fuzz suite for the epoch loop (malformed line must not crash; (epoch)/(epoch foo); stray (io-response …); two-exprs-per-line).
  • conformance: no suite catches the r7rs string->number radix shadow — the spec test fails only in the OCaml runner env; JS/Python runner r7rs load status differs.

This is a reviewer's suggested reading order, NOT a verdict — severity/confidence tags on each finding are the ranking signal; a maintainer decides what blocks a release. No new claims here; every item points back to the evidence above.

Confirmed-severe clusters, in suggested blocker order:

  1. Production serving-JIT silently miscompiles (J1J8). Highest concern because it produces wrong values with no error on the live sx.rose-ash.com HTTP path (JIT hook is unconditional at sx_server.ml:4163 — see the reconciliation note; the "opt-in/default-off" belief does not apply to http_mode). J1 (-> in arg position) and J3 (macro args) return wrong results; J2 double-applies side effects on fallback (a data-integrity risk for persist/counter writes). Blast radius depends on whether rendered lambdas hit those patterns — a fleet audit of served pages, or disabling the http_mode JIT hook until fixed, is the pragmatic call. Fix lives in lib/compiler.sx (compile-thread-step at 934-951 for J1) + macro handling in the compiler/VM.

  2. Process-killing parse crash (C1 / C1b). One malformed or non-ASCII line on the command channel kills the whole sx_server process — it's the shared channel for the Python bridge and conformance runners. Small, localized fix (wrap the command-channel parse at sx_server.ml:5048/:4950 in a handler that catches Parse_error). Cheap to fix, high blast radius; good early win.

  3. HTTP serving path breaks under concurrency (S1 / S4 / S5). S1 crashed the live server intermittently (data race on unsynchronized cross-Domain Hashtbls, OOM ruled out) — needs either a lock around cache/env mutation or per-Domain state. S4 (error pages cached as 200) and S5 (cache key ignores cookies/query) are cheap keying fixes but S5 becomes a cross-user data-leak on any cookie/auth-varying domain, so fix it before the Quart-replacement domains go live on this server.

  4. Hollow JS bundle + red JS CI (C0a / C0b). Breaks conformance/CI, not visitors (the live site serves the wasm kernel). Fix is a define-library-aware extract_defines (hosts/javascript/platform.py:15) plus loading the newer lib deps + make-rtd/dom prims — or, if the JS host is being retired, stop CI gating on it. A product/ownership decision as much as a code fix.

Not-yet-symptomatic, needs a real app to exercise:

  • S2 (queued workers read process-global request context) and S3 (expand-components? removed on the shared env) are mechanism-confirmed but only exhibit a visible symptom on a full-page route that varies by request args/cookies. Re-run the S1-style concurrent-load repro against one of the real Quart-replacement domains (not the static docs site) to turn these CONFIRMED or clear them.

Cross-cutting, lower urgency:

  • Cross-host parity (P1P12) and the Python boundary/bridge issues (C24C31, S-bridge) matter most if the JS/Python hosts are still targets; if OCaml is the sole production evaluator, prioritize the OCaml-side ones that affect the wire (P1 lossy float, P9 dict order, P3 into).
  • Hygiene items (C8, C14C18) are safe cleanups, no behavior change.

Handoffs above route the core/conformance-owned items (scope leak, let-sequential docs, the missing CEK-vs-JIT differential suite) to the sibling review lanes — not this lane's to fix.