From 2b935eb4991ce956c3c2e2dbe347bd9d33cf4295 Mon Sep 17 00:00:00 2001 From: David Christensen Date: Thu, 27 Aug 2026 17:20:50 -0700 Subject: [PATCH 1/8] docs: decide outranked scan fault reporting --- docs/adr/0043-report-outranked-scan-faults.md | 59 +++++++++++++++++ ...-27-report-outranked-scan-faults-design.md | 65 +++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 docs/adr/0043-report-outranked-scan-faults.md create mode 100644 docs/workflow/specs/2026-08-27-report-outranked-scan-faults-design.md diff --git a/docs/adr/0043-report-outranked-scan-faults.md b/docs/adr/0043-report-outranked-scan-faults.md new file mode 100644 index 0000000..8d17639 --- /dev/null +++ b/docs/adr/0043-report-outranked-scan-faults.md @@ -0,0 +1,59 @@ +# 0043 — Report outranked scan faults from the caller + +## Status + +Accepted (2026-08-27) + +## Context + +`renumbered_elsewhere` and `gate_existed_at` search several independent witnesses. A fault +from one candidate must not outrank a later positive result: a real renumber destination or +evidence that the gate existed remains valid. Both predicates therefore remember faults and +return success as soon as a positive result appears. That preserves the verdict but discards +the fact that part of the search did not run. + +[ADR 0005](0005-scan-faults-are-reported-not-collapsed.md) requires a predicate to return a +distinct fault value and its caller to report it. It does not define an outcome that combines +a positive answer with a reportable, outranked fault, and its error channel would incorrectly +turn an incomplete but decisive search into failure. + +## Decision + +An outranked scan fault is reportable. A multi-witness predicate that finds a positive result +after an earlier fault returns a distinct `positive-with-fault` status while preserving its +ordinary positive-result globals. Its caller emits one full-severity warning naming the failed +read and its exit status, then performs the same positive-result action it would have performed +for ordinary success. + +The two sites use rule-specific warning codes: `W-RENUMBER-SCAN` and +`W-GATE-WITNESS-SCAN`. A search with no positive result keeps ADR 0005's existing fault return +and error diagnostic. A search with no fault keeps its existing success or negative return. + +## Consequences + +The gate's exit status remains unchanged when a positive witness outranks a fault. Operators and +CI logs gain a stable trace showing that the answer was reached incompletely. Callers acquire a +fourth predicate outcome and must keep its warning and positive action adjacent so later edits do +not accidentally turn the warning into a failing verdict or omit the positive action. + +The warning is emitted with `warn_full`: an outranked external-read fault is independent of a +record's grandfathered shape and is not downgraded. The two mirrored gate scripts and their test +suites remain byte-identical. + +## Considered & rejected + +**Keep outranked faults silent.** judgment: observability; a correct verdict does not make an +external read that never ran irrelevant, and silence prevents an operator from distinguishing a +complete search from an incomplete one. + +**Return the existing fault status and fail the gate.** verified: issue #87 identifies fixtures +where a later candidate or witness supplies a genuine positive result; treating the unrelated +fault as decisive would reverse the established verdict rather than report how it was reached. + +**Emit the warning inside each predicate before returning success.** judgment: fit; it would +violate ADR 0005's caller-reporting boundary and make the predicate responsible for both search +and presentation. + +**Add a side-channel global while continuing to return ordinary success.** judgment: +maintainability; every caller would have to remember to inspect optional state after an +indistinguishable success, recreating the silent omission this decision is meant to prevent. diff --git a/docs/workflow/specs/2026-08-27-report-outranked-scan-faults-design.md b/docs/workflow/specs/2026-08-27-report-outranked-scan-faults-design.md new file mode 100644 index 0000000..b20b9c5 --- /dev/null +++ b/docs/workflow/specs/2026-08-27-report-outranked-scan-faults-design.md @@ -0,0 +1,65 @@ +# Report outranked scan faults — design + +Issue: #87. Decision: [ADR 0043](../../adr/0043-report-outranked-scan-faults.md). + +## Scope and outcome + +The record gate will preserve a positive result when one witness succeeds after another witness +faults, while reporting that incomplete search through a warning owned by the predicate's caller. +The change is limited to `renumbered_elsewhere` and `gate_existed_at`, their callers, mirrored +copies, and regression coverage. Other scan-fault sites remain owned by their existing issues. + +## Contract + +Both predicates retain their current three outcomes and add status `3` for +`positive-with-fault`: + +- `0`: positive result, complete search up to the decisive witness; +- `1`: negative result; +- `2`: fault with no positive result; and +- `3`: positive result after an earlier independent scan fault. + +For status `3`, the predicate preserves the same positive-result globals as status `0` plus the +existing fault-location and fault-status globals. The caller emits one `warn_full` diagnostic and +then executes its status-0 action. `renumbered_elsewhere` uses `W-RENUMBER-SCAN` before its +renumber note. `gate_existed_at` uses `W-GATE-WITNESS-SCAN` before the existing +`E-GATE-EMPTY-SET` result. Neither warning changes the process exit status. + +## Components and data flow + +`renumbered_elsewhere` remembers the first or latest candidate fault as it does today. When a +later canonical-content match is found, it returns `3` rather than `0` if a fault was remembered. +Its caller reports the saved `renumber_fault_path` and `path_exists_status`, then prints the +existing renumber note. + +`gate_existed_at` likewise returns `3` when a later filesystem or workflow witness succeeds after +a fault. `check_gate_files` reports the saved `gate_witness_path` and `path_exists_status`, then +emits the same empty-protected-set error used for an ordinary positive witness. + +The source and test mirrors under `.github/scripts/` and +`skills/tome-of-lore/assets/` are edited identically. The plugin patch version is bumped so the +change is installable. + +## Error handling and compatibility + +Status `2` remains the only no-answer fault and keeps its existing error diagnostic. Status `3` +is deliberately a warning because the positive evidence still decides the predicate. All shell +changes remain compatible with Bash 3.2 and use existing globals and emitters; no dependency or +public API is added. + +## Testing + +The existing `renumber_match_outranks_fault` fixture will assert exit 0, the real renumber note, +and `W-RENUMBER-SCAN` naming the failed candidate read and exit status. A new gate fixture will +make one `ls-tree` witness fault before a later real witness succeeds, assert the existing +`E-GATE-EMPTY-SET` verdict, and assert `W-GATE-WITNESS-SCAN` names the failed witness and status. + +Each new assertion must bite: temporarily neutralising the status-3 caller branch must make its +focused suite fail, after which the implementation is restored. `just records` proves fixture +behavior and mirror identity; `just verify` is the final repository guardrail. + +## Alternatives + +Keeping the fault silent, failing on it, reporting inside the predicate, and using an optional +success side channel are rejected in ADR 0043. The selected fourth outcome is the smallest design +that preserves both caller-owned reporting and the established positive verdict. From b743aac8fdd6ae0e65f3691864084013793b2ab1 Mon Sep 17 00:00:00 2001 From: David Christensen Date: Thu, 27 Aug 2026 17:23:21 -0700 Subject: [PATCH 2/8] docs: link current scan fault authority --- docs/adr/0043-report-outranked-scan-faults.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/adr/0043-report-outranked-scan-faults.md b/docs/adr/0043-report-outranked-scan-faults.md index 8d17639..8cdb32c 100644 --- a/docs/adr/0043-report-outranked-scan-faults.md +++ b/docs/adr/0043-report-outranked-scan-faults.md @@ -13,9 +13,11 @@ return success as soon as a positive result appears. That preserves the verdict the fact that part of the search did not run. [ADR 0005](0005-scan-faults-are-reported-not-collapsed.md) requires a predicate to return a -distinct fault value and its caller to report it. It does not define an outcome that combines -a positive answer with a reportable, outranked fault, and its error channel would incorrectly -turn an incomplete but decisive search into failure. +distinct fault value and its caller to report it. Although ADR 0005 is formally superseded, +[ADR 0024](0024-a-failing-repository-probe-is-not-evidence-of-absence.md) withdraws one +consequence while carrying its three decisions forward unchanged. Those decisions do not define +an outcome that combines a positive answer with a reportable, outranked fault, and the existing +error channel would incorrectly turn an incomplete but decisive search into failure. ## Decision @@ -51,8 +53,8 @@ where a later candidate or witness supplies a genuine positive result; treating fault as decisive would reverse the established verdict rather than report how it was reached. **Emit the warning inside each predicate before returning success.** judgment: fit; it would -violate ADR 0005's caller-reporting boundary and make the predicate responsible for both search -and presentation. +violate the caller-reporting boundary carried forward by ADR 0024 and make the predicate +responsible for both search and presentation. **Add a side-channel global while continuing to return ordinary success.** judgment: maintainability; every caller would have to remember to inspect optional state after an From 2bc29d788d5f55b5c40d78bbd576d86ead780726 Mon Sep 17 00:00:00 2001 From: David Christensen Date: Thu, 27 Aug 2026 17:26:31 -0700 Subject: [PATCH 3/8] docs: define retained scan fault order --- docs/adr/0043-report-outranked-scan-faults.md | 11 ++++++++--- .../2026-08-27-report-outranked-scan-faults-design.md | 7 +++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/adr/0043-report-outranked-scan-faults.md b/docs/adr/0043-report-outranked-scan-faults.md index 8cdb32c..43c0527 100644 --- a/docs/adr/0043-report-outranked-scan-faults.md +++ b/docs/adr/0043-report-outranked-scan-faults.md @@ -27,6 +27,10 @@ ordinary positive-result globals. Its caller emits one full-severity warning nam read and its exit status, then performs the same positive-result action it would have performed for ordinary success. +When several reads fault before the positive result, the predicate retains and reports the last +fault encountered. The warning is a trace that the search was incomplete, not an inventory of +every failed read; earlier faults are not reported individually. + The two sites use rule-specific warning codes: `W-RENUMBER-SCAN` and `W-GATE-WITNESS-SCAN`. A search with no positive result keeps ADR 0005's existing fault return and error diagnostic. A search with no fault keeps its existing success or negative return. @@ -34,9 +38,10 @@ and error diagnostic. A search with no fault keeps its existing success or negat ## Consequences The gate's exit status remains unchanged when a positive witness outranks a fault. Operators and -CI logs gain a stable trace showing that the answer was reached incompletely. Callers acquire a -fourth predicate outcome and must keep its warning and positive action adjacent so later edits do -not accidentally turn the warning into a failing verdict or omit the positive action. +CI logs gain a stable trace showing that the answer was reached incompletely, with deterministic +selection when several reads fault. Callers acquire a fourth predicate outcome and must keep its +warning and positive action adjacent so later edits do not accidentally turn the warning into a +failing verdict or omit the positive action. The warning is emitted with `warn_full`: an outranked external-read fault is independent of a record's grandfathered shape and is not downgraded. The two mirrored gate scripts and their test diff --git a/docs/workflow/specs/2026-08-27-report-outranked-scan-faults-design.md b/docs/workflow/specs/2026-08-27-report-outranked-scan-faults-design.md index b20b9c5..226b031 100644 --- a/docs/workflow/specs/2026-08-27-report-outranked-scan-faults-design.md +++ b/docs/workflow/specs/2026-08-27-report-outranked-scan-faults-design.md @@ -25,6 +25,10 @@ then executes its status-0 action. `renumbered_elsewhere` uses `W-RENUMBER-SCAN` renumber note. `gate_existed_at` uses `W-GATE-WITNESS-SCAN` before the existing `E-GATE-EMPTY-SET` result. Neither warning changes the process exit status. +If multiple reads fault before the positive result, the existing fault globals retain the last +fault encountered. The warning reports that fault only; it signals an incomplete search rather +than inventorying every failed read. + ## Components and data flow `renumbered_elsewhere` remembers the first or latest candidate fault as it does today. When a @@ -53,6 +57,9 @@ The existing `renumber_match_outranks_fault` fixture will assert exit 0, the rea and `W-RENUMBER-SCAN` naming the failed candidate read and exit status. A new gate fixture will make one `ls-tree` witness fault before a later real witness succeeds, assert the existing `E-GATE-EMPTY-SET` verdict, and assert `W-GATE-WITNESS-SCAN` names the failed witness and status. +At one site, the fixture will fault two reads before the positive result and assert that the +warning names the last fault, pinning the deterministic selection rule without duplicating the +same policy test at both callers. Each new assertion must bite: temporarily neutralising the status-3 caller branch must make its focused suite fail, after which the implementation is restored. `just records` proves fixture From e1829604eebab9854dbeb7f63c892c2adbd0ac26 Mon Sep 17 00:00:00 2001 From: David Christensen Date: Thu, 27 Aug 2026 17:30:08 -0700 Subject: [PATCH 4/8] docs: plan outranked scan fault reporting --- ...2026-08-27-report-outranked-scan-faults.md | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 docs/workflow/plans/2026-08-27-report-outranked-scan-faults.md diff --git a/docs/workflow/plans/2026-08-27-report-outranked-scan-faults.md b/docs/workflow/plans/2026-08-27-report-outranked-scan-faults.md new file mode 100644 index 0000000..6c264aa --- /dev/null +++ b/docs/workflow/plans/2026-08-27-report-outranked-scan-faults.md @@ -0,0 +1,110 @@ +# Report outranked scan faults — implementation plan + +**Issue:** #87 +**Decision:** [ADR 0043](../../adr/0043-report-outranked-scan-faults.md) +**Spec:** [design](../specs/2026-08-27-report-outranked-scan-faults-design.md) +**Branch:** `feat/report-outranked-scan-faults-87` +**BASE_BRANCH:** `main` + +## Global constraints + +- Shell remains compatible with Bash 3.2, uses tab indentation, `#!/usr/bin/env bash`, and + `set -euo pipefail` where a file already carries them. +- Use only existing shell facilities and helpers; add no dependency or public API. +- `.github/scripts/check-records.sh` stays byte-identical to + `skills/tome-of-lore/assets/check-records.sh`; their `check-records-test.sh` suites also stay + byte-identical. +- Status `3` means `positive-with-fault`. It reports the last fault encountered, emits a warning, + and preserves the status-0 action and process verdict. +- Exact guardrails: focused `just test check-records`; mirrored record gate `just records`; full + repository suite `just verify`. CI invokes the same suite through `just ci`. +- Host architecture is `x86_64`; no target architecture is declared; relationship is + `no-target-declared`. The host uses GNU userland. Repository targets are not inferred from it. +- ADR index coupling is `not coupled`: `docs/adr/README.md` is deliberately a directory policy, + not an index table. + +## Task 1 — add failing regression coverage + +**Files:** modify `.github/scripts/check-records-test.sh`; mirror the exact file to +`skills/tome-of-lore/assets/check-records-test.sh` only after the focused test is red. + +**Interfaces:** consumes existing helpers `write_ls_files_stub`, `run_case`, `expect_match`, +`expect_error_code`, and the `renumber_match_outranks_fault` fixture. Produces assertions for +`W-RENUMBER-SCAN`, `W-GATE-WITNESS-SCAN`, last-fault selection, and unchanged positive actions; +Task 2 must satisfy them without changing their expected codes. + +1. Extend `renumber_match_outranks_fault` so two earlier candidate index reads fault with + distinguishable paths before `docs/debt/0006-b.md` matches. Keep expected exit `0`, then assert + `.err` contains `::warning::W-RENUMBER-SCAN:`, names the later faulting candidate and its exit, + and `.out` still names the real renumber destination. +2. Add a gate fixture whose protected set is empty, whose first two `ls-tree` witnesses fault with + distinguishable paths, and whose later workflow witness finds the former gate basename. Assert + exit `1`, `E-GATE-EMPTY-SET`, `W-GATE-WITNESS-SCAN` naming the second fault, and absence of + `E-GATE-WITNESS-SCAN`. +3. Run `just test check-records` bare. Expected: non-zero, with the first new warning assertion + failing because neither caller emits the new code. This red result is the TDD proof; do not + alter production code before observing it. +4. Apply the same completed edits to the skill asset mirror and confirm + `cmp -s .github/scripts/check-records-test.sh skills/tome-of-lore/assets/check-records-test.sh`. + +**Acceptance:** both fixtures reach a later positive witness after earlier injected faults; the +focused suite is demonstrably red for the missing warnings; both test mirrors are identical. + +**Rollback:** revert only the two fixture edits; no scratch tree is retained by a completed suite. + +## Task 2 — implement the fourth predicate outcome + +**Files:** modify `.github/scripts/check-records.sh`; mirror it exactly to +`skills/tome-of-lore/assets/check-records.sh`. + +**Interfaces:** consumes Task 1's exact warning codes and ADR 0043's status-3 contract. Preserves +`renumbered_to`, `renumber_fault_path`, `gate_witness_path`, and `path_exists_status` for callers. +Produces status `3` from `renumbered_elsewhere` and `gate_existed_at`; Task 3 relies on both script +mirrors being identical and the focused suite being green. + +1. In each predicate's positive-result branch, return `3` when `fault_status` is non-zero and + return `0` otherwise. Before returning `3`, copy the retained `fault_status` to + `path_exists_status`; the location global already names the last fault because every fault + assignment overwrites it. +2. In the `renumbered_elsewhere` caller, add a status-3 case that emits + `warn_full "W-RENUMBER-SCAN: $record: found renumber destination $renumbered_to after an incomplete search (could not read $renumber_fault_path, exit $path_exists_status)"` + and then prints the same renumber note as status 0. +3. In the `gate_existed_at` caller, add a status-3 case that emits + `warn_full "W-GATE-WITNESS-SCAN: $gate_witness_path: a later witness established that a gate existed at $base after this read failed (git exit $path_exists_status)"` + and then emits the same `E-GATE-EMPTY-SET` error as status 0. +4. Apply the same production edits to its skill asset mirror. Run + `cmp -s .github/scripts/check-records.sh skills/tome-of-lore/assets/check-records.sh`. +5. Run `just test check-records` bare. Expected: exit 0, the suite's `0 failed` summary, and + `test: 1 suites passed`. + +**Acceptance:** status 3 preserves the positive verdict/action; one full-severity warning reports +the last retained fault; ordinary statuses 0, 1, and 2 retain their behavior; mirrors match. + +**Rollback:** revert the caller and predicate cases together; a partial rollback would leave a +status value without a consumer or a consumer no predicate can reach. + +## Task 3 — prove the tests bite and finish repository integration + +**Files:** modify `.claude-plugin/plugin.json`; no other source file is expected. + +**Interfaces:** consumes Task 2's green focused suite and identical mirrors. Produces installable +plugin version `2.12.1` and final guardrail evidence for delivery. + +1. Temporarily neutralise only the `renumbered_elsewhere` status-3 caller warning. Run + `just test check-records` bare and require non-zero with the new `W-RENUMBER-SCAN` assertion + failing. Restore the exact production line without committing the mutation. +2. Temporarily neutralise only the `gate_existed_at` status-3 caller warning. Run + `just test check-records` bare and require non-zero with the new `W-GATE-WITNESS-SCAN` + assertion failing. Restore the exact production line without committing the mutation. +3. Run `just test check-records` again. Expected: exit 0 with every suite assertion passing. +4. Change `.claude-plugin/plugin.json` version from `2.12.0` to `2.12.1`; this is a PATCH because + no invocation contract or capability is added. +5. Run `git diff --check`, then `just records`, then `just verify`, all bare. Expected: exit 0; + record mirrors compare byte-identically; all record tests pass; all repository gates and prek + checks pass without warnings. + +**Acceptance:** both controlled faults turn the focused suite red; the restored implementation is +green; version `2.12.1` is parseable and greater than the base version; full guardrails pass. + +**Rollback:** restore `2.12.0` only when reverting the entire branch. Never leave implementation +changes at the old version because installed plugin caches would not receive them. From a6f4555d5c4868dff9311f3cf7b913c84738635b Mon Sep 17 00:00:00 2001 From: David Christensen Date: Thu, 27 Aug 2026 17:32:03 -0700 Subject: [PATCH 5/8] docs: publish retained renumber fault path --- .../plans/2026-08-27-report-outranked-scan-faults.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/workflow/plans/2026-08-27-report-outranked-scan-faults.md b/docs/workflow/plans/2026-08-27-report-outranked-scan-faults.md index 6c264aa..b76cc80 100644 --- a/docs/workflow/plans/2026-08-27-report-outranked-scan-faults.md +++ b/docs/workflow/plans/2026-08-27-report-outranked-scan-faults.md @@ -36,7 +36,8 @@ Task 2 must satisfy them without changing their expected codes. 1. Extend `renumber_match_outranks_fault` so two earlier candidate index reads fault with distinguishable paths before `docs/debt/0006-b.md` matches. Keep expected exit `0`, then assert `.err` contains `::warning::W-RENUMBER-SCAN:`, names the later faulting candidate and its exit, - and `.out` still names the real renumber destination. + and `.out` still names the real renumber destination. The path assertion specifically proves + the predicate copied its local `fault_path` into caller-visible `renumber_fault_path`. 2. Add a gate fixture whose protected set is empty, whose first two `ls-tree` witnesses fault with distinguishable paths, and whose later workflow witness finds the former gate basename. Assert exit `1`, `E-GATE-EMPTY-SET`, `W-GATE-WITNESS-SCAN` naming the second fault, and absence of @@ -63,9 +64,10 @@ Produces status `3` from `renumbered_elsewhere` and `gate_existed_at`; Task 3 re mirrors being identical and the focused suite being green. 1. In each predicate's positive-result branch, return `3` when `fault_status` is non-zero and - return `0` otherwise. Before returning `3`, copy the retained `fault_status` to - `path_exists_status`; the location global already names the last fault because every fault - assignment overwrites it. + return `0` otherwise. In `renumbered_elsewhere`, copy both local retained values before return: + `path_exists_status=$fault_status` and `renumber_fault_path=$fault_path`. In + `gate_existed_at`, copy `fault_status` to `path_exists_status`; its caller-visible + `gate_witness_path` already changes with every fault. 2. In the `renumbered_elsewhere` caller, add a status-3 case that emits `warn_full "W-RENUMBER-SCAN: $record: found renumber destination $renumbered_to after an incomplete search (could not read $renumber_fault_path, exit $path_exists_status)"` and then prints the same renumber note as status 0. From 0297672ae786f34c237f34d120c11cfbf7fd28df Mon Sep 17 00:00:00 2001 From: David Christensen Date: Thu, 27 Aug 2026 17:33:47 -0700 Subject: [PATCH 6/8] docs: invoke the record suite directly --- ...2026-08-27-report-outranked-scan-faults.md | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/docs/workflow/plans/2026-08-27-report-outranked-scan-faults.md b/docs/workflow/plans/2026-08-27-report-outranked-scan-faults.md index b76cc80..7dfaff0 100644 --- a/docs/workflow/plans/2026-08-27-report-outranked-scan-faults.md +++ b/docs/workflow/plans/2026-08-27-report-outranked-scan-faults.md @@ -16,8 +16,9 @@ byte-identical. - Status `3` means `positive-with-fault`. It reports the last fault encountered, emits a warning, and preserves the status-0 action and process verdict. -- Exact guardrails: focused `just test check-records`; mirrored record gate `just records`; full - repository suite `just verify`. CI invokes the same suite through `just ci`. +- Exact guardrails: focused `./.github/scripts/check-records-test.sh`; mirrored record gate + `just records`; full repository suite `just verify`. CI invokes the same suite through + `just ci`. - Host architecture is `x86_64`; no target architecture is declared; relationship is `no-target-declared`. The host uses GNU userland. Repository targets are not inferred from it. - ADR index coupling is `not coupled`: `docs/adr/README.md` is deliberately a directory policy, @@ -42,9 +43,9 @@ Task 2 must satisfy them without changing their expected codes. distinguishable paths, and whose later workflow witness finds the former gate basename. Assert exit `1`, `E-GATE-EMPTY-SET`, `W-GATE-WITNESS-SCAN` naming the second fault, and absence of `E-GATE-WITNESS-SCAN`. -3. Run `just test check-records` bare. Expected: non-zero, with the first new warning assertion - failing because neither caller emits the new code. This red result is the TDD proof; do not - alter production code before observing it. +3. Run `./.github/scripts/check-records-test.sh` bare. Expected: non-zero, with the first new + warning assertion failing because neither caller emits the new code. This red result is the + TDD proof; do not alter production code before observing it. 4. Apply the same completed edits to the skill asset mirror and confirm `cmp -s .github/scripts/check-records-test.sh skills/tome-of-lore/assets/check-records-test.sh`. @@ -76,8 +77,8 @@ mirrors being identical and the focused suite being green. and then emits the same `E-GATE-EMPTY-SET` error as status 0. 4. Apply the same production edits to its skill asset mirror. Run `cmp -s .github/scripts/check-records.sh skills/tome-of-lore/assets/check-records.sh`. -5. Run `just test check-records` bare. Expected: exit 0, the suite's `0 failed` summary, and - `test: 1 suites passed`. +5. Run `./.github/scripts/check-records-test.sh` bare. Expected: exit 0 and the suite's + `0 failed` summary. **Acceptance:** status 3 preserves the positive verdict/action; one full-severity warning reports the last retained fault; ordinary statuses 0, 1, and 2 retain their behavior; mirrors match. @@ -93,12 +94,15 @@ status value without a consumer or a consumer no predicate can reach. plugin version `2.12.1` and final guardrail evidence for delivery. 1. Temporarily neutralise only the `renumbered_elsewhere` status-3 caller warning. Run - `just test check-records` bare and require non-zero with the new `W-RENUMBER-SCAN` assertion - failing. Restore the exact production line without committing the mutation. + `./.github/scripts/check-records-test.sh` bare and require non-zero with the new + `W-RENUMBER-SCAN` assertion failing. Restore the exact production line without committing the + mutation. 2. Temporarily neutralise only the `gate_existed_at` status-3 caller warning. Run - `just test check-records` bare and require non-zero with the new `W-GATE-WITNESS-SCAN` - assertion failing. Restore the exact production line without committing the mutation. -3. Run `just test check-records` again. Expected: exit 0 with every suite assertion passing. + `./.github/scripts/check-records-test.sh` bare and require non-zero with the new + `W-GATE-WITNESS-SCAN` assertion failing. Restore the exact production line without committing + the mutation. +3. Run `./.github/scripts/check-records-test.sh` again. Expected: exit 0 with every suite assertion + passing and the summary reporting `0 failed`. 4. Change `.claude-plugin/plugin.json` version from `2.12.0` to `2.12.1`; this is a PATCH because no invocation contract or capability is added. 5. Run `git diff --check`, then `just records`, then `just verify`, all bare. Expected: exit 0; From 773e149e979486d3e913412265b90c487cc7c02f Mon Sep 17 00:00:00 2001 From: David Christensen Date: Thu, 27 Aug 2026 17:55:11 -0700 Subject: [PATCH 7/8] fix: report outranked scan faults --- .claude-plugin/plugin.json | 2 +- .github/scripts/check-records-test.sh | 85 ++++++++++++++++++- .github/scripts/check-records.sh | 35 +++++++- .../tome-of-lore/assets/check-records-test.sh | 85 ++++++++++++++++++- skills/tome-of-lore/assets/check-records.sh | 35 +++++++- 5 files changed, 231 insertions(+), 11 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index ece7a70..99ebd5f 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "adept", - "version": "2.12.0", + "version": "2.12.1", "description": "Development-workflow skills: design, TDD, adversarial review, shipping, and campaign orchestration for Claude Code and Codex.", "author": { "name": "David Christensen" diff --git a/.github/scripts/check-records-test.sh b/.github/scripts/check-records-test.sh index 2bba930..5f1f992 100755 --- a/.github/scripts/check-records-test.sh +++ b/.github/scripts/check-records-test.sh @@ -1347,6 +1347,61 @@ STUB expect_error_code "$d/.e3" E-GATE-WITNESS-SCAN fi + # A later positive witness keeps the undeclared-rename verdict, but the earlier fault still + # needs a trace. The unrelated workflow names debt.sh, a successor whose basename did not + # change, so gate_paths does not treat that workflow as protected; gate_existed_at's broader + # closed witness set still sees it after the SELF_DIR probe for debt.sh faults. + d="$SCRATCH/gate_witness_outranks_fault" + mkdir -p "$d/docs/debt" "$d/.github/scripts/profiles" "$d/.github/workflows" + git -C "$d" init -q . + git -C "$d" config user.email test@example.invalid + git -C "$d" config user.name "check-records test" + cat >"$d/.github/workflows/unrelated.yml" <<'YAML' +name: unrelated +on: push +jobs: + note: + runs-on: ubuntu-latest + steps: + - run: echo debt.sh +YAML + git -C "$d" add -A + git -C "$d" commit -qm "before the gate" + b=$(base_of "$d") + cp "$SCRIPT_DIR/check-records.sh" "$d/.github/scripts/" + cp "$SCRIPT_DIR/profiles/debt.sh" "$d/.github/scripts/profiles/" + chmod +x "$d/.github/scripts/check-records.sh" + write_record "$d" "0001-first.md" + git -C "$d" add -A + stub_bin="$SCRATCH/git-witness-outrank-bin" + mkdir -p "$stub_bin" + cat >"$stub_bin/git" <&2 + exit 128 + fi + done +fi +exec "$real_git" "\$@" +STUB + chmod +x "$stub_bin/git" + run_case "a real gate witness outranks an earlier fault" 1 E-GATE-EMPTY-SET "$d" \ + BASE_SHA="$b" PATH="$stub_bin:$PATH" + printf ' %-4s %-44s ' "" "the outranked gate fault is reported" + expect_match "$d/.err" 'W-GATE-WITNESS-SCAN beside the verdict' \ + 'the incomplete gate-witness search was silent' '::warning::W-GATE-WITNESS-SCAN: ' + printf ' %-4s %-44s ' "" "the fault path and status are retained" + expect_match "$d/.err" 'gate fault path and status named' \ + 'the gate warning did not name the failed witness' \ + '.github/scripts/debt.sh: .*git exit 128' + printf ' %-4s %-44s ' "" "the no-answer scan error stays suppressed" + expect_no_match "$d/.err" 'E-GATE-WITNESS-SCAN suppressed' \ + 'the positive witness was replaced by the no-answer fault' \ + '::error::E-GATE-WITNESS-SCAN: ' + # gate_existed_at has two witnesses, and `renamed_gate` above happens to satisfy both at # once (its base-ref workflow names "check-records.sh", which is also the literal # basename sitting in SELF_DIR at that ref) — so neither witness is individually proven. @@ -2044,11 +2099,39 @@ STUB b=$(base_of "$d") git -C "$d" mv docs/debt/0005-b.md docs/debt/0006-b.md write_record "$d" "0002-c.md" + write_record "$d" "0003-d.md" git -C "$d" add -A stub_bin="$SCRATCH/git-renumber-outrank-bin" - write_ls_files_stub "$stub_bin" docs/debt/0002-c.md + mkdir -p "$stub_bin" + real_git=$(command -v git) + cat >"$stub_bin/git" <&2 + exit 128 + ;; + esac + done +fi +exec "$real_git" "\$@" +STUB + chmod +x "$stub_bin/git" run_case "a real renumber outranks a candidate fault" 0 - "$d" \ BASE_SHA="$b" PATH="$stub_bin:$PATH" + printf ' %-4s %-44s ' "" "the outranked fault is reported" + expect_match "$d/.err" 'exit=0 W-RENUMBER-SCAN' \ + 'the incomplete renumber search was silent' '::warning::W-RENUMBER-SCAN: ' + printf ' %-4s %-44s ' "" "the last candidate fault is retained" + expect_match "$d/.err" 'last fault path and status named' \ + 'the warning did not name the last candidate fault' \ + 'could not read the index entry for docs/debt/0003-d.md, exit 128' + printf ' %-4s %-44s ' "" "the positive renumber action survives" + expect_match "$d/.out" 'renumber destination still reported' \ + 'the positive renumber result was lost' \ + 'note: docs/debt/0005-b.md was renumbered to docs/debt/0006-b.md' # check_gate_files, on the gate file itself. A gate file present and tracked read as removed, # which reported E-GATE-GONE -- the gate accusing the change of deleting a file that is diff --git a/.github/scripts/check-records.sh b/.github/scripts/check-records.sh index ec19ebe..19d8726 100755 --- a/.github/scripts/check-records.sh +++ b/.github/scripts/check-records.sh @@ -596,6 +596,11 @@ $candidate" else used_renumber_targets=$candidate fi + if [ "$fault_status" -ne 0 ]; then + path_exists_status=$fault_status + renumber_fault_path=$fault_path + return 3 + fi return 0 fi done <<<"$records" @@ -949,6 +954,10 @@ check_no_disappearances() { renumbered_elsewhere "$base" "$record" || renum_status=$? case $renum_status in 0) info "note: $record was renumbered to $renumbered_to (content unchanged)" ;; + 3) + warn_full "W-RENUMBER-SCAN: $record: found renumber destination $renumbered_to after an incomplete search (could not read $renumber_fault_path, exit $path_exists_status)" + info "note: $record was renumbered to $renumbered_to (content unchanged)" + ;; 1) err "E-GONE: $record is no longer a record at that path (deleted, moved, untracked, or renamed with its content changed) — resolve records in place with a '> **Resolved by ...**' banner" ;; # Reported instead of E-GONE, never alongside it: whether the record moved is exactly what # could not be established — by a candidate witness that did not run, by a candidate the @@ -1074,7 +1083,8 @@ gate_known_basenames() { # result is checked non-empty before use: unguarded, an empty result makes the git argument # "${base}:/name", and depending on git's tree lookup an absolute-looking path can still # resolve — checked explicitly rather than relied on to fail closed. -# Three-valued: 0 a witness found the gate, 1 no witness did, 2 a witness could not run. +# Four outcomes: 0 a witness found the gate after a complete search, 1 no witness did, 2 no +# witness answered because a scan faulted, 3 a witness found the gate after an earlier fault. # Neither witness used to capture its status, so a scan fault was indistinguishable from every # witness genuinely finding nothing — and the caller reported that as I-GATE-BOOTSTRAP, an # exit-0 informational line, instead of the fatal E-GATE-EMPTY-SET an undeclared rename owes. @@ -1093,7 +1103,13 @@ gate_existed_at() { status=0 path_exists_at "$base" "${rel}/${name}" || status=$? case $status in - 0) return 0 ;; + 0) + if [ "$fault_status" -ne 0 ]; then + path_exists_status=$fault_status + return 3 + fi + return 0 + ;; 1) ;; *) fault_status=$path_exists_status @@ -1110,7 +1126,13 @@ gate_existed_at() { status=0 git grep --no-color -qF "$name" "$base" -- .github/workflows 2>/dev/null || status=$? case $status in - 0) return 0 ;; + 0) + if [ "$fault_status" -ne 0 ]; then + path_exists_status=$fault_status + return 3 + fi + return 0 + ;; 1) ;; *) fault_status=$status @@ -1120,7 +1142,8 @@ gate_existed_at() { done < <(gate_known_basenames) # A positive witness returns above, so it outranks a fault on any other witness: evidence - # the gate existed is not weakened by an unrelated probe failing. + # the gate existed is not weakened by an unrelated probe failing. Return 3 rather than 0 in + # that case so the caller can report the incomplete search without changing its verdict. if [ "$fault_status" -ne 0 ]; then path_exists_status=$fault_status return 2 @@ -1197,6 +1220,10 @@ check_gate_files() { gate_existed_at "$base" || existed_status=$? case $existed_status in 0) err "E-GATE-EMPTY-SET: no gate file from the base ref is protected — a rename must declare its predecessors in GATE_PREDECESSORS" ;; + 3) + warn_full "W-GATE-WITNESS-SCAN: $gate_witness_path: a later witness established that a gate existed at $base after this read failed (git exit $path_exists_status)" + err "E-GATE-EMPTY-SET: no gate file from the base ref is protected — a rename must declare its predecessors in GATE_PREDECESSORS" + ;; 1) info "I-GATE-BOOTSTRAP: no gate existed at $base — this is the change installing it" ;; *) err_full "E-GATE-WITNESS-SCAN: $gate_witness_path: could not determine whether a gate existed at $base (git exit $path_exists_status)" ;; esac diff --git a/skills/tome-of-lore/assets/check-records-test.sh b/skills/tome-of-lore/assets/check-records-test.sh index 2bba930..5f1f992 100755 --- a/skills/tome-of-lore/assets/check-records-test.sh +++ b/skills/tome-of-lore/assets/check-records-test.sh @@ -1347,6 +1347,61 @@ STUB expect_error_code "$d/.e3" E-GATE-WITNESS-SCAN fi + # A later positive witness keeps the undeclared-rename verdict, but the earlier fault still + # needs a trace. The unrelated workflow names debt.sh, a successor whose basename did not + # change, so gate_paths does not treat that workflow as protected; gate_existed_at's broader + # closed witness set still sees it after the SELF_DIR probe for debt.sh faults. + d="$SCRATCH/gate_witness_outranks_fault" + mkdir -p "$d/docs/debt" "$d/.github/scripts/profiles" "$d/.github/workflows" + git -C "$d" init -q . + git -C "$d" config user.email test@example.invalid + git -C "$d" config user.name "check-records test" + cat >"$d/.github/workflows/unrelated.yml" <<'YAML' +name: unrelated +on: push +jobs: + note: + runs-on: ubuntu-latest + steps: + - run: echo debt.sh +YAML + git -C "$d" add -A + git -C "$d" commit -qm "before the gate" + b=$(base_of "$d") + cp "$SCRIPT_DIR/check-records.sh" "$d/.github/scripts/" + cp "$SCRIPT_DIR/profiles/debt.sh" "$d/.github/scripts/profiles/" + chmod +x "$d/.github/scripts/check-records.sh" + write_record "$d" "0001-first.md" + git -C "$d" add -A + stub_bin="$SCRATCH/git-witness-outrank-bin" + mkdir -p "$stub_bin" + cat >"$stub_bin/git" <&2 + exit 128 + fi + done +fi +exec "$real_git" "\$@" +STUB + chmod +x "$stub_bin/git" + run_case "a real gate witness outranks an earlier fault" 1 E-GATE-EMPTY-SET "$d" \ + BASE_SHA="$b" PATH="$stub_bin:$PATH" + printf ' %-4s %-44s ' "" "the outranked gate fault is reported" + expect_match "$d/.err" 'W-GATE-WITNESS-SCAN beside the verdict' \ + 'the incomplete gate-witness search was silent' '::warning::W-GATE-WITNESS-SCAN: ' + printf ' %-4s %-44s ' "" "the fault path and status are retained" + expect_match "$d/.err" 'gate fault path and status named' \ + 'the gate warning did not name the failed witness' \ + '.github/scripts/debt.sh: .*git exit 128' + printf ' %-4s %-44s ' "" "the no-answer scan error stays suppressed" + expect_no_match "$d/.err" 'E-GATE-WITNESS-SCAN suppressed' \ + 'the positive witness was replaced by the no-answer fault' \ + '::error::E-GATE-WITNESS-SCAN: ' + # gate_existed_at has two witnesses, and `renamed_gate` above happens to satisfy both at # once (its base-ref workflow names "check-records.sh", which is also the literal # basename sitting in SELF_DIR at that ref) — so neither witness is individually proven. @@ -2044,11 +2099,39 @@ STUB b=$(base_of "$d") git -C "$d" mv docs/debt/0005-b.md docs/debt/0006-b.md write_record "$d" "0002-c.md" + write_record "$d" "0003-d.md" git -C "$d" add -A stub_bin="$SCRATCH/git-renumber-outrank-bin" - write_ls_files_stub "$stub_bin" docs/debt/0002-c.md + mkdir -p "$stub_bin" + real_git=$(command -v git) + cat >"$stub_bin/git" <&2 + exit 128 + ;; + esac + done +fi +exec "$real_git" "\$@" +STUB + chmod +x "$stub_bin/git" run_case "a real renumber outranks a candidate fault" 0 - "$d" \ BASE_SHA="$b" PATH="$stub_bin:$PATH" + printf ' %-4s %-44s ' "" "the outranked fault is reported" + expect_match "$d/.err" 'exit=0 W-RENUMBER-SCAN' \ + 'the incomplete renumber search was silent' '::warning::W-RENUMBER-SCAN: ' + printf ' %-4s %-44s ' "" "the last candidate fault is retained" + expect_match "$d/.err" 'last fault path and status named' \ + 'the warning did not name the last candidate fault' \ + 'could not read the index entry for docs/debt/0003-d.md, exit 128' + printf ' %-4s %-44s ' "" "the positive renumber action survives" + expect_match "$d/.out" 'renumber destination still reported' \ + 'the positive renumber result was lost' \ + 'note: docs/debt/0005-b.md was renumbered to docs/debt/0006-b.md' # check_gate_files, on the gate file itself. A gate file present and tracked read as removed, # which reported E-GATE-GONE -- the gate accusing the change of deleting a file that is diff --git a/skills/tome-of-lore/assets/check-records.sh b/skills/tome-of-lore/assets/check-records.sh index ec19ebe..19d8726 100755 --- a/skills/tome-of-lore/assets/check-records.sh +++ b/skills/tome-of-lore/assets/check-records.sh @@ -596,6 +596,11 @@ $candidate" else used_renumber_targets=$candidate fi + if [ "$fault_status" -ne 0 ]; then + path_exists_status=$fault_status + renumber_fault_path=$fault_path + return 3 + fi return 0 fi done <<<"$records" @@ -949,6 +954,10 @@ check_no_disappearances() { renumbered_elsewhere "$base" "$record" || renum_status=$? case $renum_status in 0) info "note: $record was renumbered to $renumbered_to (content unchanged)" ;; + 3) + warn_full "W-RENUMBER-SCAN: $record: found renumber destination $renumbered_to after an incomplete search (could not read $renumber_fault_path, exit $path_exists_status)" + info "note: $record was renumbered to $renumbered_to (content unchanged)" + ;; 1) err "E-GONE: $record is no longer a record at that path (deleted, moved, untracked, or renamed with its content changed) — resolve records in place with a '> **Resolved by ...**' banner" ;; # Reported instead of E-GONE, never alongside it: whether the record moved is exactly what # could not be established — by a candidate witness that did not run, by a candidate the @@ -1074,7 +1083,8 @@ gate_known_basenames() { # result is checked non-empty before use: unguarded, an empty result makes the git argument # "${base}:/name", and depending on git's tree lookup an absolute-looking path can still # resolve — checked explicitly rather than relied on to fail closed. -# Three-valued: 0 a witness found the gate, 1 no witness did, 2 a witness could not run. +# Four outcomes: 0 a witness found the gate after a complete search, 1 no witness did, 2 no +# witness answered because a scan faulted, 3 a witness found the gate after an earlier fault. # Neither witness used to capture its status, so a scan fault was indistinguishable from every # witness genuinely finding nothing — and the caller reported that as I-GATE-BOOTSTRAP, an # exit-0 informational line, instead of the fatal E-GATE-EMPTY-SET an undeclared rename owes. @@ -1093,7 +1103,13 @@ gate_existed_at() { status=0 path_exists_at "$base" "${rel}/${name}" || status=$? case $status in - 0) return 0 ;; + 0) + if [ "$fault_status" -ne 0 ]; then + path_exists_status=$fault_status + return 3 + fi + return 0 + ;; 1) ;; *) fault_status=$path_exists_status @@ -1110,7 +1126,13 @@ gate_existed_at() { status=0 git grep --no-color -qF "$name" "$base" -- .github/workflows 2>/dev/null || status=$? case $status in - 0) return 0 ;; + 0) + if [ "$fault_status" -ne 0 ]; then + path_exists_status=$fault_status + return 3 + fi + return 0 + ;; 1) ;; *) fault_status=$status @@ -1120,7 +1142,8 @@ gate_existed_at() { done < <(gate_known_basenames) # A positive witness returns above, so it outranks a fault on any other witness: evidence - # the gate existed is not weakened by an unrelated probe failing. + # the gate existed is not weakened by an unrelated probe failing. Return 3 rather than 0 in + # that case so the caller can report the incomplete search without changing its verdict. if [ "$fault_status" -ne 0 ]; then path_exists_status=$fault_status return 2 @@ -1197,6 +1220,10 @@ check_gate_files() { gate_existed_at "$base" || existed_status=$? case $existed_status in 0) err "E-GATE-EMPTY-SET: no gate file from the base ref is protected — a rename must declare its predecessors in GATE_PREDECESSORS" ;; + 3) + warn_full "W-GATE-WITNESS-SCAN: $gate_witness_path: a later witness established that a gate existed at $base after this read failed (git exit $path_exists_status)" + err "E-GATE-EMPTY-SET: no gate file from the base ref is protected — a rename must declare its predecessors in GATE_PREDECESSORS" + ;; 1) info "I-GATE-BOOTSTRAP: no gate existed at $base — this is the change installing it" ;; *) err_full "E-GATE-WITNESS-SCAN: $gate_witness_path: could not determine whether a gate existed at $base (git exit $path_exists_status)" ;; esac From 4e4152afa16c487ce837fabdc93f52fcbccdc470 Mon Sep 17 00:00:00 2001 From: David Christensen Date: Thu, 27 Aug 2026 17:59:30 -0700 Subject: [PATCH 8/8] docs: remove plan hard-break whitespace --- .../plans/2026-08-27-report-outranked-scan-faults.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/workflow/plans/2026-08-27-report-outranked-scan-faults.md b/docs/workflow/plans/2026-08-27-report-outranked-scan-faults.md index 7dfaff0..99fa4c5 100644 --- a/docs/workflow/plans/2026-08-27-report-outranked-scan-faults.md +++ b/docs/workflow/plans/2026-08-27-report-outranked-scan-faults.md @@ -1,9 +1,13 @@ # Report outranked scan faults — implementation plan -**Issue:** #87 -**Decision:** [ADR 0043](../../adr/0043-report-outranked-scan-faults.md) -**Spec:** [design](../specs/2026-08-27-report-outranked-scan-faults-design.md) -**Branch:** `feat/report-outranked-scan-faults-87` +**Issue:** #87 + +**Decision:** [ADR 0043](../../adr/0043-report-outranked-scan-faults.md) + +**Spec:** [design](../specs/2026-08-27-report-outranked-scan-faults-design.md) + +**Branch:** `feat/report-outranked-scan-faults-87` + **BASE_BRANCH:** `main` ## Global constraints