RUL-040 — verification scope is measured at symbol granularity; a full-suite escalation is a bug
Ruling
Mohamed, 2026-10-20, verbatim:
"never escalate=true is meaningless, it must be measured never the whole suite that is a huge bug"
"the code in history has a shared ledger, it should be at the function level and the blast radius should be at the method level with blast radius"
And, on the behaviour that prompted it — the coordinator had run a 401-module full tree in codetools
and then a full orchestrata suite because an oracle returned escalate: true:
"no, if the code didn't change why run the same test again?"
"LLM doesn't determine that, deterministic code through blast radius"
Three obligations follow:
escalate: trueis not a verdict. A boolean that discards the measured reasons computed beside it, and buys a ~30k-test run, is a defect. An unboundable change produces a NAMED verdict (dependency-or-environment-change:<path>) or a REFUSAL naming the missing fact and the set it could not bound — never a quiet full run that looks like diligence.- The blast radius is computed at METHOD level. A changed
def/method resolves to the test FUNCTIONS that reach it. Module-granularity import closure is the coarse fallback, not the answer. - The reuse ledger keys at FUNCTION level. One changed line in a 60-test module must not invalidate 60 recorded greens; a test function is reused when its own body and its own measured dependencies are unchanged.
- Scope is decided by deterministic code, never by the model's judgment. The coordinator queries the oracle and obeys it. "This might touch everything" is a heuristic where a derived answer was available, and is not a justification.
Evidence
Measured 2026-10-20 in orchestrata-ultimate (branch components) and orchestrata.codetools:
- The escalation arm exists and is consumed.
scripts/run_tests.py,elif resolved.get("escalate"):(cite by symbol — the concurrent T21/D7 packet moved it off line 1446 to ~1513) prints "⚠ --affected escalated to the FULL suite" and then asserts "escalated to the full suite — this IS the authoritative run". - The oracle's own history records the pathology.
scripts/affected_tests.py: "2026-08-11: a 2-file deploy/hook change escalated to the full 32k…"; "a 16-test change escalated to 33,104 tests, three of the five causes being…". A_UNTESTED_IS_NOT_A_FULL_SUITEprinciple already exists there. - Ledger granularity is module-level.
scripts/test_green_ledger.pykeys on module content (ledger.partition(all_modules, repo_root=…, path=…));reports/test-green-ledger.jsonwas 556,595 bytes at 2026-10-20 05:22. Observed reuse line: "♻ RUL-032: 43 module(s) unchanged since their last green run — reused, not re-run; 0 to run". escalate: truethrew away precision it had already computed. For a root-tests/conftest.pychange the oracle returnedescalate: truewith the reason "for the ROOT conftest that IS the whole suite; this is the honest blast radius" — while in the SAME response naming two exact edges (.envrc→tests/deploy/test_envrc_toolchain.py,pyproject.toml→tests/core/test_pyproject_contract.py) and certifying nine docs/context files "documentation with zero reference edges — certified inert, smoke floor only". Symbol-level measurement of that conftest change contradicts the whole-suite claim: the provenance block defines no symbol any test imports (grep for_ou_provenance|_expected_pkg|_actual_pkg|_direct_url_raw|_actual_origin|_expected_origin| _PROVENANCE_DISTacrosstests/outside conftest: zero hits), so its true radius is module-level session-start code, proven executed by any module that collects.- Cost of obeying the boolean anyway. A 401-module / 832 s codetools full-tree run to check one
conftest change, which also swept in 7
tests/realize/fixtures/**fixture-DATA projects (including askeletonwhose tests are meant to fail) and 6-j 8concurrency reds, generating ~13 spurious failures that cost several turns to triage. codetools has no oracle and no ledger at all (grep -c "ledger|reuse|unchanged since" scripts/run_tests_parallel.py→ 0), so nothing there enforces RUL-029 or RUL-032. - The machinery for the ruling already exists.
scripts/rts.pyhashes every def/class at any depth plus module residue and drives tabled selection under swipl;foundation/symbol_snapshot.pycarriesbody_hash/residue_hash;docs/ast-checkable-rules.md§4 already specifies the rung ladder (rung 1 symbol closure, rung 2 module closure, rung 3 dynamic-module fallbacks, rung 4 NAMED full verdicts, rung 5SelectionRefusednaming the missing fact). This is an adoption, not an invention.
Enforcement
- gate scripts/affected_tests.py RUL-040
- gate scripts/run_tests.py RUL-040
Judgment-only at recording time (2026-10-20). This is a recorded gap, not the intended end state —
RUL-040 is explicitly NOT a judgment ruling, it demands deterministic selection. It is declared none
only because scripts/rulings_lint.py verifies a declared gate actually carries the rule-id, and no
gate in the tree references RUL-040 yet. Each mechanism below replaces this line with its gate entry
when P-RUL040 Stage 1 lands, and the ruling is complete only when all four are in force.
Mechanism (owed, RUL-014)
Compliance of each candidate gate, measured 2026-10-20, stated honestly rather than implied:
scripts/rts.py— COMPLIES in substance, not yet wired. The method-level blast radius the ruling requires already exists: it hashes every def/class at any depth plus module residue (<mod>.<module>), drives tabled selection under swipl, and forces FULL-RUN on escapes. It is not the default path behindrun_tests.py --affected; ADR-0118 adjudicates it a MERGE-ADOPTION into codetools' richer siblings, not a file move.scripts/affected_tests.py— PARTIALLY COMPLIES, and is the open violation. It computes bounded sets with per-file named reasons and already certifies inertness mechanically ("documentation with zero reference edges — certified inert, smoke floor only"), but it still emitsescalate: bool, and its own docstring records the pathology ("a 16-test change escalated to 33,104 tests").scripts/test_green_ledger.py— PARTIALLY COMPLIES. The RUL-032 reuse ledger exists and works ("♻ RUL-032: 43 module(s) unchanged since their last green run — reused, not re-run") but keys on MODULE content (ledger.partition(all_modules, …);reports/test-green-ledger.json, 556,595 bytes), which this ruling requires at FUNCTION granularity.scripts/run_tests.py— VIOLATES. The consumption armelif resolved.get("escalate"):(cite by symbol; the concurrent T21/D7 packet moved it off line 1446 to ~1513) prints "escalated to the FULL suite" then asserts "this IS the authoritative run". Stage 1 removes it.
Status: OPEN VIOLATION until P-RUL040 Stage 1 lands. The packet
(plans/briefs/p-rul040-fail-closed-blast-radius.md) carries this ruling at its head, dated and quoted
verbatim, as the Stage 1 acceptance criteria: rungs 1–5 of docs/ast-checkable-rules.md §4 become the
selector's only outputs, the escalate arm goes, and the ledger re-keys to test-function granularity.
Companion rulings this one sharpens: RUL-029 (scope verification by query — this ruling specifies the granularity the query must reach, and bans the whole-suite answer), RUL-032 (tests run once per code state — this ruling moves "code state" from module content to function content plus measured method-level dependencies).
Verification
grep -n 'resolved.get("escalate")' scripts/run_tests.py→ no hits once Stage 1 lands (today: one).python scripts/affected_tests.py --since <base> --jsonemits noescalatekey, or emits it only as a NAMED rung-4 verdict / rung-5 refusal carrying the unboundable set.- The ledger's keys are test-function ids, not module paths: inspect
reports/test-green-ledger.json. - Regression proof of the founding incident: a root-
tests/conftest.pychange whose block defines no imported symbol selects ONE representative module per lane plus the named file-level edges, not the suite. The two edges the oracle already names for the 2026-10-20 change set (tests/deploy/test_envrc_toolchain.py,tests/core/test_pyproject_contract.py) are the expected bounded answer for that diff. - codetools gains an oracle or an explicit fail-closed refusal; until then,
scripts/run_tests_parallel.pyre-runs every module unconditionally and must not be pointed at a whole tree.