Skip to content

Commit b71d9e7

Browse files
os-steveclaude
andauthored
fix(pm): refuse a claim's unreadable keyed lines before post-stamped writes the comment (#19435)
Fixes #19152 Clause-②: no A claim's `Seat:`, `Thread-read:` and `Clause-②:` lines are exact-value fields. Three post-hoc readers enforce them AFTER the claim is stored; one seat produced five half-state rows off three of them in a single shift, always the same shape — the key right, the value right, an explanation added on the same line. `post-stamped.mjs` is the one writer every claim passes through, and it said nothing. It now reads those three lines on the `--comment` path, **through the very functions that own them**, and REFUSES the write before any request when one of them cannot be read — naming the key, quoting the offending line and printing the spelling that reads. `check-half-states.mjs` prescribed exactly this instrument, in its own `Seat:` reader's docblock: > ⛔ The presence probe below therefore does NOT widen `CLAIM_SEAT_KEY_LINE`: accepting a malformed declaration would leave the writer no way to find out either, which is the same defect wearing the other costume. **Refusing it at the writing path is a different instrument's job.** ## The three owning readers, quoted beside mine | key | the owner's own reader | what this tool calls a refusal | |---|---|---| | `Seat:` | `claimSeatNumber` (`check-half-states.mjs`): key line `/^[ \t]*(?:[-*+][ \t]+)?>?[ \t]*(?:\*\*)?`?Seat`?(?:\*\*)?[ \t]*:[ \t]*(.*)$/im`, then `#(\d+)`; absent ⇒ 1, present-but-unreadable ⇒ `null` (PR #19423) | `claimSeatNumber(body) === null` — the imported call, nothing else | | `Thread-read:` | `threadReadField` returns `{present, value}`; `h50ThreadReadMismatch` then asks `field.present && field.value === found.expected`, where `expected` is `commentIdText` (`/^[1-9]\d*$/`) or the literal `none` | the field reader for the line; a value outside `/^(?:[1-9]\d*|none)$/` can never equal that `expected`, plus a presence probe for a declaration off the line start | | `Clause-②:` | `readClause2Line` (`check-clause2-carriers.mjs`) — four-valued: `declared` / `malformed` / `near-miss` (`describing`, `inline-key`, `spelling`) / `null` | anything that is neither `null` nor `declared`; `--pair` answers exit 4 on those | No second parser: the three readers are imported and called. The one spelling this file adds is the `Thread-read:` value shape, pinned in the self-test against `check-half-states.mjs`'s own source text (its `commentIdText` pattern and its `'none'` literal are not exported), which is the coupling the claim prescribed. ## Before / after — measured, `--dry-run` on each body | body | before (488f4f5) | after | |---|---|---| | `Seat:` declared inside the opening sentence | exit 0, stored | **exit 2, nothing written** | | `Thread-read:` listing nine ids | exit 0, stored | **exit 2, nothing written** | | `` `Clause-②: yes` `` followed by prose (the `describing` near miss) | exit 0, stored | **exit 2, nothing written** | | `Clause-②: yes — the ruling states it outright (…)` | exit 0 | exit 0 — deliberately unchanged, see the correction below | | a well-formed `Claim:` | exit 0 | exit 0 | | an `os-dev-report` comment | exit 0 | exit 0 | | a `## Contract review` body quoting a claim's `Seat:` line | exit 0 | exit 0 | The refusal, on the first of those: ```text post-stamped: REFUSED — 1 keyed line(s) in this `Claim:` cannot be read by the checker that owns them. Nothing was written. 1. [Seat] `claimSeatNumber` reads `null` here — the declaration is present and names no seat. The claim lands on NO seat, and H38 rows another seat's post over it. line: Claim: PM loop round 1 — dispatched by the skills seat. Seat: domain:skills#2 — R1, 2026-09-20T20:31Z. write: `Seat: domain:LANE#N` at the START of a line, lane and number both — e.g. `Seat: domain:skills#2` ``` ## Two dispatch assumptions REFUTED, and corrected here **① `Clause-②` is NOT "the value and nothing after it".** The dispatch order described the clause reader as accepting `yes`/`no` "and nothing after it". Measured on `readClause2Line`: trailing reasoning on a bare-key line is ACCEPTED, deliberately, and its own docblock records why — #13914's control case is «the PM claim comment on #12297 carries `Clause-②: yes` **with reasoning**» and is recorded there as the shape that is CORRECT, after a stricter reading rejected four real claims on the live board. What that reader refuses is structural: `describing` (the key twice on the line, or a code span opened before the key, closed later, and the line then continuing), `inline-key` (the key off the line start) and `malformed` (a value it cannot grade). So the measured shape in the card's table is refused here through its quoted-and-continued spelling, and a bare `Clause-②: yes — reasoning` is left alone. Refusing it would have made this tool reject the shape the owning gate prescribes — the second dialect this card exists to avoid. **② The scope is NOT "first line opens with `Claim:`".** The claim and the card both spelled it that way; the fleet's own definition is `CLAIM_COMMENT_MARKER` through `markerMatches`, which matches a `Claim:` line anywhere in the body, and `newestLaneClaim`'s header refuses a stricter reading by name: «Defining a stricter first-line-only claim for this row alone would leave the file disagreeing with itself about what a claim IS — H2 calling a card claimed while H38 called the same comment invisible». A first-line rule here would decline to judge bodies H50 reads as claims, which is the hole restated one layer up. The marker is imported. Assumptions ①(the tool reads no keyed line today — zero mentions of the three keys before this diff), ③(reuse `EXIT_REFUSED`; a refusal before any request) and ④(extend the existing `--self-test`, no second entry point) held as stated. ## REFUSE, not warn — on the four axes - **实际业务需求** — measured, not supposed: five rows in one shift, three keys, three detectors, every one found minutes to hours later; and one of them (#19108) accused ANOTHER seat's post. A warning on a tool whose stdout is read by scripts is a row nobody clears — this file's own header records the sibling case where a refusal was piped away and the write that followed landed anyway. - **项目长远合理性** — `SKILL.md` ranks remedies «(a) 删容许出错的构造 → (b) 让正确形态成唯一拼写 → (c) 加检查», and the card's own reading is that every existing reader is a post-hoc (c). Refusing at the single writer makes the readable spelling the only storable one: a (b) fix at the one chokepoint, with no new checker to keep in step. - **防 AI 写代码犯错** — the refusal is the contract-first direction: loud at authoring time, with the exact spelling printed, rather than a lenient consumer. A warning would be precisely the tolerant seam this repo forbids — the malformed line would still be stored, and the reader that cannot parse it would still file its row. - **创业阶段不扩散** — three keys, the ones the card measured, and no other; no `--force`, no flag, no config. The template's other keyed lines (`Session:`, `Branch:`, `Worktree:`, `Domain:`, `File surface:`, `Container & model:`, `Serial constraints cleared:`) are untouched: they have no exact-value reader to disagree with, so there is nothing to mirror. ## The two decisions the card asked to be pinned **A key inside a code fence or a code span is judged exactly as in prose.** The tool already has a quoted-span mask (`quotedSpans`) and it is deliberately NOT reused here. All three owning readers read raw text, so masking would let a body store the exact line the patrol then files — a refusal bought off with backticks. This is the asymmetry this file already states for stamps: quoting changes what is RENDERED, never what was AUTHORED. Two self-test cases pin both directions: backticks around a mid-sentence declaration buy no exemption, while a line-initial declaration inside a fence still reads as the declaration its owner reads there. **A key present twice reads the way its owner reads it — the first line-initial declaration decides, and a later one is neither read nor refused.** That is not a choice this file makes; it is what `CLAIM_SEAT_KEY_LINE.exec`, `THREAD_READ_KEY_LINE.exec` and `readClause2Line`'s first-declaration-wins loop already do, and a write side that judged the second line would refuse a body its readers call clean. Pinned by a case: a readable declaration followed by a malformed duplicate passes. ## Scope, budget and verification - `scripts/pm/post-stamped.mjs` only, `--comment` only (a claim IS a comment; a seat POST's body carries a `Seat:` line no claim reader judges). `check-half-states.mjs` and `check-clause2-carriers.mjs` are untouched — read, imported, not copied. - Net **+120 / -0** lines, the claim's budget exactly, self-test cases included. - `node scripts/pm/post-stamped.mjs --self-test` :: exit 0 — 530 cases across 18 batteries, the new battery pinned at a floor of 20. - The derived union on the merged head (`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, reconciled with `--ran`): 30 commands, every exit captured before any pipe. All green. - `pnpm exec eslint --no-inline-config scripts/pm/post-stamped.mjs` :: exit 0. The repo-wide scan is CI's run. - `skip-changeset`: `scripts/pm/**` publishes nothing from any released package. - `node scripts/pm/check-governed-merges.mjs --pr N` reads NOT governed — `scripts/pm/` is on no register row. ## Acceptance notes - Noted, not filed: the `Thread-read:` write-time rule can only decide SHAPE — whether the id is the right one is H50's, and it needs the thread this act has not fetched. A future `--thread-read` proof-of-reading flag (the shape `--ack-through` already has on `--body`) would close that half; nobody is blocked on it today. 承接者: the next card on this file's `--comment` path. - Noted, not filed: `commentIdText` and the `'none'` literal in `check-half-states.mjs` are not exported, so this file pins their spelling by reading that file's source. Exporting them would turn the coupling into an import. 承接者: whoever next edits H50's reader. --- _Generated by [Claude Code](https://claude.ai/code/session_017ETYWqMQD4qMtZzAGovWNi)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent b929e0a commit b71d9e7

