Skip to content

Commit 2b4178a

Browse files
os-litantclaude
andauthored
feat(pm): H22 judges closed cards against a dated closure floor (#12906)
* feat(pm): H22 judges closed cards against a dated closure floor The half-state patrol's closed-card row reports a card that left the board still carrying a `pm:*` state label. A sibling install measured its own closed surface at ~87% residue density and had to ship with the row switched off: at that density H22 reports the CONVENTION rather than a defect, and the rows exhaust the anchor body budget every other predicate shares. That install's card framed the choice as two-way — either stripping is the rule and ~815 closed cards need a backfill first, or the row is simply not wanted. `PM_SWEEP_CLOSED_FLOOR` is the third option both readings omit: judge only cards closed on/after a cutover date. Historical `pm:*` on an old closed card is inert (the loop reads state on open cards only), so the row gets its whole value — residue produced from now on, while the paired write is still a live duty — at zero backfill and zero historical noise. No code path here writes a label, so no bulk rewrite of closed cards is reachable. Unset is the default and preserves today's behaviour exactly. A malformed floor is refused with exit 2 rather than degrading to "no floor", which on the install that needs one would restore the flood four times a day; the parse is round-tripped because `Date.parse` rolls `2026-02-31` to March rather than rejecting it. The rendered summary names the floor, so a floored pass cannot overstate what it judged. * docs(pm): codify strip-`pm:*`-on-close in the label-discipline section The convention was ambiguous enough that two seats read the same board and landed on opposite rules. Fleet practice has since settled it — the 2026-08-27 /28 landing accounting stripped `pm:*` from every closed card as a hard step — so this writes the answer down where the loop reads it, which is the cure the original card asked for. Ratchet is zero-headroom, so this is net 0 lines. Cut ledger in the PR body. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 196a6c7 commit 2b4178a

2 files changed

Lines changed: 182 additions & 8 deletions

File tree

.claude/skills/pm-dispatch/SKILL.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,10 @@ seat-post-protocol);维护者明示召唤是仲裁:有简报径直坐席,无简
120120
默认不适用 `type:Bug` 的 hold**,三分支:可复现且用户可达 ⇒ 回 `pm:queue`(被 hold 的 Bug 就是
121121
被藏起的缺陷);declared≠enforced 观察类 ⇒ 转 enforce-or-remove 通道(不 hold 不裸关);真
122122
won't-fix 候选 ⇒ 逐卡进决策箱 —— 座位永不自行关闭真实缺陷。**机会主义重启条件必须
123-
点名触发文件**(维护者 2026-08-11 接受):写侧即 `Restart-touch:` 行(行契约见
124-
`references/state-machine.md`);派发/折叠检查时读半状态巡查锚(`half-state-patrol.yml`
125-
`ANCHOR_ISSUE` 置顶 issue)的 H17 on-hold 触发文件索引、与本次派发文件面相交,命中 ⇒ 按该
126-
hold 评论的 rider/restart 条款处置(点名该单、顺手活列为申报过的增项)。
123+
点名触发文件**(维护者 2026-08-11 接受):派发/折叠检查时读半状态巡查锚(`half-state-patrol.yml`
124+
`ANCHOR_ISSUE` 置顶 issue)的 H17 on-hold 触发文件索引、与本次派发文件面相交,命中 ⇒
125+
按该 hold 评论的 rider/restart 条款处置(点名该单、顺手活列为申报过的增项)。
126+
- **关闭即摘 `pm:*` 状态标**(与关单同一笔;`domain:*`/类型标签留下 —— 归属不是状态)。
127127
- **`Blocked-by:` 行是机器可 grep 的反向索引**,一遍读喂三个职责:上游关单时放回被解锁的、
128128
按解锁扇出排序选择、**在合并后的 ref 上重验每张回队卡的文件面**(⛔ 只做第一件;关掉
129129
上游的那个合并最可能顺手把你这张卡也修掉)。**一个标签存在,当且仅当有具名读

scripts/pm/check-half-states.mjs

