Skip to content

Commit 7599059

Browse files
committed
fix(pm): H9's issue-ref class requires the reference at the declaration position
`classifyRestartWhen`'s `issue-ref` step tested `/#\d+\b/` — an issue reference the value CARRIES anywhere — so a `Restart-when:` value that merely mentions a card mid-sentence read as a machine-fireable exit. On the 2026-09-11 census of the 104 open `pm:on-hold` cards that cleared four holds whose value actually names a tracked repo path (a misfiled `Restart-touch:` line), because each mentions an unrelated issue number in passing. The step now tests the DECLARATION POSITION: the value must START with `#N` or `owner/repo#N`, after the same leading decoration `partOfDeclarationRe` tolerates. That is H9's own advertised spelling (`Restart-when: #N rules on X`) read literally, so it costs no ruling and the class ORDER is untouched. A mid-sentence mention falls through to `command` / `tracked-path` / `prose` and fires the row its real shape earns. The `prose` row's remedy follows: a value that mentions a card without declaring one is now prescribed the declaration beside the generic "mark it `manual` or name the event", because an author who wrote a number down was not missing an event — they wrote it where nothing reads. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MCLBsUgfykL74aU716rzVK
1 parent 2070a1d commit 7599059

1 file changed

Lines changed: 164 additions & 15 deletions

File tree

‎scripts/pm/check-half-states.mjs‎

