# SX RULINGS — normative decisions on every ambiguity surfaced by the 2026-07-03 review

DRAFT for ratification. Each ruling: STATUS `PROPOSED` → flip to `RATIFIED` / `REJECTED` /
`AMENDED: <text>`. Once ratified, this file moves to `spec/RULINGS.md` and becomes the
authority the conformance batteries pin against. Evidence citations: core.md finding names,
hosts.md J/C/JS/P/S codes, conformance.md F codes.

**Default posture used for recommendations** (override per-ruling as you see fit):
1. Prefer an ERROR over any silent behavior (silent drop/no-op/misparse caused the worst findings).
2. Prefer R7RS/standard semantics where churn is low; prefer current-behavior-plus-documentation
   where churn is high and behavior is defensible.
3. Every ruling lands with conformance rows that run on BOTH production kernels (native + WASM).

**Companion decisions (not language rulings, restated for context):**
- D1 host lineup — recommended: kernel family (native OCaml + WASM) are the only evaluator
  targets; hosts/javascript and hosts/python standalone retired; shared/sx/parser.py shrunk to a
  wire-subset with a parity suite. Rulings below marked [D1] simplify to kernel-only if ratified.
- D3 gate — recommended: native corpus green (hs-upstream skip-listed) + same corpus on WASM +
  cross-kernel differential battery + CEK-vs-JIT differential (when JIT on) + sx_ref.ml regen diff.

---

## A. Bindings & scope

### R1. `set!` on an unbound name
- Current: silently creates a root binding (tested intent, test-scope.sx:196) — but BOTH spec docs
  say error (eval-rules.sx:112, special-forms.sx:141), and under JIT it writes a different global
  table than the interpreter (split brain).
- RECOMMENDATION: **ERROR** ("set!: <name> is not bound — use define"). Typo'd set! is a bug-hider;
  the docs already promise this. Flip test-scope.sx:196; sweep the corpus for reliance (expected
  small — the idiom is define-then-set!). Either way the JIT/interpreter split MUST die.
- Churn: low-medium. Findings: core set!-unbound; hosts J-globals split. STATUS: PROPOSED

### R2. The ~60 special-form/HO names (`map`, `filter`, `bind`, `match`, `do`, `case`, `->`, …)
- Current: `define`/`let`/`defmacro` of these names is silently accepted but ignored in call
  position (CEK); the VM honors them (J8) — worst of both worlds.
- RECOMMENDATION: **reserved words** — `define`/`let`/`set!`/`defmacro` of any dispatch-table name
  is a load-time ERROR. Publish the list in spec. Align the VM. (Full lexical honoring is more
  Schemely but taxes every list-head dispatch and rescues little real code.)
- Churn: low (error surfaces existing dead definitions). Findings: core unshadowable-names; J8. STATUS: PROPOSED

### R3. `let` semantics
- Current: sequential (`let*`), body = implicit begin, on BOTH engines (tested intent). CLAUDE.md
  island rules claim the opposite (describes a dead evaluator).
- RECOMMENDATION: **ratify current behavior**: `let` ≡ `let*`; body sequences. Fix CLAUDE.md.
  Document (or forbid) the observed letrec-ish quirk that binding-init lambdas capture the shared
  frame (`(let ((f (fn () a)) (a 5)) (f))` → 5).
- Churn: zero (docs only). Findings: core let-docs; hosts handoff let-sequential. STATUS: PROPOSED

### R4. `letrec`
- Current: parallel (all inits evaluated, then bound); read-before-init yields nil silently; PLUS
  two outright bugs (names injected into foreign lambdas' closures = global contamination;
  named-let loop name leaks into and clobbers the enclosing frame).
- RECOMMENDATION: **letrec\* semantics** (sequential init) with ERROR on read-before-init
  (pre-bind to an "uninitialized" sentinel that faults on read). Named-let binds its loop name in
  a fresh frame, invisible after the form. The closure-injection and frame-leak are bugs to fix
  regardless of ruling.
