You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
tooling(pm): split the clause-② declaration limb's NO READING into two readings (#15735)
The declaration limb returned one `absent` state for two different facts: a
card whose thread carries no claim comment at all, and a card whose claim
comment carries no `Clause-②:` line. One sentence, one count, and two
different things owed -- a report could say "15 NO READING" about two
populations and a seat reading its own row could not tell which remedy was
being asked for.
`cardDeclaration` now returns `absent` (no comment on the thread is a claim
comment -- no line beginning `Claim:`) and `missing` (a claim comment is there
and carries no declaration line) as two members of the same enum, under the
same exit code the limb has always returned. `c2DeclarationUnreadable` writes
a sentence per state: the ABSENT one names the fixed first-line spelling as
its remedy and says in one clause that a heading-style claim is not a claim
comment to this predicate; the MISSING one sends the remedy to the claim
comment already on the thread. The sweep's per-repo summary counts them as
two numbers under two labels, from the same reader the rows use, and `--json`
carries the same pair.
`CLAIM_COMMENT_MARKER` is unchanged -- it is imported from
`check-half-states.mjs` so the two readers cannot drift, and the spelling it
accepts is a maintainer ruling. No exit code changes.
Self-test: 174 -> 190 cases. New cases cover a prose-only thread (ABSENT), a
`## Claim --` heading thread (ABSENT, with the remedy sentence), a `Claim:`
comment with no line (MISSING), a `Claim:` comment carrying the key only
inside prose (MISSING, never readable -- the reader is line-anchored), and
the summary tally over all of them.
Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
Co-authored-by: Claude <noreply@anthropic.com>
@@ -1672,13 +1747,23 @@ export function selfTest() {
1672
1747
t('a card that never mentions the clause reads null',readClause2Line('Claim: whatever\nBranch: x')===null);
1673
1748
t('⛔ the reader never invents a value from an adjacent word',readClause2Line('this card is clause 2 yes in substance')?.kind!=='declared');
1674
1749
1675
-
// -- the card-level declaration: four states, none collapsed into another ---
1676
-
battery('the card-level declaration: four states, none collapsed into another');
1750
+
// -- the card-level declaration: every state, none collapsed into another --
1751
+
battery('the card-level declaration: every state, none collapsed into another');
1677
1752
t('a claim comment carrying the line reads DECLARED',cardDeclaration([CLAIM('Clause-②: no')]).state==='declared');
1678
1753
t('…and keeps the value',cardDeclaration([CLAIM('Clause-②: yes')]).value==='yes');
1679
-
t('a thread with no claim comment at all reads ABSENT',cardDeclaration([{body: 'just a comment',created_at: '2026-08-31T10:00:00Z'}]).state==='absent');
1680
-
t('the #13910 shape — a claim comment with no Clause-② line — reads ABSENT',cardDeclaration([CLAIM('Domain: `domain:engine`')]).state==='absent');
1681
-
t('⛔ ABSENT is not `no`',cardDeclaration([CLAIM('Domain: x')]).state!=='declared');
1754
+
t('a thread with no claim comment at all reads ABSENT — no carrier, so no line could be read',cardDeclaration([{body: 'a triage note, and nothing that begins a line with the claim key',created_at: '2026-08-31T10:00:00Z'}]).state==='absent');
1755
+
// The claim predicate is a LINE BEGINNING `Claim:`, and that one spelling is
1756
+
// the whole set. A heading-style claim carries no such line, so the thread
1757
+
// has no claim carrier at all and what it owes is the comment, not the line.
1758
+
t('a heading-style claim is NOT a claim comment — the thread reads ABSENT, not missing-a-line',cardDeclaration([{body: '## Claim — PM loop round R1\nBranch: `claude/issue-13476-unresolvable-engine-403`\nDomain: `domain:engine`',created_at: '2026-08-31T10:00:00Z'}]).state==='absent');
1759
+
t('the #13910 shape — a claim comment with no Clause-② line — reads MISSING: the carrier is there, the line is not',cardDeclaration([CLAIM('Domain: `domain:engine`')]).state==='missing');
1760
+
t('⛔ ABSENT and MISSING are two readings, never one — one owes a comment, the other a line',cardDeclaration([{body: 'a triage note, and nothing that begins a line with the claim key',created_at: '2026-08-31T10:00:00Z'}]).state!==cardDeclaration([CLAIM('Domain: x')]).state);
1761
+
// The substring trap: a claim comment that DESCRIBES the declaration carries
1762
+
// the key as a fragment inside a sentence, never as a line of its own. The
1763
+
// reader is line-anchored, so a description is MISSING and never readable.
1764
+
t('a claim comment that only DESCRIBES the line reads MISSING, never declared',cardDeclaration([CLAIM('the dev declares `Clause-②: yes|no` from the diff')]).state==='missing');
1765
+
t('…and it carries no value — a fragment inside prose is not a reading of one',cardDeclaration([CLAIM('the dev declares `Clause-②: yes|no` from the diff')]).value===undefined);
1766
+
t('⛔ neither not-read state is `no`',cardDeclaration([CLAIM('Domain: x')]).state!=='declared'&&cardDeclaration([{body: 'a triage note, and nothing that begins a line with the claim key',created_at: '2026-08-31T10:00:00Z'}]).state!=='declared');
1682
1767
t('the line in a NON-claim comment reads MISPLACED, not absent and not declared',cardDeclaration([CLAIM('Domain: x'),{body: 'Clause-②: yes',created_at: '2026-08-31T11:00:00Z'}]).state==='misplaced');
1683
1768
t('a malformed line in the claim comment reads MALFORMED',cardDeclaration([CLAIM('Clause-②: Yes')]).state==='malformed');
1684
1769
t('an UNREADABLE thread reads unreadable — never absent (#4690)',cardDeclaration(null).state==='unreadable');
@@ -1717,13 +1802,35 @@ export function selfTest() {
1717
1802
1718
1803
// -- C2, the row this file exists for --------------------------------------
t('a card with no Clause-② line in its claim comment produces a C2 row',typeofmissingLine==='string');
1807
+
t('…and says NO READING in as many words',says(missingLine,'NO READING'));
1808
+
t('…and names the DECLARATION LINE as the thing that is missing',says(missingLine,'DECLARATION LINE is what is missing'));
1809
+
t('…and sends the remedy to the claim comment that is already there',says(missingLine,'add the line to that claim comment'));
1810
+
constnoClaim=c2DeclarationUnreadable(pair({cardComments: [{body: 'a triage note, and nothing that begins a line with the claim key',created_at: '2026-08-31T10:00:00Z'}]}));
1811
+
t('a thread with no claim comment produces a C2 row of its own',typeofnoClaim==='string');
1812
+
t('…and names the CLAIM COMMENT as the thing that is missing, not the line',says(noClaim,'CLAIM COMMENT is what is missing'));
1813
+
t('…and its remedy names the fixed first-line spelling',says(noClaim,'first line beginning `Claim:`'));
1814
+
t('…and names the heading-style claim as the shape that does not count',says(noClaim,'## Claim —'));
1815
+
t('the heading-style thread gets that same row — the shape the predicate never matched',c2DeclarationUnreadable(pair({cardComments: [{body: '## Claim — PM loop round R1\nBranch: `claude/issue-13476-unresolvable-engine-403`\nDomain: `domain:engine`',created_at: '2026-08-31T10:00:00Z'}]}))===noClaim);
1816
+
t('⛔ the two not-read rows are DIFFERENT sentences — one number over both describes neither',missingLine!==noClaim);
1817
+
t('…and both still say it is not a declared `no`',says(missingLine,'NOT a declared')&&says(noClaim,'NOT a declared'));
1818
+
t('…and both refuse to have the line filled in on the seat\'s behalf',says(missingLine,'Do not fill the line in')&&says(noClaim,'Do not fill the line in'));
1819
+
t('…and both forbid relaxing the spelling to prose',says(missingLine,'do not relax the')&&says(noClaim,'do not relax the'));
1820
+
t('…and both quote the fixed spelling so the remedy is executable',says(missingLine,'Clause-②: yes')&&says(noClaim,'Clause-②: yes'));
1821
+
// The summary line is what a round report quotes, so the two readings are
1822
+
// counted apart there too — from the same reader the rows use.
1823
+
constTALLY=[
1824
+
pair({cardComments: [CLAIM('Clause-②: no')]}),
1825
+
pair({cardComments: [CLAIM('Domain: x')]}),
1826
+
pair({cardComments: [CLAIM('Domain: y')]}),
1827
+
pair({cardComments: [{body: 'a triage note, and nothing that begins a line with the claim key',created_at: '2026-08-31T10:00:00Z'}]}),
1828
+
pair({cardComments: null}),
1829
+
];
1830
+
t('the sweep counts the two not-read states separately',declarationLimbTally(TALLY).missing===2&&declarationLimbTally(TALLY).absent===1,JSON.stringify(declarationLimbTally(TALLY)));
1831
+
t('…and counts a DECLARED card into neither',declarationLimbTally([pair({cardComments: [CLAIM('Clause-②: yes')]})]).absent===0&&declarationLimbTally([pair({cardComments: [CLAIM('Clause-②: yes')]})]).missing===0);
1832
+
t('…and an UNREADABLE thread into neither — it is UNJUDGED, never a not-read declaration',declarationLimbTally([pair({cardComments: null})]).absent===0&&declarationLimbTally([pair({cardComments: null})]).missing===0);
1833
+
t('…and a MISPLACED declaration into neither — it has its own row and its own remedy',declarationLimbTally([pair({cardComments: [CLAIM('Domain: x'),{body: 'Clause-②: yes',created_at: '2026-08-31T11:00:00Z'}]})]).missing===0);
0 commit comments