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/docs/adr/0043-report-outranked-scan-faults.md b/docs/adr/0043-report-outranked-scan-faults.md new file mode 100644 index 0000000..43c0527 --- /dev/null +++ b/docs/adr/0043-report-outranked-scan-faults.md @@ -0,0 +1,66 @@ +# 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. 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 + +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. + +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. + +## 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, 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 +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 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 +indistinguishable success, recreating the silent omission this decision is meant to prevent. 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..99fa4c5 --- /dev/null +++ b/docs/workflow/plans/2026-08-27-report-outranked-scan-faults.md @@ -0,0 +1,120 @@ +# 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 `./.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, + 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. 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 + `E-GATE-WITNESS-SCAN`. +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`. + +**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. 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. +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 `./.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. + +**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 + `./.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 + `./.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; + 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. 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..226b031 --- /dev/null +++ b/docs/workflow/specs/2026-08-27-report-outranked-scan-faults-design.md @@ -0,0 +1,72 @@ +# 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. + +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 +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. +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 +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. 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