- Churn: low. Findings: core letrec-parallel/-injection/named-let. STATUS: PROPOSED

### R5. Component `&key` conventions
- Current: `:flag false` is coerced to nil (indistinguishable from omitted); trailing keyword with
  no value silently binds nil; `&key` on plain fn/defmacro silently misbinds.
- RECOMMENDATION: `false` is a legal &key value (bind via has-key, not `(or …)`); trailing keyword
  without a value = ERROR; `&key` in fn/defmacro either implemented identically to components or
  ERROR at definition (recommend: implement — one binding path for all three).
- Churn: low. Findings: core &key-false / trailing-kw / defmacro-&key. STATUS: PROPOSED

## B. Errors & conditions

### R6. Handler installation semantics
- Current: a handler runs with ITSELF still installed → any raise/error inside a guard clause or
  handler-bind handler loops forever (crit 1; WASM-verified).
- RECOMMENDATION: **R7RS/CL semantics** — handlers run with the OUTER handler set; guard clause
  bodies evaluate after the escape (the no-match auto-reraise path already does this correctly —
  make it the only path).
- Churn: zero for correct code (only un-hangs broken cases). Findings: crit 1, guard family. STATUS: PROPOSED

### R7. What is catchable
- Current: only guest `(raise …)` reaches guard; host primitive errors, undefined-symbol, arity
  errors, and strict type errors all blow through every handler.
- RECOMMENDATION: **everything is a condition.** Host/primitive/strict/undefined-symbol errors are
  raised as structured condition dicts ({:type :message :op …}) through the same channel guard
  sees. Reserve a non-catchable class only for kernel panics.
- Churn: low-medium (code that "relied" on uncatchability is unlikely). Findings: core
  host-errors-uncatchable, strict-uncatchable; enables sane server error pages. STATUS: PROPOSED

### R8. `raise-continuable` / `signal-condition`
- RECOMMENDATION: ratify R7RS: handler's value returns to the signal site (the current
  whole-program-result behavior is crit 2's frame-key bug, not a semantic choice). STATUS: PROPOSED

## C. Special forms

### R9. `cond` grammar — kill the dual-mode heuristic
- Current: flat pairs documented; undocumented Scheme clause mode auto-detected iff every arg is a
  2-element list → silent side-effect drops, mode flips, wrong values (core cond-ambiguity).
- RECOMMENDATION: **flat pairs only**: `(cond t1 r1 t2 r2 … :else d)`. Multi-expression results
  use explicit `(do …)`. Support arrow as a flat triple `t => receiver`. A clause-shaped arg list
  as a test position is just evaluated — no mode detection ever. Migrate the cond-arrow suite
  (test-r7rs.sx:135-145) and any clause-mode usage (sweep needed).
- Churn: medium (sweep + migrate clause-mode call sites). Findings: core cond-ambiguity,
  guard-multi-expr (inherits). STATUS: PROPOSED

### R10. `case`
- RECOMMENDATION: ratify the flat evaluated-datums form and document it (vals ARE evaluated,
  first-match, structural `=`); `:else`/`else` legal ONLY in final position (else ERROR); Scheme
  datum-list clause syntax → clear parse-time ERROR ("use flat pairs"). Keyword/string punning
  follows R21 and gets documented.
- Churn: low. Findings: core case-else-position / case-dialect. STATUS: PROPOSED

### R11. `do`
- Current: `do` is a begin-alias EXCEPT when its first form's head is a list — then it's a Scheme
  do-loop → IIFE misparse.
- RECOMMENDATION: **`do` = begin alias, always.** Scheme do-loop moves to a distinct name
  (`do-loop`) or is dropped (named let covers it). Kills the heuristic.
- Churn: low (sweep for real do-loop usage; expected rare). Findings: core do-IIFE. STATUS: PROPOSED

