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

26 KiB
Raw Blame History

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!: 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 wordsdefine/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: letlet*; 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-parsesx-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 (J1J8 are preconditions, not rulings).