1 file changed

Lines changed: 120 additions & 0 deletions

File tree

‎scripts/pm/post-stamped.mjs‎

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,13 @@
361361
* The line now says which verdict it is standing on and what was quoted, so it
362362
* is true of every body that reaches it.
363363
*
364+
* ## A claim's keyed lines are refused BEFORE the write (#19152)
365+
*
366+
* A claim's three exact-value fields — `Seat:`, `Thread-read:`, `Clause-②:` — are read HERE through the functions that
367+
* own them, so a line those functions cannot read is `EXIT_REFUSED` before any request instead of a half-state row on
368+
* someone else's board hours later (five such rows off three keys in one seat's shift, the measurement behind this
369+
* rule). `claimKeyedLineRefusals` carries the four decisions that keep it a mirror and not a fourth dialect.
370+
*
364371
* ## ⚖️ Why this ACTS by default, where `sweep-closed-cards.mjs` dry-runs
365372
*
366373
* Its sibling next door defaults to a dry run and needs `--write`, because it
@@ -740,18 +747,23 @@ import { readFileSync } from 'node:fs';
740747
import { fileURLToPath } from 'node:url';
741748
import { isEntrypoint } from '../invoked-as.mjs';
742749
import {
750+
CLAIM_COMMENT_MARKER,
743751
COMMENT_BODY_LIMIT,
744752
EXIT_PREREQUISITE_NOT_MET,
745753
H56_STAMP_TOLERANCE_MIN,
746754
ISSUE_BODY_LIMIT,
747755
PROXY_FLAG,
756+
claimSeatNumber,
748757
h56StampedReadings,
758+
markerMatches,
749759
protocolStamps,
750760
proxyRearmPlan,
751761
resolveSweepRepo,
752762
stampDriftMinutes,
753763
stampSpan,
764+
threadReadField,
754765
} from './check-half-states.mjs';
766+
import { readClause2Line } from './check-clause2-carriers.mjs';
755767