### R12. Quasiquote
- RECOMMENDATION, four sub-rulings:
  a. `unquote-splicing` becomes an alias of `splice-unquote` (one-line; kills the silent
     zero-splice trap; rename the misleadingly-named tests).
  b. Implement standard **depth tracking** (nested quasiquote raises quote depth; `,,x` works).
     Hosts agree current shallow behavior is consistent-but-nonstandard — fix at spec level.
  c. Quasiquote **traverses dict literals** (`{:k ,v}` works).
  d. Splicing a non-list and malformed splice arity → ERROR.
- Churn: low (b is the only subtle one). Findings: core qq-longhand/-depth/-dicts/-splice-nonlist. STATUS: PROPOSED

### R13. Threading `->` / `->>`
- RECOMMENDATION: (a) steps evaluate in CEK frames (bug: guard/IO broken through threading);
  (b) a lambda literal as a step = expand-time ERROR; (c) keyword step sugar: `(-> x :k :j)` ≡
  `(-> x (get :k) (get :j))` — cheap, expected, kills the `Not callable: nil` trap; (d) remove the
  dead `|>` dispatch branch (parser rejects `|` anyway); (e) fix reduce-seeding via R15.
- Churn: low. Findings: core threading-nested-CEK/-lambda-literal/|>-dead/keywords-as-getters. STATUS: PROPOSED

### R14. `match`
- RECOMMENDATION: `(pattern (when cond))` guard clauses either implemented or ERROR — never
  silently read as a structural pattern (current). Recommend: implement (small, high value).
  Document let-match as dict-destructuring-only with a clear error for list patterns.
- Churn: low. Findings: core match-guards. STATUS: PROPOSED

## D. Calling convention

### R15. Higher-order forms
- RECOMMENDATION, six sub-rulings:
  a. Arg-order swap happens ONLY when exactly one argument is callable (components count as
     callable); both-callable or neither → ERROR "map: cannot determine function/collection".
  b. `(reduce f coll)` (2-arg) = Clojure-style fold (first element as init, empty coll → error
     unless f has identity? keep simple: empty → ERROR); `(reduce init f coll)` and threaded
     `(-> init (reduce f coll))` work via the one-callable rule in (a).
  c. Data-first with extra args = ERROR (today silently dropped).
  d. Multi-collection map coerces every collection with seq-to-list (strings/vectors), zips to
     shortest (already); map over a dict iterates `(k v)` pairs.
  e. HO names are first-class: `map` etc. in value position resolve to real closures so
     `(define f map)` / `(apply map …)` work.
  f. Zero/one-arg HO calls = arity ERROR (today silently `()`).
  Also fix the O(n²) accumulation (implementation, not semantics).
- Churn: medium — (a) changes behavior for ambiguous calls, sweep needed. Findings: core reduce-2arg /
  reduce-swap / swap-drops-args / HO-not-first-class / ho-cryptic-errors / multi-coll / zero-arg;
  J7 (VM parity). STATUS: PROPOSED

### R16. `apply`
- Current: native never spreads; WASM spreads 2-arg; test runner has a third behavior (three-way
  divergence, F-3 + core corrected finding).
- RECOMMENDATION: **R7RS**: `(apply f a b … rest-list)` spreads, leading args prepended. All
  surfaces align; strict checks fire through apply (R25).
- Churn: low (today it mostly errors). STATUS: PROPOSED

### R17. Arity checking (too-few args)
- Current: missing params silently nil-fill (this is load-bearing: 1-arg `(assert x)` works only
  via nil-fill); too-many errors.
- RECOMMENDATION: **ERROR on too-few** as well, with `&optional`/`&key`/`&rest` as the explicit
  mechanisms. Sweep required (harness `assert`, any nil-fill reliance). If the sweep turns up
  heavy reliance, fallback position: keep nil-fill but document it loudly and make strict mode
  error. Primary recommendation stands: error.
- Churn: **high** — flagged as the riskiest ruling; do the sweep before ratifying. Findings: core
  strict-too-few / harness-assert nit. STATUS: PROPOSED

