diff --git a/.claude/skills/pm-dispatch/SKILL.md b/.claude/skills/pm-dispatch/SKILL.md index 9e5c43b43a..ec05194278 100644 --- a/.claude/skills/pm-dispatch/SKILL.md +++ b/.claude/skills/pm-dispatch/SKILL.md @@ -120,10 +120,10 @@ seat-post-protocol);维护者明示召唤是仲裁:有简报径直坐席,无简 默认不适用 `type:Bug` 的 hold**,三分支:可复现且用户可达 ⇒ 回 `pm:queue`(被 hold 的 Bug 就是 被藏起的缺陷);declared≠enforced 观察类 ⇒ 转 enforce-or-remove 通道(不 hold 不裸关);真 won't-fix 候选 ⇒ 逐卡进决策箱 —— 座位永不自行关闭真实缺陷。**机会主义重启条件必须 - 点名触发文件**(维护者 2026-08-11 接受):写侧即 `Restart-touch:` 行(行契约见 - `references/state-machine.md`);派发/折叠检查时读半状态巡查锚(`half-state-patrol.yml` 的 - `ANCHOR_ISSUE` 置顶 issue)的 H17 on-hold 触发文件索引、与本次派发文件面相交,命中 ⇒ 按该 - hold 评论的 rider/restart 条款处置(点名该单、顺手活列为申报过的增项)。 + 点名触发文件**(维护者 2026-08-11 接受):派发/折叠检查时读半状态巡查锚(`half-state-patrol.yml` + 的 `ANCHOR_ISSUE` 置顶 issue)的 H17 on-hold 触发文件索引、与本次派发文件面相交,命中 ⇒ + 按该 hold 评论的 rider/restart 条款处置(点名该单、顺手活列为申报过的增项)。 +- **关闭即摘 `pm:*` 状态标**(与关单同一笔;`domain:*`/类型标签留下 —— 归属不是状态)。 - **`Blocked-by:` 行是机器可 grep 的反向索引**,一遍读喂三个职责:上游关单时放回被解锁的、 按解锁扇出排序选择、**在合并后的 ref 上重验每张回队卡的文件面**(⛔ 只做第一件;关掉 上游的那个合并最可能顺手把你这张卡也修掉)。**一个标签存在,当且仅当有具名读 diff --git a/scripts/pm/check-half-states.mjs b/scripts/pm/check-half-states.mjs index b9df3b2f68..6d08189c69 100644 --- a/scripts/pm/check-half-states.mjs +++ b/scripts/pm/check-half-states.mjs @@ -3809,9 +3809,30 @@ export const PM_RESIDUE_LABELS = [ * predicate cannot double-report the population every other item already reads. * That gate is the predicate's own, not the caller's, because it is the one * thing separating this row from a restatement of H3. + * + * `floor` is the optional dated closure floor (see `resolveClosureFloor`): a + * `Date` before which a closed card is out of scope, or null for "judge every + * card in the window", which is the default and this repo's own behaviour. + * + * ⚠️ A card whose `closed_at` cannot be read is judged, NOT skipped. The floor + * is a scope decision that needs a date to make; without one the card's + * position relative to the cutover is UNKNOWN, and silently dropping it would + * narrow the pass on unread data — #4690 in the direction this file refuses + * everywhere else. The listing endpoint always carries `closed_at` on a closed + * issue, so fail-open costs no noise in practice; it just keeps the one + * unreadable card visible instead of disappeared. + * + * @param {any} issue + * @param {Date | null} [floor] */ -export function h22ClosedCardPmResidue(issue) { +export function h22ClosedCardPmResidue(issue, floor = null) { if (issue?.state !== 'closed') return null; + if (floor) { + const closedAt = Date.parse(issue.closed_at ?? ''); + // Strictly BEFORE the floor is out of scope; a card closed ON the cutover + // date is the first day the convention applies and is judged. + if (!Number.isNaN(closedAt) && closedAt < floor.getTime()) return null; + } const residue = labelNames(issue ?? {}).filter((l) => PM_RESIDUE_LABELS.includes(l)); if (residue.length === 0) return null; const list = residue.map((l) => `\`${l}\``).join(', '); @@ -6692,7 +6713,7 @@ export const SWEEP_COUNT_KEYS = [ * blockerTargets?: number, crossRepoProbed?: number, * crossRepoUnreadable?: number, seatMarkersRead?: number, * seatCandidates?: number, commits?: number, commitBindings?: number, - * commitBindingMessages?: number }} counts + * commitBindingMessages?: number, closedFloor?: string }} counts * @param {number} findingCount */ @@ -6772,7 +6793,9 @@ export function summaryLine(counts, findingCount) { `(merge state read on ${probed} of ${candidates} H16 candidate(s)) ` + `and ${counts.merged} recently-merged PR(s) in ${counts.repo} — ${findingCount} half-state(s) found. ` + `H22 read ${counts.closed ?? 0} recently-closed issue(s) for \`pm:*\` state residue (bounded window; ` + - `older closed carriers are outside it by design). ` + + `older closed carriers are outside it by design` + + `${counts.closedFloor ? `, and only cards closed on/after ${counts.closedFloor} are judged — ` + + 'earlier closures predate the strip-on-close convention and are NOT a reading about them' : ''}). ` + `H23 read ${commits} squash commit message(s) from the default branch's recent window, carrying ` + `${commitBindings} closing-keyword binding(s) across ${commitBindingMessages} message(s) ` + `(bounded window; a message that landed before it is invisible by design). ` + @@ -7869,6 +7892,79 @@ async function listRecentlyMergedPullRequests() { */ export const CLOSED_ISSUE_WINDOW_PAGES = 4; +/** + * H22's DATED CLOSURE FLOOR — the cutover date at and after which a closed + * card's `pm:*` residue is judged (objectui#5985). + * + * ## The dilemma this dissolves + * + * The window above is bounded by UPDATE recency, which is the wrong axis for + * the one question a sibling install kept running into: "was this card closed + * under the convention, or before it existed?" Measured in objectui + * 2026-08-24, while porting this file: 815 closed cards there carry + * `pm:dispatched`, and ~347 of the 400 issues in the window above carry some + * `pm:*` residue (~87%, against the 26% this repo measured on its own board). + * At that density H22 reports the CONVENTION rather than a defect — ~347 rows + * that exhaust the anchor body budget and trim every other predicate's + * findings out of the report. That install therefore shipped with the closed + * reader switched off, and its card recorded the choice as a two-way one: + * either stripping is the rule (and ~815 cards need a BACKFILL before H22 can + * be honest) or it is not (and H22 is simply not a predicate that repo wants). + * + * The floor is the third option both readings omit. `pm:*` on a card closed + * before the convention was written is inert history: nothing queries it as a + * claim of in-flight-ness, because the loop reads state on OPEN cards only + * (`is:open` is in every one of its inventory queries). Judging only cards + * closed on/after a cutover date therefore buys the row's whole value — the + * residue produced from now on, while the paired write is still a live duty + * someone remembers — at zero backfill and zero historical noise. ⛔ The + * alternative this file must never grow is a bulk label rewrite of closed + * cards: 815 mutating writes to make a report quieter is machinery serving the + * instrument, and no code path here writes a label at all. + * + * ## Default: unset, which is exactly today's behaviour + * + * An install that wants every card in the window judged sets nothing, and this + * resolver returns a null floor that the predicate ignores. That keeps this + * repo's own patrol byte-identical across this change — it measured 26% and + * treats recent closed residue as a live duty — and makes the floor a + * per-install adaptation rather than a policy shipped to everyone. + * + * ## Malformed is REFUSED, never defaulted + * + * A typo'd floor that silently became "no floor" would restore the 87% flood + * on the one install that set it, four times a day, and the flood reads as a + * working patrol — the same trap `resolveSweepRepo` refuses by name. So an + * unparseable value is `valid: false` and the entrypoint exits 2 on it. Only + * the `YYYY-MM-DD` spelling is accepted: a bare `Date` parse would take + * "yesterday-ish" strings and timezone-bearing ones whose midnight is not the + * one the workflow author meant, and the value is written by hand in a + * workflow file exactly once. + */ +export function resolveClosureFloor(env = {}) { + const raw = String(env.PM_SWEEP_CLOSED_FLOOR ?? '').trim(); + if (!raw) return { floor: null, source: 'default', valid: true, raw: '' }; + if (!/^\d{4}-\d{2}-\d{2}$/.test(raw)) { + return { floor: null, source: 'PM_SWEEP_CLOSED_FLOOR', valid: false, raw }; + } + const at = Date.parse(`${raw}T00:00:00Z`); + if (Number.isNaN(at)) return { floor: null, source: 'PM_SWEEP_CLOSED_FLOOR', valid: false, raw }; + // A shape-valid string can still name a date that does not EXIST, and + // `Date.parse` does not reject all of them: `2026-13-01` is NaN (the month + // is outside the ISO range) but `2026-02-31` silently ROLLS to 2026-03-03. + // So the parse is round-tripped rather than trusted. Letting a rolled date + // through would move the floor days past where its author wrote it and, + // worse, do it silently — the floor is the one input here whose whole job is + // to say which cards were judged. + const floor = new Date(at); + if (floor.toISOString().slice(0, 10) !== raw) { + return { floor: null, source: 'PM_SWEEP_CLOSED_FLOOR', valid: false, raw }; + } + return { floor, source: 'PM_SWEEP_CLOSED_FLOOR', valid: true, raw }; +} + +const CLOSED_FLOOR = resolveClosureFloor(process.env); + async function listRecentlyClosedIssues() { const out = []; for (let page = 1; page <= CLOSED_ISSUE_WINDOW_PAGES; page++) { @@ -8351,9 +8447,10 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen // line can say what this pass covered on its own terms. for (const issue of await listRecentlyClosedIssues()) { seenClosed.set(issue.number, issue); - const residue = h22ClosedCardPmResidue(issue); + const residue = h22ClosedCardPmResidue(issue, CLOSED_FLOOR.floor); if (residue) findings.push([issue, 'H22', residue]); } + stats.closedFloor = CLOSED_FLOOR.raw; // H23 — the commit-message surface (#10942). The counting is not incidental: // this row's measured yield is ~6 in 1,546, so a silent H23 is the normal @@ -9557,10 +9654,75 @@ function selfTest() { t(`H22: \`${label}\` on a closed card is residue`, typeof h22ClosedCardPmResidue(closedCard([label])), 'string'); } + // -- H22's DATED CLOSURE FLOOR (objectui#5985) ------------------------------ + // + // The floor is what lets a sibling install re-enable this row without the + // backfill its own card thought was the only alternative: judge cards closed + // on/after a cutover date, leave the historical carriers unjudged, write no + // labels at all. The cases below pin the three properties that decision rests + // on — the floor is HONOURED, its absence changes nothing, and a malformed + // value is refused rather than silently becoming "no floor". + const FLOOR = new Date(Date.parse('2026-08-28T00:00:00Z')); + const closedOn = (labels, closed_at) => ({ ...closedCard(labels), closed_at }); + + // Honoured, both directions. The old card is the ~815-card backlog in + // miniature: it carries real residue and is deliberately NOT a finding. + t('H22 floor: a card closed BEFORE the floor is out of scope', h22ClosedCardPmResidue(closedOn(['pm:dispatched'], '2026-08-01T09:00:00Z'), FLOOR), null); + t('H22 floor: …however much residue it carries', h22ClosedCardPmResidue(closedOn(['pm:dispatched', 'pm:queue', 'pm:blocked'], '2026-01-01T00:00:00Z'), FLOOR), null); + t('H22 floor: a card closed AFTER the floor is judged', typeof h22ClosedCardPmResidue(closedOn(['pm:dispatched'], '2026-08-29T09:00:00Z'), FLOOR), 'string'); + 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); + // The boundary is inclusive: the cutover date is the first day the convention + // applies, so a card closed within it is the convention's own population. + t('H22 floor: a card closed ON the floor date is judged', typeof h22ClosedCardPmResidue(closedOn(['pm:queue'], '2026-08-28T00:00:00Z'), FLOOR), 'string'); + t('H22 floor: …and later the same day too', typeof h22ClosedCardPmResidue(closedOn(['pm:queue'], '2026-08-28T23:59:59Z'), FLOOR), 'string'); + t('H22 floor: one second before the floor is out', h22ClosedCardPmResidue(closedOn(['pm:queue'], '2026-08-27T23:59:59Z'), FLOOR), null); + // The floor narrows scope; it never invents findings. A clean recent card is + // still clean, and an OPEN card is still not this row's. + t('H22 floor: a clean card after the floor is still clean', h22ClosedCardPmResidue(closedOn(['domain:cli'], '2026-08-29T09:00:00Z'), FLOOR), null); + t('H22 floor: the closed gate still outranks the floor', h22ClosedCardPmResidue({ ...issue(['pm:dispatched']), state: 'open', closed_at: null }, FLOOR), null); + // Fail-OPEN on an unreadable closure date: the floor cannot be applied, so + // the card stays visible rather than being dropped on unread data (#4690). + t('H22 floor: a card with no closed_at is judged, not dropped', typeof h22ClosedCardPmResidue(closedOn(['pm:dispatched'], null), FLOOR), 'string'); + t('H22 floor: …and an unparseable one likewise', typeof h22ClosedCardPmResidue(closedOn(['pm:dispatched'], 'not-a-date'), FLOOR), 'string'); + + // Floor ABSENT — the default, and the property that makes this change a + // no-op for the install that wants every card in the window judged. + t('H22 floor: absent floor judges an old closed card exactly as before', typeof h22ClosedCardPmResidue(closedOn(['pm:dispatched'], '2026-01-01T00:00:00Z')), 'string'); + t('H22 floor: …an explicit null is the same as omitting it', typeof h22ClosedCardPmResidue(closedOn(['pm:dispatched'], '2026-01-01T00:00:00Z'), null), 'string'); + t('H22 floor: …and a clean old card is still clean', h22ClosedCardPmResidue(closedOn(['domain:cli'], '2026-01-01T00:00:00Z'), null), null); + + // resolveClosureFloor — the env reading, including the loud refusal. + t('closure floor: unset means no floor', resolveClosureFloor({}).floor, null); + t('closure floor: …and that is a VALID reading, not an error', resolveClosureFloor({}).valid, true); + t('closure floor: …reported as the default source', resolveClosureFloor({}).source, 'default'); + t('closure floor: whitespace is unset too', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: ' ' }).floor, null); + 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'); + t('closure floor: …and is valid', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: '2026-08-28' }).valid, true); + t('closure floor: …and names its source', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: '2026-08-28' }).source, 'PM_SWEEP_CLOSED_FLOOR'); + t('closure floor: surrounding whitespace is trimmed, not rejected', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: ' 2026-08-28 ' }).valid, true); + // Malformed is REFUSED. Each of these would otherwise become "no floor" and + // restore the flood on the one install that set the variable. + for (const bad of ['28-08-2026', '2026/08/28', 'yesterday', '2026-08-28T00:00:00Z', '2026-8-28', 'O', '0']) { + t(`closure floor: \`${bad}\` is refused, not defaulted`, resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: bad }).valid, false); + } + // …including a well-SHAPED date that does not exist — the case a bare regex + // would pass and whose floor would exclude every card, rendering an empty + // H22 as a clean closed surface. + t('closure floor: a shape-valid impossible date is refused', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: '2026-02-31' }).valid, false); + t('closure floor: …and an impossible month likewise', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: '2026-13-01' }).valid, false); + t('closure floor: a refused value carries no floor to fall back on', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: 'yesterday' }).floor, null); + t('closure floor: …and is reported as itself for the error message', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: 'yesterday' }).raw, 'yesterday'); + // The summary line's H22 clause — a pass that read nothing must not read the // same as a board with no residue (#4690), so the count is always stated. 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); 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); + // …and when a floor is in force the line SAYS so: "read 200" with a floor + // silently applied would overstate what was judged, which is the same + // unread-reads-as-clean defect the count itself exists to prevent. + 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); + 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); + 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); // -- H23: the COMMIT-MESSAGE surface (#10942) ------------------------------- // @@ -12730,6 +12892,18 @@ if (isMain) { ); process.exit(2); } + // A malformed closure floor is the same class and gets the same answer. It + // must not degrade to "no floor": the install that sets one is the install + // whose closed surface is ~87% residue, so a silent default would flood the + // anchor body four times a day and the flood renders as a working patrol. + if (!process.argv.includes('--self-test') && !CLOSED_FLOOR.valid) { + console.error( + `check-half-states: ${CLOSED_FLOOR.source}=${JSON.stringify(CLOSED_FLOOR.raw)} is not a ` + + '`YYYY-MM-DD` date. Refusing to fall back to an unfloored closed pass — on the install ' + + 'that needs a floor, no floor is a report about the convention rather than about defects.', + ); + process.exit(2); + } if (process.argv.includes('--self-test')) { selfTest(); } else if (process.argv.includes('--probe')) {