Skip to content

Commit e3411ed

Browse files
claude[bot]claude
andauthored
feat(pm): H61 reports a merged PR or closed card still carrying needs:contract-review (#17831)
The half-state patrol could see a contract-review gate on an OPEN carrier (H31 compares the card's and the PR's, H51 reads one outliving a verdict on the current head, H53 reads one with no increment behind it) and it could see a gate REMOVAL (H35). It could see nothing at the other end: a pull request that MERGED, or a card that CLOSED, with `needs:contract-review` still on it. 载体纪律's invariant is 「开着的载体恒 = 真实待审」, so every one of those labels still tells a later reader — a seat, a post-merge tier audit, any candidate query that forgets to scope `state=open` — that a real contract review is owed on work that is already in `main`. This adds H61, one report-only row per landed carrier, printing the merge or closure stamp and the PR's head sha, so the post-merge tier audit has a list instead of a memory. It costs no request. Both populations were already in hand by the time the row runs: merged PRs from H8's 8-day window (`listRecentlyMergedPullRequests`) and closed cards from H22's 3-day window (`listRecentlyClosedIssues`), whose list rows already carry `labels`, `merged_at` / `closed_at` and `head.sha`. Exit codes, bands and every other row are untouched — a completed sweep still exits 0 whatever it finds, and the script still never writes a label: clearing a carrier is 清标即落地, a seat's audited write with its provenance comment, and a sweeper striking a review gate would be issuing the verdict. Fixes #17510 ## Acceptance notes ### The director seat's ask, verbatim From comment 5597753733 on #17040 (os-bill, 2026-09-09T07:11Z, "What the mechanisable fix should now also cover"), quoted exactly as written: > A `check-half-states` row for a **closed card still carrying `pm:*` or a carrier** (row 1 left #16231 closed with `pm:queue` + `needs:contract-review`; row 2 left the carrier on a merged PR). The `pm:*` half of that sentence is H22's and stays there. The CARRIER half is H61. The card paraphrases it as 「merged PR / closed card still carrying a gate label」; the sentence above is the actual wording. ### Premises, re-checked on `origin/main` at `f830fa21` 1. **Holds.** No predicate named a merged PR or a closed card still carrying the gate. `h51SpeaksAbout` excludes a merged or closed PR by name; H31's population test excludes a merged delivering PR and a closed card by name; H35 reads removal events; H8 reads `pm:dispatched`; H22 reads `pm:*` on closed cards and files no row at all since #14072. `CONTRACT_REVIEW_LABEL` is reused — no second spelling, pinned the way H51 and H53 pin it. 2. **Half false, and corrected in place.** H60 is indeed the highest number in use and H99 is the self-test sentinel, so H61 is the next free one. But the parenthetical is wrong: the top docblock's "## The invariants" index stops at **H28**. H29 through H60 have no entry there at all. H59 and H60 were added as a banner comment block above their predicate plus a `HALF_STATE_FAMILY_BAND` entry, and `familyRegistryCoverage` is the mechanical index that holds the band registry equal to the codes the sweep actually pushes. H61 follows that idiom; adding a lone H61 line to an index that stops thirty-three rows earlier would have been worse than leaving it out. 3. **Holds.** `33e07f7c` (PR #17484) is an ancestor of `origin/main`. The front end is closed, so this row is residue reporting, not a bypass, and the row says so in its own text. 4. **Updated, larger, and measured.** See below. ### The measured population, 2026-09-12 `GET /repos/{owner}/{repo}/issues?state=closed&labels=needs:contract-review`, one page per repo, both complete (39 and 29 rows, under the 100-row page size): | repo | total | merged PRs | closed-UNMERGED PRs | closed cards | |:--|--:|--:|--:|--:| | objectstack | 39 | 23 | 2 | 14 | | objectui | 29 | 17 | 0 | 12 | | **both** | **68** | **40** | **2** | **26** | Oldest closure still wearing the label: 2026-08-20T23:58Z (objectstack), 2026-08-30T17:32Z (objectui). The card named three specimens; the class is an order of magnitude larger. **What the row reaches, and what it does not.** H61 reads the two windows the sweep already holds, so a merged carrier is visible for 8 days after it lands and a closed-card carrier for 3. At the patrol's 6-hour cadence that is roughly 32 and 12 consecutive sweeps: every NEW carrier is seen, repeatedly. What ages out is the standing backlog above. That boundary is deliberate — the alternative is a `state=closed&labels=…` listing, which is both a new fetch class and a second closed-card reader beside `pmLabelListingPath`, whose `state=open` scoping is the entire mechanism by which maintainer ruling 批 #13 holds. The header records the number so a reader can tell a short reach from a clean board. ### Work item B — closed-UNMERGED PRs are EXCLUDED Stated, not folded. Two of the 68 (objectstack #16733 and #14923; objectui 0 — 2.9%) are pull requests closed without merging, carrier still on. They are out of this row for three reasons: 1. **The harm model does not reach them.** This row exists because a gate on a LANDED increment says a review is owed on work already in `main`; the post-merge tier audit is the consumer. A PR closed unmerged delivered nothing and owes no post-merge verdict — the shape H51's own header already calls moot, 「载体不迁移」. 2. **The data path does not hold them.** `listRecentlyMergedPullRequests` SELECTS on `merged_at`, and its header says why in as many words: an unmerged closed PR is an abandoned attempt, not a delivery. Widening that selector would change what H8 and H49 are handed — two other rows' inputs, and out of scope here. 3. **Size.** 2 of 68, both on one install, neither inside the window this row reads. A separate count rendered on every run for a class that contributes nothing to it is a number a reader learns to ignore. Both legs refuse them, and both refusals are pinned. Writing that pin is what found a real hole in my own first draft: a closed-unmerged PR arriving from the PULLS listing carries no `pull_request` field at all, while `state` is `closed` and `closed_at` is readable — so the card leg accepted it. `h61SpeaksAboutClosedCard` now refuses a row carrying `head` as well. Live, the two collections never cross; a population test that is only correct because of its caller is one refactor from being wrong. ### The sha is the HEAD, not the merge commit The row prints `head.sha`. 载体纪律 says a clearing write cites 「所判 head」 and H51 resolves a verdict against `pr.head.sha`, so the head is the only sha a contract-review record can be matched on. Measured on the three specimens that filed this card: #16783 head `47eea7a8` / merge commit `854639b3`; #17036 head `530469ff` / merge commit `e4fd55d9`; objectui#8779 head `a04441f7` / merge commit `6cc48c4e`. The three shas quoted on the card and in the dispatch are the MERGE COMMITS — a reader searching a thread for one of them finds nothing. Noted, not filed: a card-text inaccuracy, not a defect in any code. ### Band, and the repo column `inventory`. Refused in turn: not `gate` (that band is for a row that can tell a STRIPPED gate from an ungated one — an absence reading as a green light; here the carrier is present), not `stall` (nothing is stopped — the increment merged), not `state` (whose definition says a LIVE card, while every subject here is merged or closed, ARCHIVE in ruling 批 #13's own word for this population). Being first in line for the body trim is correct for it, and the ledger callout names the family and the omitted count rather than dropping rows silently. The row text is deliberately short for this file for the same reason: it fires per carrier, and 20 of the 23 objectstack merged carriers sit inside the 8-day window today. No repo column: each install sweeps its own repository through `resolveSweepRepo`, one token and one board, so this row can never see more than one and the column would be a constant. The two-install figures above are a hand measurement taken for the header, not something a run reproduces. ### Verification - `node scripts/pm/check-half-states.mjs --self-test` :: exit 0 — 3748 cases pass (3707 before this change, 41 new). Both directions per the dispatch: a merged PR carrier fires, a closed card carrier fires, an OPEN PR carrier does not (and H51 still speaks about exactly that PR, so nothing fell between the two rows), a merged PR without the label does not, a closed-unmerged PR does not through either leg, unreadable `labels` is excluded rather than read as unlabelled, an unreadable stamp yields no row. The row text is pinned on the invariant, the remedy (清标即落地 / 同笔剥双载体 / the provenance comment / who acts), the "never a label written from this script" clause, the neighbouring rows it defers to, and the fact that it prints `CONTRACT_REVIEW_LABEL` rather than a re-typed literal. - Exit codes and bands pinned unchanged: `EXIT_PREREQUISITE_NOT_MET` is still 3, `HALF_STATE_FAMILY_BANDS` still spells `gate,unregistered,stall,state,inventory`, `isLoudFinding` is false on the row, and `familyRegistryCoverage().missing.length` is 0. - All 41 gate families derived by `node scripts/pm/dispatch-gates.mjs --commands` in this worktree were run; each exit code is in the report on #17510. - No changeset: `scripts/pm/**` publishes nothing from any released package, which lint.yml calls the textbook `skip-changeset` case in as many words. ### Out of scope, noted not filed - The card's and the dispatch's three shas are merge-commit shas presented as head shas (measured above). Successor: this PR body and the row's own header carry the correction; no code is wrong. - #16231 no longer carries `pm:queue` — the director's row 1 recorded it on 2026-09-09 and it has since been cleared. The carrier is still on, which is why it serves as this row's closed-card specimen. --- _Generated by [Claude Code](https://claude.ai/code/session_01MCLBsUgfykL74aU716rzVK)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8305ad6 commit e3411ed

1 file changed

Lines changed: 366 additions & 0 deletions

File tree

0 commit comments

Comments
 (0)