## E. Keywords, equality, types

### R18 (=R21 referenced above). Keywords
- RECOMMENDATION: ratify current model — keywords self-evaluate to their string name; keyword-ness
  exists only in unevaluated AST. Consequences made explicit: `(keyword-name :k)` needs a quote;
  `"keyword"` is REMOVED from the strict type system; case/dict punning documented. NOT callable
  (R13c covers the getter idiom).
- Churn: zero (docs + removing a dead type branch). STATUS: PROPOSED

### R19. Equality
- RECOMMENDATION (low-churn variant, chosen deliberately over full R7RS split):
  a. `=` stays deep structural equality (alias equal?) — ubiquitous in the corpus; add the missing
     **Char arm** (today `(= #\a #\a)` → false) and any other missing type arms; document that
     `(= 1 1.0)` → true (numeric value equality inside =).
  b. Add real `eqv?` (identity + exact numeric/char equality) and `eq?` (alias identical?) as
     kernel primitives — they are spec-declared today but implemented NOWHERE.
  c. Comparisons `< > <= >=` become n-ary chained (R7RS); `=` stays 2+-ary deep.
  d. If content-addressing ever needs exactness-distinguishing equality, that's `eqv?`, not `=`.
- Churn: low. Findings: core eq?/eqv?-missing, =-binary, char-equality; P5. STATUS: PROPOSED

### R20. Strict typing
- RECOMMENDATION: (a) checks move to the continue-with-call/vm_call chokepoints → HO callbacks,
  apply, components, => receivers all covered; (b) unknown type name at declaration = ERROR;
  (c) `"component"` becomes a real type branch; `"keyword"` removed (R18); (d) `(:as type)` param
  annotations become the declaration channel (deprecate the name-keyed global dict, which is
  trivially evaded and inherited by shadowers); (e) strict errors are catchable conditions (R7);
  (f) set-prim-param-types! merges and validates; (g) return types: explicitly out of scope now.
- Churn: low-medium. Findings: core strict-* family (8 findings). STATUS: PROPOSED

## F. Numbers

### R21. Integer model & overflow
- Current: native = int63 with overflow-promote-to-float on + and * but silent WRAP on expt;
  WASM = 32-bit silent wrap (F-1 — production browsers!); JS bundle = float64.
- RECOMMENDATION: spec defines SX integers as **exact within ±2^53** (the portable range);
  arithmetic that exceeds the host's exact range **promotes to float** (never wraps) — `expt`
  included. WASM must be fixed to match (js_of_ocaml int64/boxed or explicit overflow checks) —
  hosts lane feasibility-checks the mechanism; silent 32-bit wrap is a bug under any ruling.
  Values beyond 2^53 must not be trusted exact across the wire.
- Churn: low at spec level; WASM fix is real hosts work. Findings: F-1, P4, core expt. STATUS: PROPOSED

### R22. Division & zero
- RECOMMENDATION: integer `/`, `mod`, `quotient`, `remainder` by zero = catchable SX condition
  (today: raw OCaml Division_by_zero for mod/quotient, silent `inf` for /); float ops keep IEEE
  (inf/nan). `/` doc fixed: returns int when exact, float otherwise (current behavior ratified).
- Churn: low. Findings: core div-by-zero, /-doc. STATUS: PROPOSED

