feat(release): T-5 reconcile guards — check_pr_closes_issue.sh + check_reconcile.sh (APR-RELEASE-001 §6, kaizen) - #3200
Conversation
…eck_reconcile.sh APR-RELEASE-001 section 6: R-2 (PR body must close what it cites, or say why not via no-close:) and the five T-5 reconcile predicates (R1..R5), each factored into file-driven functions so --self-test never touches the network. Both self-tests carry must-RED/must-GREEN twins per predicate (mutation proof). Pmat-Ticket: PMAT-1098 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
§13.11 rung 1 — quorum shadow verdict Shadow mode: this records a verdict and merges nothing. A refusal |
…very cargo-free guard with no args Pmat-Ticket: PMAT-1098 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
check_roadmap_diff_additive.sh reported base id(s) missing at head: this branch forked before they landed on main, and a plain update takes one side of the file whole. Resolved as main's roadmap plus this branch's own new entries, each at its sorted slot among same-prefix peers. The other three roadmap guards pass on the result. Pmat-Ticket: PMAT-1098 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Ran this guard against the real tree before it merged — R2 needs an epoch. R3 conflates dead with stranded. It named R4 is volatile against main. It read 0 at 11:44Z and 1 at 11:50Z, because #3164, None of this blocks this PR — the guard measures what it says it measures. It is what the |
…he table was not hermetic once it started resolving TWO MORE, found by measuring the first change instead of trusting it. 1. A REFERENCE TO A CLOSED ISSUE OWES NO REASON. "left open forever" is this guard's whole purpose, so demanding a `no-close:` line for an issue that is already closed is the same false positive as demanding one for a sibling PR. Of the references still flagged after PR refs were dropped, #2706, #338 and #532 were all already closed; the reason a body would have had to write is "it is closed", which the API already knows. Someone else closing it still counts, and that is not a loophole: the outcome R-2 wants is a closed issue, not a particular author closing it. What it cannot do is pass on an OPEN one — mutation M5 (drop `issue` too) turns six rows red, including the three that shipped with the guard. 2. THE SELF-TEST STOPPED BEING HERMETIC THE MOMENT THE GUARD RESOLVED ANYTHING. The rows cite "#123" and "#1", and once ref_kind existed those were no longer fixtures — they became live API calls against real issues in this repository. Both are closed here, so `refs-only` and `noclose-empty`, two must-RED rows, started PASSING for a reason that had nothing to do with what they test. Caught because they went red, not because anyone reasoned about it. Every row is pinned to the stub now: no network, no token, deterministic. 9001 a PR, 9002 an open issue, 9004 closed, 9003 unresolvable, everything else an issue. AND ONE ROW THAT IS NOT THE SEAM. Sixteen rows pinned to a stub prove the DECISION and say nothing about the RESOLVER. `gh-error-fails-closed` shadows `gh` with one that exits 1 — a runner with no token, a rate limit, or no network — and requires the body to still FAIL. Mutation M8 (resolver error answers `closed`) kills it; without that row, the only way this change could make the guard laxer than it was would be invisible. `gh` is shadowed rather than removed from PATH because it lives in /usr/bin beside the grep and sed this script needs: an empty PATH tests nothing but exit 127, which is what the first attempt measured. MEASURED EFFECT on the 31 open PRs: 8 passing before any of this, 13 after PR refs were dropped, 16 now. The 15 that still fail every one cite a genuinely OPEN issue with no keyword and no reason — which is the debt §6 R-2 exists to surface, not noise to tune away. Self-test 16/16, all hermetic. Refs #3200 Pmat-Ticket: PMAT-1098 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…aller further out
guard-tree caught it on this PR's own branch:
FAIL R2-positive: expected [10], got []
self-test FAILED: 1 case(s).
check_reconcile.sh's R2 row delegates to check_pr_closes_issue.sh, and its
fixture body is `Refs #5`. Once that script began resolving references, #5
stopped being a fixture: it is a real, already-CLOSED issue in this
repository, so the new rule dropped it and R2 correctly reported no
offenders — while the table still expected [10].
This is the same defect I fixed inside check_pr_closes_issue.sh's own
table in the previous commit, one caller further out, and I did not look
for the second caller. Extending a script's behaviour extends it for
everyone who calls it; the old proof does not transfer, and neither do the
old fixtures.
Pinned to a stub that answers `issue` for every number — which is what
these fixtures were always written to mean. Exported, because
r2_missing_closing_ref runs the close script as a child process.
Mutation-verified: unsetting the pin reproduces the CI failure exactly,
`FAIL R2-positive: expected [10], got []`, so the pin is load-bearing.
check_reconcile.sh is the ONLY other caller — grepped rather than assumed —
and its bare path is the self-test, so guard_tree makes no API calls.
Self-test 17/17.
Refs #3200
Pmat-Ticket: PMAT-1098
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
APR-RELEASE-001 §6 T-5 Reconcile is a hard gate of every train (operator ruling 2026-09-13, kaizen). Measured on the 0.67.0 train: 308 open issues, 424 opened vs 124 closed in 30 d, 48 merged PRs of which 9 carried a closing reference, 880 remote branches of which 847 had no PR. Filing was the work product; closing was nobody's.
Two guards, each with a must-RED/must-GREEN self-test (mutation-checked: neutering the fail path turns the self-test red):
scripts/check_pr_closes_issue.sh— a PR body that cites#Nmust useCloses/Fixes/Resolves, or carryno-close: <reason>. 9-case table.scripts/check_reconcile.sh <tag> --prev-tag <prev> --json OUT— the five predicates R1 fixed-but-open, R2 missing closing refs, R3 PR-less branches older than 14 d, R4 DIRTY PRs older than a train, R5 closure/arrival; exit 1 unless R1..R4 are 0, exit 2 on env. 17-case offline self-test viaRECONCILE_INPUT_DIR.First real receipt (v0.66.0→v0.67.0, after archiving 776 dead branches to
refs/archive/): R1=0 R2=28 R3=0 R4=1 R5=22/61=0.36. Wiring R2 into ci.yml andcheck_reconcile.shinto the autopilot'sclosestep follows in the next PR.no-close: the spec amendment lives on PR #3164; this PR adds the guards only.
🤖 Generated with Claude Code