From db16743da1bbba12a50c2a4d45a74a410a4cee82 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 16:10:17 +0000 Subject: [PATCH] feat(pm): report a `[Decision]`-titled card sitting in the dispatch pool (H54) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `pm:queue` is the dispatch pool: a PM seat reads it as capacity and picks from it. A card whose title opens `[Decision]` declares its deliverable is a ruling, and a PM seat may not rule a contract or product decision — so the one act the pool invites is the one act forbidden on that card. The filing card measured the cost on one lane twice: 8 read as dispatchable and 1 was, then 21 read and at most 3 were. H54 is a report-only row over the `pm:queue` label page this sweep's loop already consumes: one label and one title, both fields of a payload already paid for, so the row costs no request and its population is covered by construction. It fires on an OPEN issue (never a PR, never a closed card) carrying `pm:queue` whose title, after optional leading whitespace, begins with `[Decision]` — anchored at the start, never a substring. The row reports the DISAGREEMENT between two carriers and hands it to the triage seat, which owns the second one. It never relabels, never treats the prefix as the state (the prefix is the filer's, the state is triage's), and needs no new label: the vocabulary already carries the state. H26's deferred-target note said this shape was deliberately not reported because a title heuristic would make the sweeper guess at intent. That stance was about H26 inferring a block's fate from its target's title and it stands — H26 is not widened. Both prose sites now point at H54 so the file does not contradict itself; two self-test cases pin the pointers and pin that H26's own criterion survives at both. Self-tests: 43 cases — a positive control plus six negatives that each differ from it in exactly one feature, driven over a length-pinned table so a fixture that stops being exercised fails. Claude-Session: https://claude.ai/code/session_01MoTv7pn338AZ71owsp19gQ Co-authored-by: Claude --- scripts/pm/check-half-states.mjs | 277 ++++++++++++++++++++++++++++++- 1 file changed, 268 insertions(+), 9 deletions(-) diff --git a/scripts/pm/check-half-states.mjs b/scripts/pm/check-half-states.mjs index 7444122787..becd0cecd5 100644 --- a/scripts/pm/check-half-states.mjs +++ b/scripts/pm/check-half-states.mjs @@ -543,10 +543,14 @@ * ⛔ Not a judgement that the block is wrong — waiting on a deferred card * is sometimes right; the row says the wait has no releasing mechanism, * which is a fact a human should be handed rather than discover. - * Deliberately NOT reported: a target labelled `pm:queue` while titled - * `[Decision]` (one of the six). That is a mislabelling, not a fact in - * the labels, and a title heuristic would make this sweeper guess at - * intent. + * NOT reported BY THIS ROW: a target labelled `pm:queue` while titled + * `[Decision]` (one of the six). The reason is unchanged and this row is + * ⛔ NOT widened to chase it — H26 reads LABELS, and inferring a BLOCK's + * fate from its target's title would make this sweeper guess at intent. + * What changed is that the shape has a row of its own since #16688: + * H54 reports the title/label disagreement itself, on the card that + * carries it, and hands it to the triage seat — ⛔ still never reading a + * prefix as a state. * * ## H27 — the claim is perfect and the claimant is dead * @@ -5213,11 +5217,15 @@ export function h31ContractReviewCarrierSplit(issue, openPrs) { // objectos#75, #135 -> objectos#68 (`needs-user-decision`) // cloud#1332 -> cloud#1331 (`pm:queue`, titled `[Decision]`) // -// The last row is deliberately NOT reported by this predicate: a decision card -// wearing a work label is a mislabelling to fix, not a fact readable from the -// labels this row reads, and inventing a title heuristic would make the sweeper -// guess at intent. Two of the rows are one repo's ENTIRE blocked inventory -// waiting on its one unanswered decision card — one ruling clears the repo. +// The last row is not reported by THIS predicate, and the reason has not moved: +// this row reads LABELS, and inferring a BLOCK's fate from its target's title +// would make the sweeper guess at intent — so it is ⛔ not widened to chase it. +// The shape is no longer unread, though, and this note would otherwise say the +// file leaves it to nobody: since #16688 it has a row of its own — +// H54 reports the title/label disagreement on the card that carries it and +// hands it to the triage seat, ⛔ never reading a prefix as a state. Two of the +// rows are one repo's ENTIRE blocked inventory waiting on its one unanswered +// decision card — one ruling clears the repo. // // ## The second leg: the stale chain // @@ -10466,6 +10474,148 @@ export function h53CarrierWithoutIncrement(issue, commentRows, openPrs, mergedPr ); } +// --------------------------------------------------------------------------- +// H54 — a `[Decision]`-titled card sitting in the DISPATCH pool (#16688). +// +// ## The shape, and the acceptance criterion that names it +// +// `pm:queue` is the dispatch pool: a PM seat reads it as CAPACITY and picks +// from it. A card whose title opens `[Decision]` declares that its deliverable +// is a RULING — and 「⛔ PM 席不得裁决契约或产品决定」, so the one act the pool +// invites is the one act forbidden on this card. The filing card measured the +// cost on one lane, twice: 8 cards read as dispatchable and 1 was (2026-09-07), +// then 21 read and at most 3 were (2026-09-09) — the ratio did not improve, the +// set grew and the over-read grew with it. Each of the others cost a full read, +// body AND comments, to establish that it could not be picked up, and the next +// seat pays it again because nothing writes the reading back. +// +// Triage's acceptance criterion asks for exactly this shape and no more (its +// §验收口径 2 on the filing card): 「一张标题以 `[Decision]` 开头的卡带着 +// `pm:queue`」 — 「这是今天唯一一个"约束只由标题前缀表达、而无查询会读"的地方, +// 也是最便宜的一道网」. +// +// ## ⛔ The prefix is NOT the state, and this row never treats it as one +// +// The same ruling fences the row in the same breath: 「⛔ 不要把闸门做成"标题前缀 +// 即状态":前缀是作者写的,状态是分诊判的,让前者决定后者就是把分诊席的职责交给了 +// 立卡人」. So this row reports a DISAGREEMENT between two carriers and hands it +// to the seat that owns the second one. It does not say the card is +// mislabelled, does not say the prefix is right, and — like every row in this +// file — writes nothing. Which carrier is wrong is triage's judgement, and the +// remedy sentence says so rather than prescribing a relabel. +// +// ⛔ And no new label, by the same ruling: 「不要新增标签。你已经证明词汇表够用」. +// `needs-user-decision` is live on cards and PRs and `pm:awaiting-maintainer` +// is live on cards, so the state the remedy points at already exists and the +// deliverable triage ruled for is 逐卡改判, not a new state. +// +// ## Why the row is worth its line, given that a reader can see the title +// +// A seat sizing a round reads a LISTING. The over-read is not that the title is +// unreadable — it is that nothing QUERIES it: the constraint 「this card needs a +// decision, not a developer」 lives only in a prefix the state machine never +// consults, which is the filing card's own thesis (「A constraint that lives +// only in prose is invisible to a seat reading labels」). This row is that +// query, and it is the whole of what a title can honestly buy. +// +// ⚠️ Deliberately the CHEAP net and not the complete one. The filing thread +// records three further readings this row does NOT attempt, every one of them +// true: a ruled card and a never-ruled card look identical from the body, so a +// `[Decision]` title outlives its own ruling and this row will fire on a card +// whose question was answered days ago; the same lane's largest bucket turned +// out to be contract-review-TIER starvation rather than rulings (8 cards +// against 2); and a hot-file hold releases on MERGE, so its answer changes +// under the reader on a timescale of hours. None of those is readable from a +// title or a label, ⛔ so none of them is claimed here. This row answers one +// question — do the title and the state label disagree — and its remedy names +// the reader who can answer the rest. +// +// ## Quota +// +// Free, and the population is covered BY CONSTRUCTION for H45's reason: the +// carrier holds `pm:queue`, which is a label page this sweep's loop already +// consumes, and the title rides in on that same payload. Nothing here adds a +// request, and no card outside the listing can reach the row. +// --------------------------------------------------------------------------- + +/** + * The title prefix by which a card declares its deliverable is a RULING. + * + * Anchored at the START, after optional leading whitespace, and never a + * substring: `[Decision]` occurring later in a title is an ordinary mention + * (「rename the [Decision] prefix convention」), and reporting that would be the + * intent-guessing the ruling forbids. All nine live carriers measured on + * 2026-09-09 open with it, so the anchor costs no coverage. + * + * Case-insensitive on the WORD and exact on the brackets. `[decision]` and + * `[Decision]` are one author's one intent, and this file's standing doctrine + * is that an unknown reading must never present as a clean one (#4690) — a + * carrier missed on its capitalisation would read as a clean queue. The closing + * bracket is required immediately after the word, so `[Decisions log]` and + * `[Decision-log]` are NOT this prefix; and `[finding]` — the 80-strong prefix + * that must never be caught — differs in the word itself, not in its case. + */ +export const DECISION_TITLE_PREFIX = /^\s*\[decision\]/iu; + +/** + * Does H54 speak about this card at all? + * + * ⛔ ISSUES only, never a PR. Every listing feeding this sweep already filters + * `!i.pull_request`, so the guard is defence in depth rather than a live + * filter — but the row's subject is the DISPATCH POOL and a PR is not in it, so + * a predicate that COULD fire on one would have a population that is an + * accident of its caller rather than a property of the row. + * + * A CLOSED card is out, on H24's gate in mirror image: `pm:queue` on a closed + * card is residue, counted into the closed census since #14072, and a card + * nobody can dispatch is a card nothing over-reads as capacity. + * + * An unreadable `labels` is out and is never read as "unlabelled" — the + * three-input-states discipline (#4690) applied to the one field that decides + * membership. + */ +export function h54SpeaksAbout(issue) { + if (issue?.pull_request) return false; + if (issue?.state === 'closed') return false; + if (!Array.isArray(issue?.labels)) return false; + return labelNames(issue).includes('pm:queue'); +} + +/** + * H54 — null when clean, else the finding sentence. + * + * @param {object} issue — a card from a listing this sweep already holds. + */ +export function h54DecisionTitledQueueCard(issue) { + if (!h54SpeaksAbout(issue)) return null; + if (!DECISION_TITLE_PREFIX.test(String(issue?.title ?? ''))) return null; + return ( + '`pm:queue` while the TITLE opens `[Decision]` — the two carriers disagree about what this ' + + 'card is for. The label puts it in the DISPATCH pool, which a PM seat reads as capacity and ' + + 'picks from; the prefix declares the deliverable is a RULING, and 「⛔ PM 席不得裁决契约或产品' + + '决定」 — so the one act the pool invites is the one act forbidden here. Measured on one lane, ' + + 'twice: 8 cards read as dispatchable and 1 was, then 21 read and at most 3 were. The others ' + + 'each cost a full read — body AND comments — to establish they could not be picked up, and ' + + 'the next seat pays it again, because nothing writes the reading back. ' + + '⛔ This row does NOT say which carrier is wrong: 「前缀是作者写的,状态是分诊判的,让前者决定' + + '后者就是把分诊席的职责交给了立卡人」, so it reports the disagreement and names the reader. ' + + 'Remedy, and it belongs to the TRIAGE seat, who re-judges the card: if the decision has ' + + 'already been RULED, the title is the last thing still advertising an open question — drop ' + + 'the prefix, or point the body at the ruling comment, and `pm:queue` stands as it is (a ruled ' + + 'card and a never-ruled card look identical from the body, which is exactly why the title ' + + 'must stop saying otherwise); if it is genuinely UNRULED it belongs in the decision inbox — ' + + '`needs-user-decision`, carrying its four-facet analysis at the moment the label lands, ⛔ not ' + + 'left for the maintainer to fill in on arrival. ⛔ No new label is needed or wanted: the ' + + 'vocabulary already carries the state (`needs-user-decision` on cards and PRs, ' + + '`pm:awaiting-maintainer` on cards), and the ruled deliverable is 逐卡改判 rather than a new ' + + 'state. Report-only: this row writes no label, relabels nothing, and its verdict is a ' + + 'disagreement between two carriers — ⛔ never a finding that the card is mislabelled. ' + + '⚠️ It is the cheapest net and not the complete one: a hot-file hold releases on MERGE, a ' + + 'contract-review-tier wait shows in no carrier at all, and a ruled card keeps its prefix ' + + 'until someone edits the title. ⛔ None of those is readable here, and none is claimed.' + ); +} + // --------------------------------------------------------------------------- // Report rendering — pure over (findings, counts), so `--self-test` pins both // media offline. The live sweep below picks a renderer and prints it; nothing @@ -11594,6 +11744,20 @@ export const HALF_STATE_FAMILY_BAND = Object.freeze({ // though the first census found fourteen at once. H53: 'state', + // H54 is a `state`, and each of the other three bands is refused for its own + // stated criterion. ⛔ Not `gate`: that band exists for the row that can tell + // a STRIPPED gate from an ungated card — an ABSENCE reading as a green light + // — and here both carriers are present and legible; nothing was removed. + // ⛔ Not `stall`: whether any of these cards is actually STOPPED is UNMEASURED + // by this row, and the filing thread's own correction found the lane's binding + // constraint to be contract-review-tier headroom rather than rulings — so the + // row claims no card is halted. ⛔ Not `inventory`: it alarms about one card, + // not a population, even though the first census found nine at once. What is + // left is `state`'s definition exactly — two carriers on one LIVE card + // contradicting each other, the whole repair on the board, in one seat's + // judgement. + H54: 'state', + // H52 is a `stall` and not a `state` (#16662): the board is not contradicting // itself — every label on the card is correct — and no later sweep frees the // card, because the only thing that moves it is a maintainer who cannot see @@ -14561,6 +14725,13 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen "still the epic's.", ]); } + // H54 — the same free read one field further: a label AND a title, both of + // them fields of a payload this loop already paid for. Its population is + // covered BY CONSTRUCTION for H45's reason — the carrier holds `pm:queue`, + // which is a page this loop consumes, so the row can never be about a card + // the sweep did not list. + const decisionInQueue = h54DecisionTitledQueueCard(issue); + if (decisionInQueue) findings.push([issue, 'H54', decisionInQueue]); // H24 + H25 — two field/label intersections over cards this loop already // holds, so neither costs a request. H24's population is the `pm:queue` // listing; H25's carriers are all listed too, either by the awaiting label @@ -22572,6 +22743,94 @@ Mutual exclusion: \`get_comments\` page 747 → \`[]\`, page 746 = my own R+117 t('H53 summary: the clause is rendered on EVERY run, not just interesting ones', saidBy('h53Carrier', summaryLine({}, 0)).includes('0 of 0'), true); t('H53 summary: a bare line renders numbers, never `undefined`', saidBy('h53Carrier', summaryLine({}, 0)).includes('undefined'), false); + // -- H54 — a `[Decision]`-titled card sitting in the DISPATCH pool (#16688) - + // The fixtures are SYNTHETIC (⛔ the self-test never touches GitHub); their + // shapes come from the nine live carriers measured 2026-09-09 and from the + // two prefixes that must stay apart on this board — `[Decision]` (9 live) and + // `[finding]` (80 live), which is the one that must NOT be caught. + const card54 = (extra = {}) => ({ + number: 15071, + state: 'open', + labels: [{ name: 'pm:queue' }], + assignees: [], + body: '', + title: '[Decision] sandboxed CRASH vs the unwrap door', + ...extra, + }); + const h54 = (extra = {}) => h54DecisionTitledQueueCard(card54(extra)); + + // ⭐ The positive control, then SIX negatives — each differing from the + // positive in exactly ONE feature, so a case can only be answered by the + // feature its name claims. The table is length-pinned and then driven whole: + // a fixture that stops being exercised fails here rather than going quiet. + t('H54 fires: a `[Decision]`-titled open card carrying `pm:queue`', typeof h54(), 'string'); + const NEGATIVES54 = [ + ['`pm:queue` with no prefix — the ordinary dispatchable card', { title: 'client SDK `organizations.invite` accepts no `role?`' }], + ['a `[finding]` prefix — the 80-strong prefix that must NOT be caught', { title: '[finding] `domain:cli`\'s `pm:queue` reads as 8 dispatchable cards and is 1' }], + ['`[Decision]` carrying `needs-user-decision` and NOT `pm:queue` — already in the inbox', { labels: [{ name: 'needs-user-decision' }] }], + ['a CLOSED card — nothing over-reads it as capacity', { state: 'closed' }], + ['a PULL REQUEST — the dispatch pool is cards, ⛔ never PRs', { pull_request: { url: 'x' } }], + ['`[Decision]` LATER in the title — a mention, not the prefix', { title: 'rename the [Decision] title convention' }], + ]; + t('H54 controls: the negative table is exercised WHOLE — six fixtures, none quietly dropped', NEGATIVES54.length, 6); + for (const [why, extra] of NEGATIVES54) t(`H54 silent: ${why}`, h54(extra), null); + + // The anchor, read from both sides. + t('H54 anchor: leading whitespace still opens the title', typeof h54({ title: ' [Decision] rename `specVersionGap` to `protocolVersionGap`' }), 'string'); + t('H54 anchor: a lowercase spelling is one author\'s one intent, not a clean queue', typeof h54({ title: '[decision] does a declared 5xx on a polled route log per request' }), 'string'); + t('H54 anchor: ⛔ `[Decisions log]` is not this prefix — the bracket closes on the word', h54({ title: '[Decisions log] the batch' }), null); + t('H54 anchor: ⛔ nor `[Decision-log]`', h54({ title: '[Decision-log] the batch' }), null); + t('H54 anchor: ⛔ nor the bare word without brackets', h54({ title: 'Decision: rename the field' }), null); + t('H54 anchor: a card with no title at all is clean, never a crash', h54({ title: undefined }), null); + + // Population — the same three-input-states care every row here takes. + t('H54 population: an open `pm:queue` card is in', h54SpeaksAbout(card54()), true); + t('H54 population: a closed one is out', h54SpeaksAbout(card54({ state: 'closed' })), false); + t('H54 population: a PR is out', h54SpeaksAbout(card54({ pull_request: { url: 'x' } })), false); + t('H54 population: an unreadable `labels` is out, never read as unlabelled', h54SpeaksAbout(card54({ labels: undefined })), false); + t('H54 population: a missing card is out, never a crash', h54SpeaksAbout(undefined), false); + t('H54: …and the predicate itself does not crash on one either', h54DecisionTitledQueueCard(undefined), null); + + // Adjacency — ⛔ this row restates no neighbour's verdict on its own specimen. + t('H54 adjacency: H24 is silent — the card is UNASSIGNED, which is correct for `pm:queue`', h24QueuedWithAssignee(card54()), null); + t('H54 adjacency: H3 is silent — one pm state label, not two', h3QueueAndDispatched(card54()), false); + t('H54 adjacency: H45 is silent — `pm:epic` is not in play', h45EpicAndQueued(card54()), false); + + // The row's own sentence — the halves triage's acceptance criterion requires. + t('H54 row: it reports a DISAGREEMENT rather than a verdict', h54().includes('the two carriers disagree'), true); + t('H54 row: …and quotes the prohibition that makes the pool dangerous here', h54().includes('⛔ PM 席不得裁决契约或产品决定'), true); + t('H54 row: ⛔ the prefix is NOT the state, in the ruling\'s own words', h54().includes('让前者决定后者就是把分诊席的职责交给了立卡人'), true); + t('H54 row: the remedy names the reader — the TRIAGE seat, who re-judges the card', h54().includes('belongs to the TRIAGE seat, who re-judges the card'), true); + t('H54 row: …the RULED branch drops the prefix and leaves `pm:queue` standing', h54().includes('`pm:queue` stands as it is'), true); + t('H54 row: …the UNRULED branch names the inbox AND its four-facet block', h54().includes('`needs-user-decision`, carrying its four-facet analysis'), true); + t('H54 row: ⛔ no new label — the vocabulary already carries the state', h54().includes('⛔ No new label is needed or wanted'), true); + t('H54 row: report-only — it writes nothing and relabels nothing', h54().includes('writes no label, relabels nothing'), true); + t('H54 row: it declares what it CANNOT see, so a reader does not over-trust it', h54().includes('the cheapest net and not the complete one'), true); + t('H54 row: not a loud finding', isLoudFinding(h54()), false); + + // Band + registry. + t('H54 band: registered as a STATE row — the repair is on the board', familyBand('H54'), 'state'); + t('H54 band: ⛔ NOT `stall` — this row claims no card is stopped', familyBand('H54') === 'stall', false); + t('H54 band: ⛔ NOT `gate` — both carriers are PRESENT, so no absence reads as a green light', familyBand('H54') === 'gate', false); + t('H54 band: …and the sweep really pushes it, so the registry sees it', familyRegistryCoverage().emitted.includes('H54'), true); + t('H54 band: no code is left unregistered by this change', familyRegistryCoverage().missing.length, 0); + t('H54 band: …and no band names a family the sweep never emits', familyRegistryCoverage().extra.length, 0); + t('H54 band: the registry still fits inside the ledger ROW CAP', Object.keys(HALF_STATE_FAMILY_BAND).length <= FAMILY_LEDGER_ROW_CAP, true); + t('H54 band: a gate row still outranks it, so H31/H35 survive the trim longer', familyRank('H31') < familyRank('H54'), true); + + // ⚖️ The reconciliation half. H26's deferred-target note said this shape was + // 「deliberately NOT reported … a title heuristic would make this sweeper guess + // at intent」. That stance was about H26 inferring a BLOCK's fate from its + // TARGET's title and it stands — ⛔ H26 is not widened, and both sites still + // carry its criterion. What changed is that both now point at the row that + // does read the shape, so the file no longer contradicts itself. Pinned on the + // source, because a pointer nobody checks is the declared-not-enforced shape + // this file spends its length refusing. The needles are ASSEMBLED at runtime + // so these two cases cannot satisfy themselves out of their own source text. + const SELF54 = readFileSync(SELF_PATH, 'utf8'); + t('H54 reconciliation: BOTH deferred-target prose sites point at this row', SELF54.split(['H54 reports the title', 'label disagreement'].join('/')).length - 1, 2); + t('H54 reconciliation: ⛔ and both keep H26\'s own criterion — the row is NOT widened', SELF54.split(['guess at', 'intent'].join(' ')).length - 1, 2); + // -- The `[::]` collapse (#12090): behaviour-preserving, asserted as such --- // The class held U+003A TWICE, never the fullwidth U+FF1A its shape implied. // These cases pin that the collapse changed nothing a reader could observe.