Lines changed: 164 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2412,14 +2412,30 @@ export function h8MergedPrStillDispatched(issue, mergedPrs, openPrs) {
24122412
// classifies on its SPELLING alone, whatever #N's reachability; the paragraph
24132413
// above says why that ruling is not this card's to make.
24142414
//
2415-
// The ruling spelling stays fireable: a value carrying an issue reference
2416-
// (`#N` or `owner/repo#N`) is `issue-ref`, tested BEFORE the two unfireable
2417-
// classes, so `Restart-when: #N rules on X` reads exactly as it did. That
2418-
// order has a measured price — on the 2026-09-11 census of the 104 open holds,
2419-
// four values name a tracked repo path and read clean on an unrelated issue
2420-
// mention (#8753, #8607, #8589, #6009). Recorded here, not worked around: the
2421-
// alternative is to rank `tracked-path` above `issue-ref`, which would make
2422-
// the advertised ruling spelling fire, and that is the question above.
2415+
// The ruling spelling stays fireable, and it is read at the DECLARATION
2416+
// POSITION (#17605): a value that STARTS with an issue reference (`#N` or
2417+
// `owner/repo#N`, after the leading decoration `partOfDeclarationRe`
2418+
// tolerates) is `issue-ref`, tested BEFORE the two unfireable classes, so
2419+
// `Restart-when: #N rules on X` reads exactly as this header advertises it. A
2420+
// reference the value merely CARRIES mid-sentence is a mention, not an exit,
2421+
// and falls through to `command` / `tracked-path` / `prose`.
2422+
//
2423+
// That position test is what RETIRED the price this note used to record. On
2424+
// the 2026-09-11 census of the 104 open holds, four values name a tracked repo
2425+
// path and read clean on an unrelated mid-sentence issue mention (#8753,
2426+
// #8607, #8589, #6009); under the declaration test all four classify
2427+
// `tracked-path` and get the rewrite row the class exists for. ⛔ The CLASS
2428+
// ORDER was NOT the instrument and is unchanged: ranking `tracked-path` above
2429+
// `issue-ref` would make the advertised ruling spelling fire, and that is the
2430+
// ruled question above. Narrowing the POSITION costs no ruling because it
2431+
// preserves that spelling literally — and it is the shape this file already
2432+
// carries for `Part of #N` (`partOfDeclarationRe`), not a new one.
2433+
//
2434+
// The `prose` row moves with it: a value that mentions a card without
2435+
// declaring one is prescribed the DECLARATION (「declare it: `Restart-when: #N
2436+
// rules on X`」) beside the generic 「mark it `manual` or name the event」,
2437+
// because an author who wrote a number down was not missing an event — they
2438+
// wrote it where nothing reads.
24232439
//
24242440
// ## H9's path test is WIDER than H17's, deliberately
24252441
//
@@ -2500,6 +2516,59 @@ export function restartWhenTrackedPaths(value, isTracked = () => false) {
25002516
return out;
25012517
}
25022518

2519+
/**
2520+
* An issue reference a `Restart-when:` value CARRIES anywhere.
2521+
*
2522+
* The anywhere-form, kept as a LABEL and never as the class test: it is what
2523+
* lets the `prose` row say 「this value mentions a card」 and prescribe the
2524+
* declaration, instead of reporting a missing event to an author who plainly
2525+
* had one in mind. Same pair as `partOfRe` / `partOfDeclarationRe`, with the
2526+
* roles reversed — there the wide form classifies and the narrow one labels;
2527+
* here the narrow one classifies and the wide one labels.
2528+
*/
2529+
function restartWhenIssueRefRe() {
2530+
return /#\d+\b/;
2531+
}
2532+
2533+
/**
2534+
* The same reference, restricted to the DECLARATION POSITION — the value
2535+
* STARTS with `#N` or `owner/repo#N`, after the leading decoration
2536+
* `partOfDeclarationRe` tolerates (blockquote markers, a list bullet, a
2537+
* markdown emphasis run).
2538+
*
2539+
* ## Why this is a class TEST here, where the `Part of` one is only a label
2540+
*
2541+
* `partOfDeclarationRe`'s docblock refuses to narrow its relation, and on its
2542+
* own corpus that refusal is right: narrowing there EMPTIES a set and drops a
2543+
* row through to a fallback channel, so a real declaration written second on
2544+
* a line is a finding LOST. Nothing is lost here. `classifyRestartWhen` is
2545+
* total — every value lands in one of six classes — so a value that fails
2546+
* this test is not dropped, it falls through to `command` / `tracked-path` /
2547+
* `prose` and fires a row carrying the remedy for the shape it actually has.
2548+
* The direction of the error is therefore opposite: the wide form here loses
2549+
* findings (a mention CLEARS the card), the narrow one gains them.
2550+
*
2551+
* ## The measured reason (#17605)
2552+
*
2553+
* On the 2026-09-11 census of the 104 open `pm:on-hold` cards, the wide form
2554+
* read four tracked-path holds clean on an unrelated issue number mentioned
2555+
* mid-sentence — #8753, #8607, #8589 and #6009, every one of them a misfiled
2556+
* `Restart-touch:` line, exactly the class #17377 built a row for. The
2557+
* declaration position is H9's own advertised spelling (`Restart-when: #N
2558+
* rules on X`) read literally, so the narrowing costs no ruling: #13718
2559+
* (`#13651 closes …`) and #3267 (`objectstack-ai/cloud#861 … is scheduled`)
2560+
* both start with their reference and stay `issue-ref`.
2561+
*
2562+
* The qualifier is this file's one reference grammar (`referenceRe`) — a full
2563+
* `owner/repo`, never a bare single segment, because `/` is what separates a
2564+
* cross-repo reference from a path segment. The NUMBER test is left exactly as
2565+
* the wide form spelled it (`#\d+\b`): this card narrows the POSITION and
2566+
* nothing else.
2567+
*/
2568+
function restartWhenIssueDeclarationRe() {
2569+
return /^[ \t]*(?:>[ \t]*)*(?:(?:[-*+]|\d{1,9}[.)])[ \t]+)?[*_]{0,3}(?:[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9][A-Za-z0-9._-]*)?#\d+\b/;
2570+
}
2571+
25032572
/**
25042573
* Which of the six shapes is this `Restart-when:` value? (#17377)
25052574
*
@@ -2510,9 +2579,12 @@ export function restartWhenTrackedPaths(value, isTracked = () => false) {
25102579
* the one-word spelling can never be rescued by anything
25112580
* that follows it
25122581
* 2. `closed-ref` the unlock sweep's own literal form
2513-
* 3. `issue-ref` any issue reference the value CARRIES — the ruling
2514-
* spelling H9's header admitted (`#N rules on X`), which
2515-
* is why it is tested before the two unfireable classes
2582+
* 3. `issue-ref` an issue reference the value DECLARES — one it opens
2583+
* with, which is the ruling spelling H9's header admitted
2584+
* (`#N rules on X`) read literally, and why it is tested
2585+
* before the two unfireable classes. A reference merely
2586+
* CARRIED mid-sentence is a mention, not an exit, and
2587+
* falls through to 4/5/6 (#17605)
25162588
* 4. `command` a backticked opening span, or one of the measured bare
25172589
* heads — a one-line executable predicate
25182590
* 5. `tracked-path` some token is a tracked repo file: a misfiled
@@ -2531,7 +2603,7 @@ export function classifyRestartWhen(value, isTracked = () => false) {
25312603
if (!v) return 'prose';
25322604
if (/^manual\b/i.test(v)) return 'manual';
25332605
if (/^closed\b[^\n]*?#\d+\b/i.test(v)) return 'closed-ref';
2534-
if (/#\d+\b/.test(v)) return 'issue-ref';
2606+
if (restartWhenIssueDeclarationRe().test(v)) return 'issue-ref';
25352607
if (/^`[^`\n]+`/.test(v)) return 'command';
25362608
if (RESTART_WHEN_COMMAND_HEADS.includes(v.split(/\s+/)[0].toLowerCase())) return 'command';
25372609
if (restartWhenTrackedPaths(v, isTracked).length > 0) return 'tracked-path';
@@ -2640,9 +2712,15 @@ export function h9OnHoldNoRestartWhen(issue, commentBodies, isTracked = () => fa
26402712
'`closed <owner/repo>#N`. Rewrite the line as `Restart-touch: <path>` (one path per line) ' +
26412713
'and give `Restart-when:` a real exit or `manual`'
26422714
: classes.includes('prose')
2643-
? 'its `Restart-when:` is prose naming no issue, no tracked path and no runnable ' +
2644-
'command — a `manual` in disguise; nothing schedules the actor it waits on. Mark it ' +
2645-
'`manual` or name the event'
2715+
? values.some((v, i) => classes[i] === 'prose' && restartWhenIssueRefRe().test(v))
2716+
? 'its `Restart-when:` is prose that MENTIONS an issue reference mid-sentence but ' +
2717+
'DECLARES none — a mention is not an exit; `issue-ref` is read at the declaration ' +
2718+
'position, so nothing schedules the actor this value waits on. If the mentioned ' +
2719+
'card IS the exit, declare it: `Restart-when: #N rules on X` (the reference ' +
2720+
'first). Otherwise mark it `manual` or name the event'
2721+
: 'its `Restart-when:` is prose naming no issue, no tracked path and no runnable ' +
2722+
'command — a `manual` in disguise; nothing schedules the actor it waits on. Mark it ' +
2723+
'`manual` or name the event'
26462724
: values.length > 0
26472725
? 'its only `Restart-when:` is `manual`, which no mechanism can fire'
26482726
: commentsRead
@@ -20598,6 +20676,77 @@ async function selfTest() {
2059820676
t('gate: the ruling spelling still buys nothing', needsRestartWhenComments(hold('Restart-when: #13651 rules on it'), h9Tracked), false);
2059920677
t('gate: the reserved `closed …#N` class still buys nothing', needsRestartWhenComments(hold(`Restart-when: ${v5499}`), h9Tracked), false);
2060020678

20679+
// -- H9: `issue-ref` is the DECLARATION position (#17605) -------------------
20680+
// The wide form (`/#\d+\b/`) read a MENTION as an exit, so four live holds
20681+
// whose value names a tracked repo path cleared H9 on an unrelated issue
20682+
// number written mid-sentence. Every value below is VERBATIM from the live
20683+
// card on 2026-09-12, pinned as a literal string for the reason the #17377
20684+
// specimens are: a later reader sees what the position test was measured
20685+
// against, not a paraphrase of it. Both directions, on the same four.
20686+
const v8753 =
20687+
'any PR makes provisionTenantScopeIndex in packages/objectql/src/registry.ts read ' +
20688+
'injected-column provenance (#7865 convergence map resumes), or a gate begins pinning ' +
20689+
'/meta/object/:name index accuracy';
20690+
const v8607 =
20691+
'the ADR-0087 changeset-gate family is reworked (scripts/check-adr-0087-registration.mjs ' +
20692+
'and siblings, e.g. out of #8299), or a second breaking-behavior changeset lands unmarked';
20693+
const v8589 =
20694+
'buildMcpBridge in packages/runtime/src/domains/mcp.ts reaches the engine other than ' +
20695+
'through callData, a third McpDataBridge implementation appears, or the #7823 tripwire is ' +
20696+
'narrowed or moved off the protocol ingress path';
20697+
const v6009 =
20698+
'any PR touches sqliteCanonicalDatetimeSql or backfillCanonicalDatetimes in ' +
20699+
'packages/drivers/driver-sql/src/sql-driver.ts (price the backfill-SET-side guard, NOT the ' +
20700+
'cloud#1005-rejected shared-read heuristic), or unresolvedEpochTextRows counts non-zero on ' +
20701+
'real data, or a bare-numeric-TEXT Field.datetime site is reported';
20702+
const v13718 = '#13651 closes (ask 1 lands and reports the count of constants an app cannot reach)';
20703+
const v3267 =
20704+
'objectstack-ai/cloud#861 (Phase 3 EE governance) is scheduled, or a real customer / ' +
20705+
'example scenario needs durable suspension inside a parallel branch (parallel approval, ' +
20706+
'wait signal, or a subflow containing a pause)';
20707+
// The census oracle: the one tracked path each of the four names, and
20708+
// nothing else — the fixture states the tracked set rather than inheriting
20709+
// whatever this checkout happens to hold (`h9Tracked`'s reason).
20710+
const censusTracked = (p) =>
20711+
['packages/objectql/src/registry.ts', 'scripts/check-adr-0087-registration.mjs',
20712+
'packages/runtime/src/domains/mcp.ts',
20713+
'packages/drivers/driver-sql/src/sql-driver.ts'].includes(p);
20714+
20715+
t('H9 class: #8753 — a mid-sentence mention is not an exit', classifyRestartWhen(v8753, censusTracked), 'tracked-path');
20716+
t('H9 class: #8607 — same, the gate family it names is tracked', classifyRestartWhen(v8607, censusTracked), 'tracked-path');
20717+
t('H9 class: #8589 — same, and the mention sits after the path', classifyRestartWhen(v8589, censusTracked), 'tracked-path');
20718+
t('H9 class: #6009 — same, on a cross-repo `cloud#N` mention', classifyRestartWhen(v6009, censusTracked), 'tracked-path');
20719+
// The other direction: with no oracle the same four collapse into `prose` —
20720+
// same verdict, the less specific row — which is what the wide form's
20721+
// false-clean was hiding in BOTH oracle states.
20722+
t('H9 class: #8753 with no oracle is prose, never fireable', classifyRestartWhen(v8753), 'prose');
20723+
t('H9 class: #8607 with no oracle is prose, never fireable', classifyRestartWhen(v8607), 'prose');
20724+
t('H9 class: #8589 with no oracle is prose, never fireable', classifyRestartWhen(v8589), 'prose');
20725+
t('H9 class: #6009 with no oracle is prose, never fireable', classifyRestartWhen(v6009), 'prose');
20726+
// ⛔ And the narrowing does NOT reach a value that DECLARES its reference —
20727+
// the advertised spelling, read literally, in both its forms.
20728+
t('H9 class: #13718 declares its reference first -> still `issue-ref`', classifyRestartWhen(v13718, censusTracked), 'issue-ref');
20729+
t('H9 class: #3267 declares a cross-repo one -> still `issue-ref`', classifyRestartWhen(v3267, censusTracked), 'issue-ref');
20730+
t('H9 class: …and the decoration a declaration may carry is the `Part of` one', classifyRestartWhen('> **#13651** rules on it'), 'issue-ref');
20731+
t('H9 class: a bare single-segment qualifier is a path segment, not a repo', classifyRestartWhen('cloud#861 is scheduled'), 'prose');
20732+
// The four are H9 ROWS now, each prescribing the rewrite its class names.
20733+
t('H9: #8753 is a finding, not a legal hold', typeof h9OnHoldNoRestartWhen(hold(`Restart-when: ${v8753}`), undefined, censusTracked), 'string');
20734+
t('H9: …and the row names the tracked path it misfiled', h9row(hold(`Restart-when: ${v8753}`), undefined, censusTracked).includes('`packages/objectql/src/registry.ts`'), true);
20735+
t('H9: …and prescribes the `Restart-touch:` rewrite', h9row(hold(`Restart-when: ${v8753}`), undefined, censusTracked).includes('Rewrite the line as `Restart-touch: <path>`'), true);
20736+
t('H9: #13718 stays clean — a declaration is an exit', h9OnHoldNoRestartWhen(hold(`Restart-when: ${v13718}`), undefined, censusTracked), null);
20737+
t('H9: #3267 stays clean too', h9OnHoldNoRestartWhen(hold(`Restart-when: ${v3267}`), undefined, censusTracked), null);
20738+
// The prose row's remedy follows the value: a MENTION gets the declaration
20739+
// prescribed, a value with no reference at all keeps the #10102 sentence.
20740+
const mentionProse = 'the design session rules the question below, with #5493 context on the table';
20741+
t('H9: a prose value that MENTIONS a card is prose', classifyRestartWhen(mentionProse, censusTracked), 'prose');
20742+
t('H9: …and its row prescribes the DECLARATION', h9row(hold(`Restart-when: ${mentionProse}`), undefined, censusTracked).includes('declare it: `Restart-when: #N rules on X`'), true);
20743+
t('H9: …saying in as many words that a mention is not an exit', h9row(hold(`Restart-when: ${mentionProse}`), undefined, censusTracked).includes('MENTIONS an issue reference mid-sentence'), true);
20744+
t('H9: …while still naming the event as the other branch', h9row(hold(`Restart-when: ${mentionProse}`), undefined, censusTracked).includes('mark it `manual` or name the event'), true);
20745+
t('H9: a reference-free prose value does NOT gain the clause', h9row(h3739, undefined, h9Tracked).includes('declare it: `Restart-when: #N rules on X`'), false);
20746+
t('H9: …and keeps the #10102 sentence verbatim', h9row(h3739, undefined, h9Tracked).includes('Mark it `manual` or name the event'), true);
20747+
// ⛔ The class ORDER is untouched — B was never the instrument (#17605).
20748+
t('H9 class: the fireable set is STILL exactly the three', FIREABLE_RESTART_WHEN_CLASSES.join('|'), 'closed-ref|issue-ref|command');
20749+
2060120750
// -- H10: stale unclaimed p0 (routing-gap backstop) -------------------------
2060220751
const NOW = Date.parse('2026-08-16T12:00:00Z');
2060320752
const hoursAgo = (h) => new Date(NOW - h * 3_600_000).toISOString();

0 commit comments

Comments
 (0)