### R23. Float text & wire
- RECOMMENDATION: **shortest-round-trip printing everywhere** (native `%g` 6-sig-digit printing is
  a wire-corruption bug — P1); `inf`/`-inf`/`nan` are THE wire tokens on all hosts (P10); `round`
  stays half-away-from-zero, documented (R7RS banker's rejected: churn without benefit);
  `inexact->exact` rounding behavior kept + documented; `str 1.0` → keep `"1"` but canonical/wire
  serializers must preserve the float/int distinction (`1.0` serializes as `1.0`).
- Churn: low. Findings: P1, P10, core round/float-rendering; canonical CID determinism. STATUS: PROPOSED

### R24. Rationals
- RECOMMENDATION: `string->number` parses `"1/2"`; `(/ 1 3)` stays float (rationals remain opt-in
  via make-rational) — documented; radix arg restored by fixing the r7rs.sx shadow (C2).
- Churn: low. STATUS: PROPOSED

## G. Strings

### R25. Unit semantics
- Current: native counts UTF-8 bytes (substring can split codepoints → invalid UTF-8); JS counts
  UTF-16 units; constructors are codepoint-aware. Project style mandates UTF-8 text everywhere.
- RECOMMENDATION: **codepoint semantics** for length/substring/index/ref at the spec level; kernel
  implements UTF-8-aware ops. Accept the perf cost (or add byte-* variants for hot paths later).
- Churn: medium (kernel work + any code relying on byte counts). Findings: core UTF-8 family, P6. STATUS: PROPOSED

### R26. Case mapping
- RECOMMENDATION: kernel `upcase`/`downcase`/`upper`/`lower` are **ASCII-only, documented** (full
  Unicode case tables deferred; JS's full-Unicode behavior dies with D1). Aliases exist on all
  surfaces (P11).
- Churn: zero. STATUS: PROPOSED

### R27. `split` and escapes
- RECOMMENDATION: `split` = literal substring separator, keeps empties, empty separator → chars
  (ratifies native; pin with the multi-char test that history shows is needed). String escape
  table is normative: `\n \t \r \\ \" \uXXXX(validated: 4 hex digits, scalar value, else ERROR)`;
  **unknown escape = parse ERROR** (kills the native-keeps-backslash vs guest-drops-it silent
  divergence, C25 direction fight).
- Churn: low. Findings: core split note, \u family, unknown-escape divergence; C25. STATUS: PROPOSED

## H. Collections, nil, dicts

### R28. nil vs empty list
- Current: distinct values in the reader/serializer; `(cons 1 nil)` → `(1)` on native (nil-as-
  empty in constructors); read ops inconsistent (`first nil` → nil but `reverse nil` → error).
- RECOMMENDATION: nil and `()` remain **distinct values**; collection READ ops uniformly
  **nil-pun** (treat nil as empty: first/rest/nth/last/reverse/len/empty? all accept nil);
  constructors keep nil-as-empty seeding (cons/append onto nil). `nil?` ≠ `empty?` preserved.
- Churn: low (only un-errors cases). Findings: core nil-tolerance; P7/P8 arms. STATUS: PROPOSED

### R29. Dict ordering
- RECOMMENDATION: **insertion order preserved** — iteration, keys/vals, and serialization (OCaml
  Hashtbl replaced with an insertion-indexed structure; keys-reversed bug dies). CANONICAL form
  always sorts keys independently (already true in the CBOR/CID layer). Duplicate literal keys:
  last-wins, documented.
- EMPIRICAL NOTE (quick-wins batch, 2026-07-03): an interim sorted-keys change broke 4 render
  tests — attr emission order flows through dict_keys and the tests PIN source-order attributes
  (`width` before `height` etc.). So the current reverse-ish order is load-bearing for render;
  any change here must land together with the render-attr ordering contract. Reverted; do not
  change keys order except via this ruling.
- Churn: medium (kernel dict rework) but pays across wire/golden/cache findings C27/P9/core-keys. STATUS: PROPOSED

### R30. Small-primitive contract fixes (spec already says; hosts violate)
- RECOMMENDATION: ratify the spec text and fix: `contains?` on dicts = key check; `merge` skips
  nil; `into` native on the kernel; `sort` takes an optional comparator, compares int/float
  numerically, stable; `get` returns a STORED nil (default only when key absent); `zip-pairs` =
  sliding window per spec (kernel currently chunks); `(max)`/`(min)` zero-arg = ERROR.
- Churn: low each. Findings: core contains?/sort/keys; P2/P3/P8/P12; JS `get` arm. STATUS: PROPOSED

### R31. `append!` and mutation
- Current: silently no-ops on ANY derived list (map/filter/rest/reverse output) — worst silent-
  data-loss finding in the primitives sweep.
- RECOMMENDATION: `append!` **ERRORS on non-mutable lists** immediately (honest), and is
  deprecated in favor of persistent `append` + a real mutable vector/buffer for accumulator
  idioms. Sweep the corpus (it's a known accumulator idiom in loops).
- Churn: medium (idiom sweep). Findings: core append!. STATUS: PROPOSED

## I. Parser & wire

### R32. One token grammar
- RECOMMENDATION: publish the normative ident/number classifier in spec/parser.sx and make every
  surface bind THE SAME table (today: four divergent tables → same source, different ASTs).
  Specific token rulings: maximal-munch then classify (`1+`, `a,b` are symbols — ratifies native);
  hex/binary/octal `#x/#o/#b`-style and `0x10` accepted, documented; `inf`/`nan`/`-inf` are number
  literals (reserved, not idents); `1e` and other malformed numbers = parse ERROR (never nil);
  unicode identifiers **allowed** (UTF-8 letters — the docs mandate UTF-8 text; native reader
  extends its charset); `$`/`|` NOT ident chars; `.` IS a valid symbol (ratifies native; JS4 dies
  with D1); `#t`/`#f` = boolean literals on all surfaces.
- Churn: medium (native reader charset + guest table sync). Findings: core parser-divergence
  family; C1b (unicode symbol kills server — fixed by charset + C1 try-wrap); JS4. STATUS: PROPOSED

### R33. Reader extensibility & comments
- RECOMMENDATION: implement the `#name` reader-macro registry on the kernel (spec documents it;
  only JS has it today) — small, and sx-pub extensibility wants it. `#;` datum comment valid
  before `)` and at EOF (standard). `#|…|` stays a RAW STRING (documented loudly as not-a-block-
  comment); no block comments.
- Churn: low. Findings: core reader-macro/datum-comment/raw-string. STATUS: PROPOSED

### R34. Dict literals & serializer round-trip
- RECOMMENDATION: dict literal keys must be keyword/string/symbol — anything else is a parse ERROR
  on every parser (guest currently stringifies `{1 2}` silently); odd form count gets a "dict
  needs key-value pairs" error. Serializer: dict keys escaped/round-trippable (today unparseable
  output for non-ident keys — also a CID hazard); chars serialize by codepoint (`#\é` readable
  back once R25 lands); PROPERTY TEST: `parse(serialize(x)) = x` for the full value lattice, run
  on both kernels.
- Churn: low. Findings: core serializer-dict-keys / multibyte-chars / dict-edges. STATUS: PROPOSED

### R35. Canonical form & CIDs (sx-pub-critical)
- RECOMMENDATION: the **native CBOR/CID path is normative** (key-sorted, verified native==WASM,
  F-3). The canonical TEXT form is defined as: sorted keys, shortest-round-trip floats with
  preserved int/float distinction, fully-escaped strings, and is a fixed point
  (canonical(parse(canonical(x))) = canonical(x)) — property-tested cross-kernel. spec/canonical.sx
  either becomes a tested mirror of the native path (fix its runner-only helpers) or is deleted;
  two silently-diverging implementations is the one unacceptable state.
- Churn: low-medium. Findings: core canonical family; F-3; P9/C27 (via R29). STATUS: PROPOSED

### R40. Primitive naming & small-default unification (answers the hosts handoff list)
- RECOMMENDATION: one canonical name registry in spec/primitives.sx; per-host aliases die (with
  D1 most of these resolve to "make it native on the kernel"): `json-encode`/`json-parse` are
  KERNEL primitives (not IO-bridge helpers — today unavailable sandboxed); `regex-*` is the
  canonical family name; `parse`/`sx-parse` — `sx-parse` canonical, `parse` alias documented;
  1-arg `(range n)` = 0..n-1 (ratifies native); `parse-int`/`string->number` on failure → nil
  (ratifies native, never 0); `format` and the stdlib move for real (the primitives.sx header
  claims a stdlib migration that never happened — make the header true or revert it) and
  spec/stdlib.sx loads in production (today `format` is unresolved on the server).
- Churn: low. Findings: F-9 naming splits, P7 arms, core spec-drift / stdlib-header. STATUS: PROPOSED

## J. Render contracts

### R36. Attribute contract (all four adapters)
- RECOMMENDATION: one contract, HTML-mode's as base: boolean-registry attrs — false/nil omit,
  anything else emits bare name (SX truthiness, documented footgun stands); non-boolean attrs —
  value stringified INCLUDING `"true"`/`"false"` (DOM adapter aligns — C19/core, found by both
  lanes); attribute NAMES validated `[A-Za-z_:][A-Za-z0-9_:.-]*` else ERROR (kills spread-dict
  injection); nil attr value omits the attribute.
- Churn: low. Findings: core attr-name-injection / bool-footguns / dom-html-parity; C19. STATUS: PROPOSED

### R37. Raw-text elements & voids
- RECOMMENDATION: `<script>`/`<style>` children are NOT entity-escaped; instead the renderer
  ERRORS if content contains `</script`/`</style` (raw! unchanged as the explicit bypass) — HTML-
  correct and injection-safe, and stops corrupting legitimate inline JS/CSS. Void-element registry
  completed (area/base/embed/param/track added to HTML_TAGS); children passed to a void element =
  ERROR (today silently dropped). Component render gets a depth limit (default 512) with a clear
  error. `is-render-expr?` either wired in (html:/custom elements) or deleted.
- Churn: low. Findings: core script-style / void-elements / recursive-component / is-render-expr. STATUS: PROPOSED

### R38. aser wire format
- RECOMMENDATION: list-valued keyword args serialize quoted (`:items (quote (…))`) so the wire
  form re-evaluates to the same value; contract documented: components unexpanded, control flow
  evaluated, all VALUES must round-trip through parse+eval. Property-test aser output re-evaluation.
- Churn: low. Findings: core aser-list-kwargs; S14 (deep-nesting parity — add the depth-2 test). STATUS: PROPOSED

## K. Signals (spec-level semantics)

### R39. Reactive semantics
- RECOMMENDATION: (a) change detection by `=` (deep equality — needs R19's Char arm etc.), not
  physical identity — kills the every-reset-notifies behavior; (b) `batch` is unwind-safe
  (depth decremented on any exit — today one throw wedges all reactivity forever); (c) notify is
  glitch-free: two-phase (mark dirty → recompute in topological order) so diamonds recompute once;
  (d) dispose-computed actually unsubscribes (bug); (e) effect cleanup cleared after invocation
  (bug). Ratify as the documented reactive contract with tests (today: zero coverage of these).
- Churn: low-medium (topological notify is the real work). Findings: core signals family (5). STATUS: PROPOSED

---

## Ratification checklist

1. Flip each STATUS; AMENDED rulings get their text edited in place.
2. High-churn rulings needing a pre-ratification sweep: **R17 (arity)**, R9 (cond clause-mode
   usage), R31 (append! idiom), R15a (ambiguous HO calls).
3. On ratification: move to spec/RULINGS.md; every ruling becomes (a) a conformance battery row
   (native + WASM), (b) a fix ticket if behavior changes, (c) a docs line in CLAUDE.md's rewrite.
4. Rulings deliberately NOT made here (need your call, no strong recommendation):
   - Whether rationals should ever be the default result of exact `/` (R24 keeps float).
   - Whether to pursue full-Unicode case mapping (R26 defers).
   - Whether `do-loop` (R11) is worth keeping at all vs deleting Scheme do entirely.
   - JIT re-enable timeline (J1–J8 are preconditions, not rulings).