756768
const SELF_PATH = fileURLToPath(import.meta.url);
757769
const API = 'https://api.github.com';
@@ -1601,6 +1613,76 @@ export function substitutionSummary({ substituted = 0, quoted = 0, verbatim = 0
16011613
);
16021614
}
16031615

1616+
// ---------------------------------------------------------------------------
1617+
// The claim's keyed lines — refused before the write (#19152)
1618+
// ---------------------------------------------------------------------------
1619+
1620+
/** What each owning reader accepts, printed BY the refusal so the fix is one line away. */
1621+
export const CLAIM_KEY_SPELLINGS = Object.freeze({
1622+
Seat: '`Seat: domain:LANE#N` at the START of a line, lane and number both — e.g. `Seat: domain:skills#2`',
1623+
'Thread-read': '`Thread-read: ID` at the START of a line — ONE comment id, or `none`, and nothing after it',
1624+
'Clause-②': '`Clause-②: yes` or `Clause-②: no` at the START of a line — reasoning after the value is fine, a quotation around it is not',
1625+
});
1626+
1627+
/** PRESENCE only, `CLAIM_SEAT_DECLARATION_ANYWHERE`'s calibration one key along: case-SENSITIVE and demanding the whole
1628+
* payload, because off the line start no position tells a declaration from prose. Consulted solely when `threadReadField` saw none. */
1629+
const THREAD_READ_DECLARATION_ANYWHERE = /(?:\*\*)?`?Thread-read`?(?:\*\*)?[ \t]*:[ \t]*`?(?:[1-9]\d*|none)\b/;
1630+
1631+
/** The ONLY values H50's equality can ever accept: `commentIdText`'s id pattern, and the `none` it answers for a claim that
1632+
* opened the thread. Neither is exported from `check-half-states.mjs`, so the self-test pins this spelling against its source. */
1633+
const THREAD_READ_VALUE = /^(?:[1-9]\d*|none)$/;
1634+
1635+
/** The first line naming a key — a LOCATOR for the writer's eye, ⛔ never the judgement. */
1636+
function keyLine(text, key) {
1637+
const hit = text.split(/\r?\n/).find((line) => line.includes(key));
1638+
return hit === undefined ? '(no single line carries the key)' : offendingSpan(hit.trim(), 160);
1639+
}
1640+
1641+
/**
1642+
* The keyed-line problems in a body about to be written, judged by the readers that OWN each key — ⛔ never by a rule
1643+
* spelled here. Four decisions:
1644+
* IMPORTED `claimSeatNumber` answers `null` exactly for a `Seat:` line it cannot read; `readClause2Line` answers
1645+
* `declared` for the shape its own control case calls correct, `Clause-②: yes — reasoning` included, so
1646+
* ⛔ nothing is tightened here. `Thread-read:` alone exports a FIELD and not a verdict, H50's other half
1647+
* being the thread this act has not fetched; what IS decidable is that a value which is neither an id
1648+
* nor `none` can never equal the id H50 compares it against.
1649+
* SCOPED the fleet's own claim marker, ⛔ not a first-line rule — `newestLaneClaim`'s header refuses that
1650+
* narrowing by name — and `--comment` alone, since a claim IS a comment and a seat POST's body carries
1651+
* a `Seat:` line no claim reader judges.
1652+
* QUOTE-BLIND all three owners read raw text, so masking a quotation here would store exactly the row the patrol
1653+
* then files: quoting changes what is RENDERED, never what was AUTHORED.
1654+
* FIRST MATCH each reader stops at its first readable declaration — that is what a duplicate key gets.
1655+
* @param {string} body — the bytes this act is about to send.
1656+
* @returns {{ key: string, why: string, line: string }[]} — empty when the body is not a claim, or when every key reads.
1657+
*/
1658+
export function claimKeyedLineRefusals(body) {
1659+
const text = String(body ?? '');
1660+
if (!markerMatches(CLAIM_COMMENT_MARKER, text)) return [];
1661+
const rows = [];
1662+
if (claimSeatNumber(text) === null)
1663+
rows.push({ key: 'Seat', line: keyLine(text, 'Seat'), why: '`claimSeatNumber` reads `null` here — the declaration is present and names no seat. The claim lands on NO seat, and H38 rows another seat\'s post over it.' });
1664+
const thread = threadReadField(text);
1665+
if (!thread.present && THREAD_READ_DECLARATION_ANYWHERE.test(text))
1666+
rows.push({ key: 'Thread-read', line: keyLine(text, 'Thread-read'), why: '`threadReadField` sees NO line: the key is off the line start, so H50 reads this claim as carrying no `Thread-read:` at all.' });
1667+
else if (thread.present && !THREAD_READ_VALUE.test(thread.value))
1668+
rows.push({ key: 'Thread-read', line: keyLine(text, 'Thread-read'), why: `the value reads \`${offendingSpan(thread.value)}\`, which is neither one comment id nor \`none\` — H50 compares it for EQUALITY against one id, so no thread makes this match.` });
1669+
const clause = readClause2Line(text);
1670+
if (clause !== null && clause.kind !== 'declared')
1671+
rows.push({ key: 'Clause-②', line: clause.line, why: clause.kind === 'malformed' ? 'the value slot holds something `readClause2Line` cannot grade — the two spellings are the closed set.' : `\`readClause2Line\` reads this as a NEAR MISS (${clause.reason}), ⛔ not a declaration — \`check-clause2-carriers.mjs --pair\` answers exit 4 on it.` });
1672+
return rows;
1673+
}
1674+
1675+
/** The refusal a caller reads, from `claimKeyedLineRefusals`' rows. */
1676+
export function keyedLineRefusalText(rows) {
1677+
return (
1678+
`post-stamped: REFUSED — ${rows.length} keyed line(s) in this \`Claim:\` cannot be read by the checker that owns them. Nothing was written.\n` +
1679+
rows.map((r, i) => ` ${i + 1}. [${r.key}] ${r.why}\n line: ${r.line}\n write: ${CLAIM_KEY_SPELLINGS[r.key]}`).join('\n') +
1680+
'\n\n `claimSeatNumber` and `h50ThreadReadMismatch` (`check-half-states.mjs`) and `readClause2Line` (`check-clause2-carriers.mjs`)\n' +
1681+
' are imported HERE, so this IS the row they would file — hours earlier, and on your own claim rather than on someone\n' +
1682+
' else\'s post. ⛔ No flag turns it off: a line those readers cannot read is a half-state, not a formatting preference.'
1683+
);
1684+
}
1685+
16041686
/**
16051687
* The block the platform appends to an artefact whose sent body does not
16061688
* already carry one: a blank line, a rule, the bare attribution line — 58
@@ -2587,6 +2669,8 @@ const USAGE = [
25872669
' no token at all is unchanged.',
25882670
' A body refresh is REFUSED while comments newer than the body\'s last write stamp exist and',
25892671
' --ack-through=ID does not name the newest of them — a refresh must not void an unread knock.',
2672+
' A `Claim:` comment\'s `Seat:`, `Thread-read:` and `Clause-②:` lines are read here by the checkers that OWN them,',
2673+
' and the comment is REFUSED when one cannot be read — the refusal prints the spelling that can.',
25902674
' The attribution footer is the caller\'s: its form differs by channel and act, so this tool adds none.',
25912675
'',
25922676
` Exit: 0 written and stored · ${EXIT_USAGE} usage · ${EXIT_REFUSED} refused, nothing written ·`,
@@ -2632,6 +2716,13 @@ async function main(argv) {
26322716
return EXIT_REFUSED;
26332717
}
26342718

2719+
// ⛔ Comments only: a claim IS a comment, and a seat POST's body carries a `Seat:` line of its own that no claim reader judges.
2720+
const keyed = options.mode === 'comment' ? claimKeyedLineRefusals(rendered.body) : [];
2721+
if (keyed.length > 0) {
2722+
console.error(keyedLineRefusalText(keyed));
2723+
return EXIT_REFUSED;
2724+
}
2725+
26352726
if (options.dryRun) {
26362727
console.error(
26372728
`post-stamped: DRY RUN — nothing was written. Substituted with \`${rendered.stamp}\` — ` +
@@ -2779,6 +2870,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
27792870
'the unread-knock check: a refresh cannot void what nobody read': 49,
27802871
'the size refusal: a 422 the platform answered is not a route that never existed': 53,
27812872
'the shared rule: this tool and H56 cannot come to disagree': 6,
2873+
'the keyed lines: a claim\'s exact-value fields, judged by the readers that own them': 20,
27822874
});
27832875
const SELF_TEST_BATTERY_FLOOR = 15;
27842876
const UNATTRIBUTED_BATTERY = '(unattributed)';
@@ -3696,6 +3788,34 @@ export function selfTest() {
36963788
t('…and an estimate is beyond the tolerance by the same arithmetic', stampDriftMinutes('2026-09-10T05:27Z', '2026-09-10T06:37:48Z') > H56_STAMP_TOLERANCE_MIN);
36973789
t('the exit register keeps a contract refusal apart from a transport failure', EXIT_REFUSED !== EXIT_PREREQUISITE_NOT_MET);
36983790

3791+
battery('the keyed lines: a claim\'s exact-value fields, judged by the readers that own them');
3792+
{
3793+
const CLAIM = (...lines) => ['Claim: PM loop round 1', 'Session: `session_x`', ...lines].join('\n');
3794+
const keys = (body) => claimKeyedLineRefusals(body).map((r) => r.key).join();
3795+
const MISLAID_SEAT = 'Claim: PM loop round 1 — dispatched. Seat: domain:skills#2 — R1.';
3796+
let ownerSource = ''; try { ownerSource = readFileSync(new URL('./check-half-states.mjs', import.meta.url), 'utf8'); } catch { ownerSource = ''; }
3797+
t('⭐ a well-formed claim passes — every key reads', keys(CLAIM('Seat: `domain:skills#2`', 'Thread-read: 5747819898', 'Clause-②: no')) === '');
3798+
t('⭐ the measured `Seat:` shape — declared inside the opening sentence — is REFUSED, on the OWNER\'s verdict and ⛔ no rule spelled here', keys(MISLAID_SEAT) === 'Seat' && claimSeatNumber(MISLAID_SEAT) === null);
3799+
t('…and the refusal prints the spelling that reads', keyedLineRefusalText(claimKeyedLineRefusals(MISLAID_SEAT)).includes(CLAIM_KEY_SPELLINGS.Seat));
3800+
t('a line-initial `Seat:` naming no number is refused too — the reader\'s other `null`', keys(CLAIM('Seat: domain:skills')) === 'Seat');
3801+
t('⭐ the measured `Thread-read:` shape — a list of ids — is refused, named as a value H50\'s EQUALITY can never match', keys(CLAIM('Thread-read: 5747819898, 5752364802, 5747819899')) === 'Thread-read' && keyedLineRefusalText(claimKeyedLineRefusals(CLAIM('Thread-read: 1, 2'))).includes('EQUALITY'));
3802+
t('`none` reads, and so does a backticked id the field reader unwraps', keys(CLAIM('Thread-read: none')) === '' && keys(CLAIM('Thread-read: `5747819898`')) === '');
3803+
t('an id with the reason glued on, an EMPTY value and the UNFILLED placeholder are all refused', ['Thread-read: 5747819898 — the lane grading', 'Thread-read:', 'Thread-read: id of the newest comment, or none'].every((line) => keys(CLAIM(line)) === 'Thread-read'));
3804+
t('a `Thread-read:` declared off the line start is refused, though the field reader calls it absent', keys(`Claim: x. Thread-read: 5747819898 was the tail.`) === 'Thread-read' && threadReadField('Claim: x. Thread-read: 5747819898 was the tail.').present === false);
3805+
t('⛔ prose naming the key with no id behind it is not a declaration', keys('Claim: x. The Thread-read: line goes last.') === '');
3806+
t('⭐ `Clause-②: yes — reasoning` is ACCEPTED — its reader\'s own control shape, ⛔ not tightened here', keys(CLAIM('Clause-②: yes — the ruling states it outright')) === '');
3807+
t('⭐ …while the quoted-and-continued spelling that reader calls `describing` IS refused', keys(CLAIM('`Clause-②: yes` — the ruling states it outright')) === 'Clause-②');
3808+
t('a value the clause reader cannot grade is refused', keys(CLAIM('Clause-②: YES')) === 'Clause-②' && keys(CLAIM('Clause-②: yes|no')) === 'Clause-②');
3809+
t('⛔ ABSENCE is nobody\'s row here: no `Seat:` line is seat 1 by the owner\'s own default, and no clause line is no declaration to grade', keys(CLAIM('Thread-read: none')) === '' && keys(CLAIM('Seat: `domain:skills#2`')) === '');
3810+
t('⛔ NOT a claim: a report carrying the same mislaid declaration is untouched', keys('os-dev-report\n\nThe claim declared Seat: domain:skills#2 mid-sentence.') === '');
3811+
t('⭐ SCOPED by the fleet\'s marker, ⛔ not by the first line: a `Claim:` further down is judged', keys('Round report\n\nClaim: x. Seat: domain:skills#2 taken.') === 'Seat');
3812+
t('⭐ QUOTE-BLIND: backticks round a mid-sentence declaration buy no exemption, while a line-initial one inside a FENCE still reads as the declaration the owner reads there — masking either way would store the row', keys('Claim: x, `Seat: domain:skills#2`, R1.') === 'Seat' && keys(CLAIM('```', 'Seat: domain:skills#2', '```')) === '');
3813+
t('FIRST MATCH: a readable declaration followed by a malformed duplicate passes, exactly as the owner reads it', keys(CLAIM('Seat: `domain:skills#2`', 'Thread-read: 5747819898', 'Seat: domain:skills')) === '');
3814+
t('↔ owner coupling: the id pattern and the `none` are `check-half-states.mjs`\'s own spellings, read off its source', ownerSource.includes('/^[1-9]\\d*$/') && ownerSource.includes("'none'") && THREAD_READ_VALUE.source.includes('[1-9]\\d*') && THREAD_READ_VALUE.test('none'));
3815+
t('structural: the CLI runs this on `--comment` only, ⛔ never on a card body, and every reader is imported, ⛔ none restated', /const keyed = options\.mode === 'comment' \? claimKeyedLineRefusals\(rendered\.body\) : \[\];/u.test(stampSource) && new RegExp('function\\s+(claimSeatNumber|threadReadField|readClause2Line)\\b').test(stampSource) === false);
3816+
t('the refusal names all three readers, and that no flag turns it off', ['claimSeatNumber', 'h50ThreadReadMismatch', 'readClause2Line', 'No flag turns it off'].every((s) => keyedLineRefusalText(claimKeyedLineRefusals(MISLAID_SEAT)).includes(s)));
3817+
}
3818+
36993819
// The floor, evaluated last: a battery that stops running names itself here.
37003820
const failed = cases.filter((c) => !c.ok);
37013821
for (const c of cases) console.log(` ${c.ok ? '✓' : '✗'} ${c.name}`);

0 commit comments

Comments
 (0)