Skip to content

Commit cec7d5b

Browse files
committed
fix(pm): widen the card-relation grammar to its hyphen and colon spellings
`partOfRe` required a literal space on both sides of the word, so `Part-of #N`, `Part of: #N` and `Part-of: #N` — the three spellings every dispatch order lists as forbidden, and the last of which is the git-trailer form — were invisible to every reader of this grammar. `refsRe` carried the same colon gap, and a pinned case asserted it as if it were a decision. The separator is now the one `closingKeywordRe` already ships (an optional colon), and the word accepts a hyphen where it accepts a space. Strict superset: nothing that matched before stops matching. Claude-Session: https://claude.ai/code/session_01MCLBsUgfykL74aU716rzVK Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9bd4344 commit cec7d5b

1 file changed

Lines changed: 60 additions & 5 deletions

File tree

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

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1787,8 +1787,37 @@ function closingKeywordRe() {
17871787
return /\b(clos(?:e|es|ed)|fix(?:es|ed)?|resolv(?:e|es|ed))\b[ \t]*:?[ \t]*#(\d+)\b/gi;
17881788
}
17891789

1790+
/**
1791+
* `Part of #N` — and every spelling of it this repository's dispatch orders
1792+
* forbid, which is not the same set as the one a space-only pattern matched.
1793+
*
1794+
* MEASURED (2026-09-11, the spec seat, on one commit message fed to the gate
1795+
* that reads this): `Part of #17184` exited 1 and `Part-of: #17184` exited 0.
1796+
* The hyphen and the colon are the two spellings every `os-dev` dispatch order
1797+
* lists as forbidden, so a dev who greps for `Part-of` before pushing, gets 0,
1798+
* and trusts the gate to be the net underneath is reading a net with no thread
1799+
* in that square. The instruction surface and the enforcement surface were out
1800+
* of step at exactly the step that tells the dev to verify it themselves.
1801+
*
1802+
* The separator is therefore the one `closingKeywordRe` already ships — an
1803+
* optional colon between the word and the reference — and the word itself
1804+
* accepts a hyphen where it accepts a space. `Part-of:` is also the GIT TRAILER
1805+
* spelling (`Token: value`), which is the shape a commit message carries this
1806+
* relation in at all, so it is the spelling most likely to be typed.
1807+
*
1808+
* ⚠️ The widening is a STRICT SUPERSET of what this matched before — the
1809+
* whitespace run stays optional-colon-tolerant rather than being replaced by
1810+
* `[ \t]`, so no body or message that matched yesterday stops matching today.
1811+
* The direction matters: a narrowing here silently empties a `partOf` set and
1812+
* drops a row through to a fallback channel (see `partOfDeclarationRe`'s
1813+
* measured corpus note), which is a finding lost rather than a finding gained.
1814+
*
1815+
* `Part` alone, `Parts of` and `part-of` in prose are deliberately NOT widened
1816+
* to: the protocol has these spellings, and widening the reader further is how
1817+
* a dialect gets a home (`refsRe` makes the same call for `Ref`/`References`).
1818+
*/
17901819
function partOfRe() {
1791-
return /\bPart of\s+#(\d+)\b/gi;
1820+
return /\bPart[ \t-]of\b[ \t]*:?\s*#(\d+)\b/gi;
17921821
}
17931822

17941823
/**
@@ -1831,7 +1860,7 @@ function partOfRe() {
18311860
* position is reported instead.
18321861
*/
18331862
function partOfDeclarationRe() {
1834-
return /^[ \t]*(?:>[ \t]*)*(?:(?:[-*+]|\d{1,9}[.)])[ \t]+)?[*_]{0,3}Part of\s+#(\d+)\b/gim;
1863+
return /^[ \t]*(?:>[ \t]*)*(?:(?:[-*+]|\d{1,9}[.)])[ \t]+)?[*_]{0,3}Part[ \t-]of\b[ \t]*:?\s*#(\d+)\b/gim;
18351864
}
18361865

18371866
/**
@@ -1948,7 +1977,10 @@ export function partOfDeclarationTargets(body, { markdown = true } = {}) {
19481977
* with nothing on the board contradicting anything — H49's row.
19491978
*
19501979
* Same strictness as `partOfRe`, on purpose: the word is bound (`\\b`), the
1951-
* `#` follows on whitespace with no colon, the read is code-stripped for the
1980+
* `#` follows on whitespace or on the optional colon that every other relation
1981+
* in this file accepts — `Refs: #N` is the git-trailer spelling of this exact
1982+
* declaration and was as invisible here as `Part-of:` was there — the read is
1983+
* code-stripped for the
19521984
* BODY surface (a body QUOTING `Refs #N` in backticks declares nothing), and a
19531985
* fresh regex per call (the `lastIndex` note at `closingKeywordRe`). `Ref`,
19541986
* `References` and `See #N` are deliberately NOT this relation: the protocol
@@ -1961,7 +1993,7 @@ export function partOfDeclarationTargets(body, { markdown = true } = {}) {
19611993
* inventing an item.
19621994
*/
19631995
function refsRe() {
1964-
return /\bRefs\s+#(\d+)\b(?:[ \t]*\(([^()\n]{1,40})\))?/gi;
1996+
return /\bRefs\b[ \t]*:?\s*#(\d+)\b(?:[ \t]*\(([^()\n]{1,40})\))?/gi;
19651997
}
19661998

