Orchestrata Ultimate › Plans › briefs › p-rul040-fail-closed-blast-radius
P-RUL040 — reuse ledger + blast radius accurate: fail-closed selection, never a silent full run
Source: plans/briefs/p-rul040-fail-closed-blast-radius.md at 8a2cb42ad · last changed 2026-10-20 · first committed 2026-10-20 · 147 lines.
Prose pending.
Plan text
P-RUL040 — reuse ledger + blast radius accurate: fail-closed selection, never a silent full run
Queue position 2 (handoff 2026-10-20 §3). Task ledger: the P-RUL040 entry. Contract:
docs/ast-checkable-rules.md §4 (the ruled mechanism: symbol-precise blast radius + fail-closed
"cannot bound, named set, explicit decision — never escalate to a full run") + ADR-0118's
adjudication that rts.py is a MERGE-ADOPTION into codetools' richer siblings, not a file move.
This is a TWO-STAGE brief. Stage 0 is measurement + proposals + an adoption map — NO code. The coordinator rules the seam and the adoption from your numbers; Stage 1 fences are named in the brief amendment AFTER the ruling. Do not write implementation code in Stage 0.
RULING INJECTED 2026-10-20 (Mohamed, verbatim) — binds the Stage 1 mechanism shape
"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"
Two granularity rulings, and they are the acceptance criteria for Stage 1, not preferences:
- The reuse ledger keys at FUNCTION level. Measured current state:
scripts/test_green_ledger.pykeys on MODULE content (ledger.partition(all_modules, repo_root=…, path=…);reports/test-green-ledger.json, 556,595 bytes at 2026-10-20 05:22), so one changed line in a 60-test module invalidates all 60 greens. A function-level key reuses every test function whose own body and whose own measured dependencies are unchanged. This is the "ledger-precision half" §"The measured landscape" below already names at line 43. - The blast radius is computed at METHOD level. Measured current state:
scripts/affected_tests.pyis module-granularity (its own docstring says so), which is why rung 1 symbol closure exists but is not what the module-level path uses. The radius must resolve a changeddef/method to the test FUNCTIONS that reach it —scripts/rts.pyalready hashes every def/class at any depth plus module residue, andfoundation/symbol_snapshot.pyalready carries body_hash/residue_hash, so this is an adoption of machinery that exists, not new invention. escalate: trueis not a verdict and must not survive as one. It is a boolean that discards the measured reasons beside it and buys a 30k-test run. Rung 4's NAMED full verdicts (dependency-or-environment-change:<path>) and rung 5'sSelectionRefusednaming the missing fact are the replacements; the consumption arm atelif resolved.get("escalate"):inrun_tests.py(cite by symbol — the concurrent T21/D7 packet moved it off line 1446 to ~1513) must go.
Cost of the status quo, measured this session (2026-10-20): a root-tests/conftest.py change produced
escalate: true whose own reason text read "for the ROOT conftest that IS the whole suite; this is the
honest blast radius". Symbol-level measurement of the same change contradicts it — 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_DIST across tests/ outside conftest: zero hits), so its
true radius is "module-level session-start code, proven executed by any module that collects", i.e. one
representative module per lane. The oracle meanwhile DID name the two real edges precisely
(.envrc → tests/deploy/test_envrc_toolchain.py, pyproject.toml →
tests/core/test_pyproject_contract.py) — so the precision exists at file granularity and is thrown
away by the escalate boolean. Stage 1 must make the ladder's rungs 1–5 the ONLY outputs.
The founding incidents
- The silent escalation:
scripts/run_tests.py's affected-selection escalates to a FULL suite run when it cannot bound a change — consumption point measured at theelif resolved.get("escalate"):arm (~line 1513 post-D7; cite by symbol, lines drift). RUL-040-to-be bans this as a silent path: an unboundable change must produce a REFUSAL naming the unboundable set + requiring an explicit recorded decision — never a quiet 18-minute full run that looks like diligence. - The dead run 906142's 181s
tests/bddspec_012 affected-scope failure (L23 triage): the affected machinery's own cost/accuracy class, assigned to this packet by the triage.
The measured landscape (Stage 0 inputs — verify each, do not trust)
- codetools ALREADY implements the philosophy:
src/codetools/deploy/affected.py(SPEC-012 §16, REQ-012-015/063-067): ADR-0024's ladder climbed "one exact step at a time, never defaulting to the full suite" — rung 1 symbol closure (coverage-context bound), rung 2 module closure (line residue, data_loaders non-.py references, conftest/importers), rung 3 dynamic-module fallbacks, rung 4 NAMED full verdicts (dependency-or-environment-change:<path>for pyproject/uv.lock/env/pytest-config), rung 5SelectionRefusedNAMING THE MISSING FACT (deploy/selection_refused.py). Plusescalation.py,affected_select.py,affected_lines.py,graph/blast.py+prolog/blast.pl(Prolog reachability, ADR-0014 retired the Python walk),foundation/symbol_snapshot.py(body_hash/residue_hash — the same byte-visibility contract rts.py carries). - orchestrata's halves:
scripts/rts.py(308 LOC:extract/selectverbs,sym_hash/2+file_hash/2+defines/2/calls/2/is_test/1/safety_escape/1fact schema, byte-complete segment hashing incl. the<mod>.<module>residue symbol, swipl-driven tabled selection, the non-.py safety escape forcing FULL-RUN) andscripts/affected_tests.py(module-granularity selector behindrun_tests.py --affected; does NOT import rts — measured zero linkage; the merge was recorded as a "Phase-7 handover slice" in plans/tooling-wiring.md:86).code_intel.py affected-symbols= the symbol-closure query over the Prolog KB. - The reuse ledger (RUL-032): run_tests.py's green ledger keys modules by content — measured in action today (13 rename modules reused, not re-run). §4's "reuse-ledger keys over module + import closure at SYMBOL granularity" is the ledger-precision half of this packet.
Stage 0 deliverables (report; no code)
M1 — the adoption map. Table: every rts.py behavior (each fact predicate, the byte-hashing contract, the safety escape, the select ladder) → the codetools rung/module that already owns it, or GAP (unique residue). Candidate conclusions to cost, not assume: is rts' unique residue only the standalone swipl CLI + the fact-schema interop surface? Does codetools' rung-2 data_loaders + rung-4 named-full already subsume the safety escape? Cite file:line on both sides.
M2 — the seam proposal. How should orchestrata's run_tests.py --affected consume
fail-closed selection? Cost each (lines of change, new dependencies, failure modes, latency):
(a) codetools as an orchestrata dev-group path/git dependency, importing deploy.affected;
(b) the deployed codetools CLI as a subprocess with a JSON contract;
(c) fact-file interop — orchestrata emits the diff/snapshot facts (rts' frozen sym_hash/2 +
file_hash/2 schema), codetools' ladder consumes; (d) port the REFUSAL SHAPE into
affected_tests.py natively (the ladder stays codetools'; orchestrata grows the named-set refusal
- recorded-decision seam without cross-repo code). State which the dead-run's 181s case and the escalation arm actually need — the ruling is the coordinator's, your job is honest costs + a recommendation.
M3 — the escalation anatomy. Trace resolved.get("escalate"): who computes resolved
(affected_tests.py? which function), every input that sets escalate, every consumer of the
full-run decision, and the exact points where a refusal-with-named-set could replace it. Include
the ledger interaction: what does a refused selection record (nothing? a named-refusal row?).
M4 — the ledger-precision half. What would symbol-granularity reuse keys require: measure the current ledger key shape (run_tests.py's reuse mechanism — module + content hash?), and what §4's "module + import closure at symbol granularity" adds; cost it against the body_hash/residue_ hash data ALREADY in plans/ast-ultimate.json and codetools' symbol_snapshot. Recommend a phase split if the full precision is a bigger packet than the escalation fix.
M5 — the 181s spec_012 case. Reproduce the shape from the dead-run evidence (reports/ +
L23): what made the bdd affected-scope selection cost 181s; does codetools' rung-3
dynamic-module handling (bdd scenarios()) already answer it; what orchestrata's selector lacks.
Stage 1 — RULED 2026-10-20 (coordinator adjudication appended to reports/p-rul040-stage0.md)
Stage 0 delivered and adjudicated: M1 adoption map accepted (citations spot-verified); M2's
option-(c) recommendation INVALIDATED by measurement (its "no new dependencies / codetools already
on sys.path" premise is false — zero codetools imports in the tree, no installed package, bin dirs
are not import paths). Seam ruling: option (b) — a codetools select-affected verb consumed as
a subprocess JSON contract from the DEPLOYED CLI (ADR-0117's consumption doctrine; a concrete
consumer now exists), per the adjudication's 5-point shape: verb contract (exit 0 bounded / exit 2
refusal-with-named-set / other non-zero tool error), orchestrata consumption in affected_tests.py +
the run_tests.py escalate-arm replacement (refusal → abort with named set + explicit-decision
citation mechanism; tool-missing → fail-closed naming the tooling state — NEVER a silent full run),
rts.py extract's fact schema frozen as the interop surface (<100 LOC JSON transform), rts select
RETIRED at verb landing, option (a) rejected (cross-repo import dependency = a boundary ruling not
made), option (d) rejected (ladder drift). Phase split adopted: Phase 1 = the refusal seam (the
2026-10-20 push-gate incident — 39,102-test named-full escalation collapsing into 1,957 ambiguous
"batch error" lines — joins the founding regressions: gate output must NAME the escalation cause
and budget state); Phase 2 (deferred, separately measured) = symbol-granularity ledger keys.
Stage-1 fences (two repos, sequenced — codetools verb FIRST, deploy, then orchestrata consumption): codetools: new files in the deploy lane + the verb wiring (coordinator serial-integration step for pyproject/console/deploy per ADR-0118 §4); orchestrata: scripts/affected_tests.py, scripts/run_tests.py (the escalate arm only), scripts/rts.py (select retirement + the JSON transform), new tests both sides. A Stage-1 worker brief is minted from this section + the adjudication when dispatch slots open (post-push, post-P-RUL039 — run_tests.py fence collision with P-RUL039 must serialize).
Report
M1-M5 with every claim carrying file:line on both repos, the two recommendations (seam, phase split) with arguments, anything you could not measure named. Read-only Stage 0: the ONLY file you write is your report (reports/p-rul040-stage0.md). RUL-037 applies to measurement too — no python -c/heredocs; use the AST toolkit, jq, grep.