Lines changed: 178 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3809,9 +3809,30 @@ export const PM_RESIDUE_LABELS = [
38093809
* predicate cannot double-report the population every other item already reads.
38103810
* That gate is the predicate's own, not the caller's, because it is the one
38113811
* thing separating this row from a restatement of H3.
3812+
*
3813+
* `floor` is the optional dated closure floor (see `resolveClosureFloor`): a
3814+
* `Date` before which a closed card is out of scope, or null for "judge every
3815+
* card in the window", which is the default and this repo's own behaviour.
3816+
*
3817+
* ⚠️ A card whose `closed_at` cannot be read is judged, NOT skipped. The floor
3818+
* is a scope decision that needs a date to make; without one the card's
3819+
* position relative to the cutover is UNKNOWN, and silently dropping it would
3820+
* narrow the pass on unread data — #4690 in the direction this file refuses
3821+
* everywhere else. The listing endpoint always carries `closed_at` on a closed
3822+
* issue, so fail-open costs no noise in practice; it just keeps the one
3823+
* unreadable card visible instead of disappeared.
3824+
*
3825+
* @param {any} issue
3826+
* @param {Date | null} [floor]
38123827
*/
3813-
export function h22ClosedCardPmResidue(issue) {
3828+
export function h22ClosedCardPmResidue(issue, floor = null) {
38143829
if (issue?.state !== 'closed') return null;
3830+
if (floor) {
3831+
const closedAt = Date.parse(issue.closed_at ?? '');
3832+
// Strictly BEFORE the floor is out of scope; a card closed ON the cutover
3833+
// date is the first day the convention applies and is judged.
3834+
if (!Number.isNaN(closedAt) && closedAt < floor.getTime()) return null;
3835+
}
38153836
const residue = labelNames(issue ?? {}).filter((l) => PM_RESIDUE_LABELS.includes(l));
38163837
if (residue.length === 0) return null;
38173838
const list = residue.map((l) => `\`${l}\``).join(', ');
@@ -6692,7 +6713,7 @@ export const SWEEP_COUNT_KEYS = [
66926713
* blockerTargets?: number, crossRepoProbed?: number,
66936714
* crossRepoUnreadable?: number, seatMarkersRead?: number,
66946715
* seatCandidates?: number, commits?: number, commitBindings?: number,
6695-
* commitBindingMessages?: number }} counts
6716+
* commitBindingMessages?: number, closedFloor?: string }} counts
66966717
* @param {number} findingCount
66976718
*/
66986719