19671999
/** `#N` -> the `(item k)` annotation beside it, or `null` when bare (first occurrence wins). */
@@ -19911,6 +19943,20 @@ async function selfTest() {
1991119943
t('extractor: `Part of` in a fence is invisible on the body surface', partOfTargets('```\nPart of #1\n```').size, 0);
1991219944
t('extractor: …and visible on the commit surface', partOfTargets('```\nPart of #1\n```', { markdown: false }).size, 1);
1991319945
t('extractor: the default is byte-identical to the pre-option reading', closingKeywordTargets('Fixes #1').get('1'), 'Fixes');
19946+
// The `Part of` spellings every dispatch order forbids, each pinned on the
19947+
// COMMIT surface — the one a pre-push refusal reads. The hyphen and the colon
19948+
// were both invisible here while every order listed them, so a dev greppping
19949+
// for them got 0 and trusted a net with no thread in that square (measured by
19950+
// the spec seat on one commit message: the space spelling exited 1, the
19951+
// hyphen-colon spelling exited 0).
19952+
t('extractor: `Part of #N` — the spelling that always matched', partOfTargets('Part of #17184', { markdown: false }).has('17184'), true);
19953+
t('extractor: `Part-of #N` — the hyphen spelling', partOfTargets('Part-of #17184', { markdown: false }).has('17184'), true);
19954+
t('extractor: `Part of: #N` — the colon spelling', partOfTargets('Part of: #17184', { markdown: false }).has('17184'), true);
19955+
t('extractor: `Part-of: #N` — the git-trailer spelling, the measured miss', partOfTargets('Part-of: #17184', { markdown: false }).has('17184'), true);
19956+
t('extractor: the widening is a superset — a line break still reaches the reference', partOfTargets('Part of\n#17184', { markdown: false }).has('17184'), true);
19957+
t('extractor: ⛔ `Parts of #N` is not this relation', partOfTargets('Parts of #17184', { markdown: false }).size, 0);
19958+
t('extractor: ⛔ nor a `part` that only ends the word', partOfTargets('Counterpart of #17184', { markdown: false }).size, 0);
19959+
t('extractor: the declaration subset reads every spelling the relation does', [...partOfDeclarationTargets('Part-of: #17184', { markdown: false })].join(','), '17184');
1991419960

1991519961
// The REMEDY TEXT. The realistic regression is someone copying H7's tail
1991619962
// across, so H7's own sentence is asserted to CARRY the clause this one must
@@ -25162,7 +25208,16 @@ Mutual exclusion: \`get_comments\` page 747 → \`[]\`, page 746 = my own R+117
2516225208
t('refs extractor: case-insensitive, exactly as `partOfRe`', refsTargets('refs #16003').has('16003'), true);
2516325209
t('refs extractor: ⛔ `Ref #N` is not the protocol\'s spelling', refsTargets('Ref #16003').size, 0);
2516425210
t('refs extractor: ⛔ nor `References #N`', refsTargets('References #16003').size, 0);
25165-
t('refs extractor: ⛔ nor a colon, exactly as `Part of` takes none', refsTargets('Refs: #16003').size, 0);
25211+
// ⚠️ This pin USED to assert the opposite — that a colon was not this
25212+
// relation's spelling — and it was pinning the gap rather than a decision:
25213+
// `Refs: #N` is the git-trailer spelling of the same declaration, and the
25214+
// keyword extractor beside it has accepted `Fixes: #N` since the colon was
25215+
// measured against GitHub's own parser. Replaced rather than reworded: the
25216+
// old case passed for as long as the branch it named existed, which is the
25217+
// shape a fixture takes when what it really pins is a missing thread.
25218+
t('refs extractor: a colon IS the spelling, as at the keyword extractor', refsTargets('Refs: #16003').has('16003'), true);
25219+
t('refs extractor: …and the item annotation still reads through the colon', refsTargets('Refs: #16003 (item 2)').get('16003'), 'item 2');
25220+
t('refs extractor: the word is still bound — `Prefs: #N` is not a reference', refsTargets('Prefs: #16003').size, 0);
2516625221
t('refs extractor: the word is bound — `Prefs #N` is not a reference', refsTargets('Prefs #16003').size, 0);
2516725222
t('refs extractor: several cards in one body are each read', [...refsTargets('Refs #1 (item ①) and Refs #2').keys()].join(','), '1,2');
2516825223
t('refs extractor: a missing body is empty, never a crash', refsTargets(undefined).size, 0);

0 commit comments

Comments
 (0)