@@ -6772,7 +6793,9 @@ export function summaryLine(counts, findingCount) {
67726793
`(merge state read on ${probed} of ${candidates} H16 candidate(s)) ` +
67736794
`and ${counts.merged} recently-merged PR(s) in ${counts.repo} — ${findingCount} half-state(s) found. ` +
67746795
`H22 read ${counts.closed ?? 0} recently-closed issue(s) for \`pm:*\` state residue (bounded window; ` +
6775-
`older closed carriers are outside it by design). ` +
6796+
`older closed carriers are outside it by design` +
6797+
`${counts.closedFloor ? `, and only cards closed on/after ${counts.closedFloor} are judged — ` +
6798+
'earlier closures predate the strip-on-close convention and are NOT a reading about them' : ''}). ` +
67766799
`H23 read ${commits} squash commit message(s) from the default branch's recent window, carrying ` +
67776800
`${commitBindings} closing-keyword binding(s) across ${commitBindingMessages} message(s) ` +
67786801
`(bounded window; a message that landed before it is invisible by design). ` +
@@ -7869,6 +7892,79 @@ async function listRecentlyMergedPullRequests() {
78697892
*/
78707893
export const CLOSED_ISSUE_WINDOW_PAGES = 4;
78717894

7895+
/**
7896+
* H22's DATED CLOSURE FLOOR — the cutover date at and after which a closed
7897+
* card's `pm:*` residue is judged (objectui#5985).
7898+
*
7899+
* ## The dilemma this dissolves
7900+
*
7901+
* The window above is bounded by UPDATE recency, which is the wrong axis for
7902+
* the one question a sibling install kept running into: "was this card closed
7903+
* under the convention, or before it existed?" Measured in objectui
7904+
* 2026-08-24, while porting this file: 815 closed cards there carry
7905+
* `pm:dispatched`, and ~347 of the 400 issues in the window above carry some
7906+
* `pm:*` residue (~87%, against the 26% this repo measured on its own board).
7907+
* At that density H22 reports the CONVENTION rather than a defect — ~347 rows
7908+
* that exhaust the anchor body budget and trim every other predicate's
7909+
* findings out of the report. That install therefore shipped with the closed
7910+
* reader switched off, and its card recorded the choice as a two-way one:
7911+
* either stripping is the rule (and ~815 cards need a BACKFILL before H22 can
7912+
* be honest) or it is not (and H22 is simply not a predicate that repo wants).
7913+
*
7914+
* The floor is the third option both readings omit. `pm:*` on a card closed
7915+
* before the convention was written is inert history: nothing queries it as a
7916+
* claim of in-flight-ness, because the loop reads state on OPEN cards only
7917+
* (`is:open` is in every one of its inventory queries). Judging only cards
7918+
* closed on/after a cutover date therefore buys the row's whole value — the
7919+
* residue produced from now on, while the paired write is still a live duty
7920+
* someone remembers — at zero backfill and zero historical noise. ⛔ The
7921+
* alternative this file must never grow is a bulk label rewrite of closed
7922+
* cards: 815 mutating writes to make a report quieter is machinery serving the
7923+
* instrument, and no code path here writes a label at all.
7924+
*
7925+
* ## Default: unset, which is exactly today's behaviour
7926+
*
7927+
* An install that wants every card in the window judged sets nothing, and this
7928+
* resolver returns a null floor that the predicate ignores. That keeps this
7929+
* repo's own patrol byte-identical across this change — it measured 26% and
7930+
* treats recent closed residue as a live duty — and makes the floor a
7931+
* per-install adaptation rather than a policy shipped to everyone.
7932+
*
7933+
* ## Malformed is REFUSED, never defaulted
7934+
*
7935+
* A typo'd floor that silently became "no floor" would restore the 87% flood
7936+
* on the one install that set it, four times a day, and the flood reads as a
7937+
* working patrol — the same trap `resolveSweepRepo` refuses by name. So an
7938+
* unparseable value is `valid: false` and the entrypoint exits 2 on it. Only
7939+
* the `YYYY-MM-DD` spelling is accepted: a bare `Date` parse would take
7940+
* "yesterday-ish" strings and timezone-bearing ones whose midnight is not the
7941+
* one the workflow author meant, and the value is written by hand in a
7942+
* workflow file exactly once.
7943+
*/
7944+
export function resolveClosureFloor(env = {}) {
7945+
const raw = String(env.PM_SWEEP_CLOSED_FLOOR ?? '').trim();
7946+
if (!raw) return { floor: null, source: 'default', valid: true, raw: '' };
7947+
if (!/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
7948+
return { floor: null, source: 'PM_SWEEP_CLOSED_FLOOR', valid: false, raw };
7949+
}
7950+
const at = Date.parse(`${raw}T00:00:00Z`);
7951+
if (Number.isNaN(at)) return { floor: null, source: 'PM_SWEEP_CLOSED_FLOOR', valid: false, raw };
7952+
// A shape-valid string can still name a date that does not EXIST, and
7953+
// `Date.parse` does not reject all of them: `2026-13-01` is NaN (the month
7954+
// is outside the ISO range) but `2026-02-31` silently ROLLS to 2026-03-03.
7955+
// So the parse is round-tripped rather than trusted. Letting a rolled date
7956+
// through would move the floor days past where its author wrote it and,
7957+
// worse, do it silently — the floor is the one input here whose whole job is
7958+
// to say which cards were judged.
7959+
const floor = new Date(at);
7960+
if (floor.toISOString().slice(0, 10) !== raw) {
7961+
return { floor: null, source: 'PM_SWEEP_CLOSED_FLOOR', valid: false, raw };
7962+
}
7963+
return { floor, source: 'PM_SWEEP_CLOSED_FLOOR', valid: true, raw };
7964+
}
7965+
7966+
const CLOSED_FLOOR = resolveClosureFloor(process.env);
7967+
78727968
async function listRecentlyClosedIssues() {
78737969
const out = [];
78747970
for (let page = 1; page <= CLOSED_ISSUE_WINDOW_PAGES; page++) {
@@ -8351,9 +8447,10 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen
83518447
// line can say what this pass covered on its own terms.
83528448
for (const issue of await listRecentlyClosedIssues()) {
83538449
seenClosed.set(issue.number, issue);
8354-
const residue = h22ClosedCardPmResidue(issue);
8450+
const residue = h22ClosedCardPmResidue(issue, CLOSED_FLOOR.floor);
83558451
if (residue) findings.push([issue, 'H22', residue]);
83568452
}
8453+
stats.closedFloor = CLOSED_FLOOR.raw;
83578454

83588455
// H23 — the commit-message surface (#10942). The counting is not incidental:
83598456
// this row's measured yield is ~6 in 1,546, so a silent H23 is the normal
@@ -9557,10 +9654,75 @@ function selfTest() {
95579654
t(`H22: \`${label}\` on a closed card is residue`, typeof h22ClosedCardPmResidue(closedCard([label])), 'string');
95589655
}
95599656

9657+
// -- H22's DATED CLOSURE FLOOR (objectui#5985) ------------------------------
9658+
//
9659+
// The floor is what lets a sibling install re-enable this row without the
9660+
// backfill its own card thought was the only alternative: judge cards closed
9661+
// on/after a cutover date, leave the historical carriers unjudged, write no
9662+
// labels at all. The cases below pin the three properties that decision rests
9663+
// on — the floor is HONOURED, its absence changes nothing, and a malformed
9664+
// value is refused rather than silently becoming "no floor".
9665+
const FLOOR = new Date(Date.parse('2026-08-28T00:00:00Z'));
9666+
const closedOn = (labels, closed_at) => ({ ...closedCard(labels), closed_at });
9667+
9668+
// Honoured, both directions. The old card is the ~815-card backlog in
9669+
// miniature: it carries real residue and is deliberately NOT a finding.
9670+
t('H22 floor: a card closed BEFORE the floor is out of scope', h22ClosedCardPmResidue(closedOn(['pm:dispatched'], '2026-08-01T09:00:00Z'), FLOOR), null);
9671+
t('H22 floor: …however much residue it carries', h22ClosedCardPmResidue(closedOn(['pm:dispatched', 'pm:queue', 'pm:blocked'], '2026-01-01T00:00:00Z'), FLOOR), null);
9672+
t('H22 floor: a card closed AFTER the floor is judged', typeof h22ClosedCardPmResidue(closedOn(['pm:dispatched'], '2026-08-29T09:00:00Z'), FLOOR), 'string');
9673+
t('H22 floor: …and the row still names the residue label', h22row(closedOn(['pm:dispatched'], '2026-08-29T09:00:00Z'), FLOOR).includes('`pm:dispatched`'), true);
9674+
// The boundary is inclusive: the cutover date is the first day the convention
9675+
// applies, so a card closed within it is the convention's own population.
9676+
t('H22 floor: a card closed ON the floor date is judged', typeof h22ClosedCardPmResidue(closedOn(['pm:queue'], '2026-08-28T00:00:00Z'), FLOOR), 'string');
9677+
t('H22 floor: …and later the same day too', typeof h22ClosedCardPmResidue(closedOn(['pm:queue'], '2026-08-28T23:59:59Z'), FLOOR), 'string');
9678+
t('H22 floor: one second before the floor is out', h22ClosedCardPmResidue(closedOn(['pm:queue'], '2026-08-27T23:59:59Z'), FLOOR), null);
9679+
// The floor narrows scope; it never invents findings. A clean recent card is
9680+
// still clean, and an OPEN card is still not this row's.
9681+
t('H22 floor: a clean card after the floor is still clean', h22ClosedCardPmResidue(closedOn(['domain:cli'], '2026-08-29T09:00:00Z'), FLOOR), null);
9682+
t('H22 floor: the closed gate still outranks the floor', h22ClosedCardPmResidue({ ...issue(['pm:dispatched']), state: 'open', closed_at: null }, FLOOR), null);
9683+
// Fail-OPEN on an unreadable closure date: the floor cannot be applied, so
9684+
// the card stays visible rather than being dropped on unread data (#4690).
9685+
t('H22 floor: a card with no closed_at is judged, not dropped', typeof h22ClosedCardPmResidue(closedOn(['pm:dispatched'], null), FLOOR), 'string');
9686+
t('H22 floor: …and an unparseable one likewise', typeof h22ClosedCardPmResidue(closedOn(['pm:dispatched'], 'not-a-date'), FLOOR), 'string');
9687+
9688+
// Floor ABSENT — the default, and the property that makes this change a
9689+
// no-op for the install that wants every card in the window judged.
9690+
t('H22 floor: absent floor judges an old closed card exactly as before', typeof h22ClosedCardPmResidue(closedOn(['pm:dispatched'], '2026-01-01T00:00:00Z')), 'string');
9691+
t('H22 floor: …an explicit null is the same as omitting it', typeof h22ClosedCardPmResidue(closedOn(['pm:dispatched'], '2026-01-01T00:00:00Z'), null), 'string');
9692+
t('H22 floor: …and a clean old card is still clean', h22ClosedCardPmResidue(closedOn(['domain:cli'], '2026-01-01T00:00:00Z'), null), null);
9693+
9694+
// resolveClosureFloor — the env reading, including the loud refusal.
9695+
t('closure floor: unset means no floor', resolveClosureFloor({}).floor, null);
9696+
t('closure floor: …and that is a VALID reading, not an error', resolveClosureFloor({}).valid, true);
9697+
t('closure floor: …reported as the default source', resolveClosureFloor({}).source, 'default');
9698+
t('closure floor: whitespace is unset too', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: ' ' }).floor, null);
9699+
t('closure floor: a YYYY-MM-DD date resolves to UTC midnight', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: '2026-08-28' }).floor.toISOString(), '2026-08-28T00:00:00.000Z');
9700+
t('closure floor: …and is valid', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: '2026-08-28' }).valid, true);
9701+
t('closure floor: …and names its source', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: '2026-08-28' }).source, 'PM_SWEEP_CLOSED_FLOOR');
9702+
t('closure floor: surrounding whitespace is trimmed, not rejected', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: ' 2026-08-28 ' }).valid, true);
9703+
// Malformed is REFUSED. Each of these would otherwise become "no floor" and
9704+
// restore the flood on the one install that set the variable.
9705+
for (const bad of ['28-08-2026', '2026/08/28', 'yesterday', '2026-08-28T00:00:00Z', '2026-8-28', 'O', '0']) {
9706+
t(`closure floor: \`${bad}\` is refused, not defaulted`, resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: bad }).valid, false);
9707+
}
9708+
// …including a well-SHAPED date that does not exist — the case a bare regex
9709+
// would pass and whose floor would exclude every card, rendering an empty
9710+
// H22 as a clean closed surface.
9711+
t('closure floor: a shape-valid impossible date is refused', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: '2026-02-31' }).valid, false);
9712+
t('closure floor: …and an impossible month likewise', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: '2026-13-01' }).valid, false);
9713+
t('closure floor: a refused value carries no floor to fall back on', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: 'yesterday' }).floor, null);
9714+
t('closure floor: …and is reported as itself for the error message', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: 'yesterday' }).raw, 'yesterday');
9715+
95609716
// The summary line's H22 clause — a pass that read nothing must not read the
95619717
// same as a board with no residue (#4690), so the count is always stated.
95629718
t('summary: the H22 clause states what the closed pass read', summaryLine({ repo: 'r', issues: 1, unscoped: 1, prs: 0, merged: 0, closed: 200 }, 0).includes('H22 read 200 recently-closed issue(s)'), true);
95639719
t('summary: an absent closed count degrades to 0, never to undefined', summaryLine({ repo: 'r', issues: 1, unscoped: 1, prs: 0, merged: 0 }, 0).includes('H22 read 0 recently-closed'), true);
9720+
// …and when a floor is in force the line SAYS so: "read 200" with a floor
9721+
// silently applied would overstate what was judged, which is the same
9722+
// unread-reads-as-clean defect the count itself exists to prevent.
9723+
t('summary: a floored pass names the floor date', summaryLine({ repo: 'r', issues: 1, unscoped: 1, prs: 0, merged: 0, closed: 200, closedFloor: '2026-08-28' }, 0).includes('only cards closed on/after 2026-08-28 are judged'), true);
9724+
t('summary: …and says the earlier closures are not a reading about them', summaryLine({ repo: 'r', issues: 1, unscoped: 1, prs: 0, merged: 0, closed: 200, closedFloor: '2026-08-28' }, 0).includes('NOT a reading about them'), true);
9725+
t('summary: an unfloored pass adds no floor clause', summaryLine({ repo: 'r', issues: 1, unscoped: 1, prs: 0, merged: 0, closed: 200 }, 0).includes('are judged'), false);
95649726

95659727
// -- H23: the COMMIT-MESSAGE surface (#10942) -------------------------------
95669728
//
@@ -12730,6 +12892,18 @@ if (isMain) {
1273012892
);
1273112893
process.exit(2);
1273212894
}
12895+
// A malformed closure floor is the same class and gets the same answer. It
12896+
// must not degrade to "no floor": the install that sets one is the install
12897+
// whose closed surface is ~87% residue, so a silent default would flood the
12898+
// anchor body four times a day and the flood renders as a working patrol.
12899+
if (!process.argv.includes('--self-test') && !CLOSED_FLOOR.valid) {
12900+
console.error(
12901+
`check-half-states: ${CLOSED_FLOOR.source}=${JSON.stringify(CLOSED_FLOOR.raw)} is not a ` +
12902+
'`YYYY-MM-DD` date. Refusing to fall back to an unfloored closed pass — on the install ' +
12903+
'that needs a floor, no floor is a report about the convention rather than about defects.',
12904+
);
12905+
process.exit(2);
12906+
}
1273312907
if (process.argv.includes('--self-test')) {
1273412908
selfTest();
1273512909
} else if (process.argv.includes('--probe')) {

0 commit comments

Comments
 (0)