From f619d317d724cc8dae9f673353ca2ddafa7831da Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 12:29:56 +0000 Subject: [PATCH] fix(pm): the record reader closes a fence only on a bare run and opens its scope only on an unindented heading; the writer refuses on a key LINE and names the two new causes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `contractReviewHeadSha` (check-half-states.mjs) closed a fence on any run of the opening character, so a closing line with trailing text — fence content to CommonMark — ended the fence and a `Head-sha:` line after it was read. It also opened its heading scope on a `## Contract review` heading indented four spaces or a tab, an indented code block to the renderer, while holding the key line itself to three spaces. Both read the same as base and appear in 0 of 713 live records; both are now closed, each pinned and ablated once. `FENCE_CLOSE_LINE` admits whitespace only after the run; the scope opens on `^[ ]{0,3}#` and the shared marker keeps its tolerance for the locator. `post-stamped.mjs` tested the `Head-sha` key ANYWHERE in a record-shaped comment, so one that quoted a record heading and named the key mid-sentence was refused at write time. `HEAD_SHA_KEY_LINE` now asks for a line opening on the key, however decorated (indent, blockquotes, a bullet or a list number, bold, backticks), so every refused shape #28042 pinned stays refused and the prose mention is not. The refusal's `why` and `RECORD_KEY_SPELLINGS['Head-sha']` name the code-indented line and the line under a quoted or code-indented heading. No new refusal class, no new gate. `contract-review.md:33` now says the `Head-sha:` line sits 题头之下, at 120 bytes and line-neutral; the `--template` note says the same in English. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_016WV4czoR81DDKSnGWnBYqA --- .../pm-dispatch/references/contract-review.md | 2 +- scripts/pm/check-half-states.mjs | 44 ++++++++++++++----- scripts/pm/post-stamped.mjs | 26 +++++++---- scripts/pm/record-recognisers.mjs | 9 ++-- 4 files changed, 56 insertions(+), 25 deletions(-) diff --git a/.claude/skills/pm-dispatch/references/contract-review.md b/.claude/skills/pm-dispatch/references/contract-review.md index b1029dbe2e7..7ac8ed545ae 100644 --- a/.claude/skills/pm-dispatch/references/contract-review.md +++ b/.claude/skills/pm-dispatch/references/contract-review.md @@ -30,7 +30,7 @@ - 新 head 即清;点名现 head 时 (b) 不起;条件满足由持有车道挂等复核标记或落交接行。 - 逾 24 小时条件未满的 hold 列入分诊轮次记录(`check-owed-reviews.mjs` 标出)。 - 复核记录 = 一条评论落 PR 或卡,席内与子代理同形;散文、dev 自评、`os-dev-report` 恒不算。 -- 同形 = `## Contract review` 题头、`Head-sha:` 行独占码段、①②③ 逐项、独立性对、PASS/FAIL。 +- 同形 = `## Contract review` 题头之下 `Head-sha:` 行独占码段、①②③逐项、独立性对、PASS/FAIL。 - 同形含首行 `Served-tier:`:值写该面所定档的常量名 `CONTRACT_REVIEW_TIER` 或 `TIER_DEFAULT`; 无此行不成裁决;模板 `node scripts/pm/record-recognisers.mjs --template`,申报面加 `--face declaration`。 - 同形带 `Local-runs:` 行:`none` 或 `probe — REASON` 具名唯一探针;有行必校,缺行旧记录不改判。 diff --git a/scripts/pm/check-half-states.mjs b/scripts/pm/check-half-states.mjs index 8cd41490085..9105b4d53c7 100644 --- a/scripts/pm/check-half-states.mjs +++ b/scripts/pm/check-half-states.mjs @@ -12687,8 +12687,16 @@ export const CONTRACT_REVIEW_HEAD_SHA_LINE = new RegExp( * open fence (a nested ``` inside a ```` block, a ~~~ inside a ``` block), and * reading past either would read fenced content as the record's line. An * unclosed fence runs to the end of the body, as it renders. + * + * An OPENING fence may carry an info string after the run (```md); a CLOSING + * fence may not — a run followed by anything but whitespace is content of the + * open fence, not its close (`FENCE_CLOSE_LINE`), as CommonMark renders it. + * Measured in the delta review of the fence-tracking reader (2026-10-30): a + * ``` followed by prose closed the fence here while the renderer kept it open, + * so a `Head-sha:` line after it was read; 0 of 713 live records carry one. */ const FENCE_LINE = /^[ ]{0,3}(`{3,}|~{3,})/; +const FENCE_CLOSE_LINE = /^[ ]{0,3}(`{3,}|~{3,})[ \t]*$/; /** * The head a contract-review record names — the span on the first `Head-sha:` @@ -12702,14 +12710,17 @@ const FENCE_LINE = /^[ ]{0,3}(`{3,}|~{3,})/; * ABOVE the record's own line — read as the head, so the record was found at * the cited head and absent at the real one. None is prose to a first-line * reader, and all four shapes are what a quotation looks like. So: lines - * before the first UNQUOTED heading-marker line are skipped — the heading is - * read with `CONTRACT_REVIEW_HEADING_MARKER`, but a blockquoted heading opens - * nothing here, because a fully quoted record (heading, head line and - * signature all under `>`) followed by a plain line opening `Head-sha:` would - * otherwise hand the quoted record's signature to the plain line's head; the - * locator still finds such a comment as a record, and it then names no head. - * A fenced line is skipped, fence boundaries included, with the fence closed - * only by its own character and length (`FENCE_LINE`); and + * before the first UNQUOTED, UNINDENTED heading-marker line are skipped — the + * heading is read with `CONTRACT_REVIEW_HEADING_MARKER`, but a blockquoted + * heading opens nothing here, because a fully quoted record (heading, head + * line and signature all under `>`) followed by a plain line opening + * `Head-sha:` would otherwise hand the quoted record's signature to the plain + * line's head; nor does a heading indented four spaces or a tab, which is an + * indented code block to the renderer, held to the same three-space bound as + * the key line itself; the locator still finds such a comment as a record, + * and it then names no head. A fenced line is skipped, fence boundaries + * included, with the fence closed only by its own character and length on a + * line of its own (`FENCE_LINE`, `FENCE_CLOSE_LINE`); and * `CONTRACT_REVIEW_HEAD_SHA_LINE` itself refuses the blockquote, the bullet * and an indented-code line. Among the lines that remain, the FIRST wins, as * `readServedTier` reads its key: a record is one comment and names one head, @@ -12729,15 +12740,18 @@ export function contractReviewHeadSha(body) { let fence = null; // the OPEN fence — its character and run length — or null outside one let belowHeading = false; for (const line of String(body ?? '').split(/\r?\n/)) { + if (fence !== null) { + const c = FENCE_CLOSE_LINE.exec(line); + if (c !== null && c[1][0] === fence.ch && c[1].length >= fence.len) fence = null; + continue; + } const f = FENCE_LINE.exec(line); if (f !== null) { - if (fence === null) fence = { ch: f[1][0], len: f[1].length }; - else if (f[1][0] === fence.ch && f[1].length >= fence.len) fence = null; + fence = { ch: f[1][0], len: f[1].length }; continue; } - if (fence !== null) continue; if (!belowHeading) { - belowHeading = CONTRACT_REVIEW_HEADING_MARKER.test(line) && !/^\s*>/.test(line); + belowHeading = CONTRACT_REVIEW_HEADING_MARKER.test(line) && /^[ ]{0,3}#/.test(line); continue; } const m = CONTRACT_REVIEW_HEAD_SHA_LINE.exec(line); @@ -33545,6 +33559,12 @@ Doubles as the fire's **write self-check** (step 0). \`201\` is not the reading. // ⛔ A fence closes only on its OWN character and a run at least as long: a nested ``` inside ````, a ~~~ inside ```, // are content, and a `Head-sha:` line between them stays fenced; a longer closing run closes; a never-closed one runs out. t('⛔ record reader: a nested or mismatched fence does not close the open one — the `Head-sha:` line inside stays fenced; a longer run closes; an unmatched run leaves it open to the end', [contractReviewHeadSha(UNDER51('````', '```', `Head-sha: \`${HEAD51}\``, '```', '````', `Head-sha: \`${OLDHEAD51}\``)), contractReviewHeadSha(UNDER51('```', '~~~', `Head-sha: \`${HEAD51}\``, '```', `Head-sha: \`${OLDHEAD51}\``)), contractReviewHeadSha(UNDER51('```', `Head-sha: \`${HEAD51}\``, '`````', `Head-sha: \`${OLDHEAD51}\``)), contractReviewHeadSha(UNDER51('```', '~~~', `Head-sha: \`${HEAD51}\``, '~~~', `Head-sha: \`${OLDHEAD51}\``))].join('|'), [OLDHEAD51, OLDHEAD51, OLDHEAD51, ''].join('|')); + // ⛔ A closing run with TRAILING TEXT is fence content, not a close (CommonMark): the fence stays open past it, so a + // `Head-sha:` line after such a line is still fenced; a bare run later closes it; whitespace after the run still closes. + t('⛔ record reader: a fence is closed only by a run on a line of its OWN — a run with trailing text is content, so the line after it stays fenced; a bare run later closes; trailing whitespace still closes', [contractReviewHeadSha(UNDER51('```', '``` not a close', `Head-sha: \`${HEAD51}\``)), contractReviewHeadSha(UNDER51('```', '``` not a close', `Head-sha: \`${HEAD51}\``, '```', `Head-sha: \`${OLDHEAD51}\``)), contractReviewHeadSha(UNDER51('~~~', '~~~ tail', `Head-sha: \`${HEAD51}\``, '~~~', `Head-sha: \`${OLDHEAD51}\``)), contractReviewHeadSha(UNDER51('```md', `Head-sha: \`${HEAD51}\``, '``` ', `Head-sha: \`${OLDHEAD51}\``))].join('|'), ['', OLDHEAD51, OLDHEAD51, OLDHEAD51].join('|')); + // ⛔ A heading indented as CODE (four spaces or a tab) opens no scope — the same three-space bound the key line is held + // to — while one indented up to three spaces does; the marker itself keeps its tolerance for the locator. + t('⛔ record reader: a `## Contract review` heading indented four spaces or a tab opens no scope, one indented three does; the marker still finds the indented one', [contractReviewHeadSha([' ## Contract review', `Head-sha: \`${HEAD51}\``].join('\n')), contractReviewHeadSha(['\t## Contract review', `Head-sha: \`${HEAD51}\``].join('\n')), contractReviewHeadSha([' ## Contract review', `Head-sha: \`${HEAD51}\``].join('\n')), latestContractReviewOnHead([cm51(1, [' ## Contract review', `Head-sha: \`${HEAD51}\``].join('\n'), T51)], HEAD51), CONTRACT_REVIEW_HEADING_MARKER.test(' ## Contract review')].join('|'), ['', '', HEAD51, '', 'true'].join('|')); t('⛔ record reader: the key folded INTO the span, a bare hex value, a lowercase key and a sub-floor span name no head', [contractReviewHeadSha(UNDER51(`\`Head-sha: ${HEAD51}\``)), contractReviewHeadSha(UNDER51(`Head-sha: ${HEAD51}`)), contractReviewHeadSha(UNDER51(`head-sha: \`${HEAD51}\``)), contractReviewHeadSha(UNDER51('Head-sha: `ba3d95`'))].join('|'), '|||'); // ⛔ The shapes the review of the line-form reader probed (2026-10-30): a `Head-sha:` line naming ANOTHER head above the // record's own — placed before the heading, blockquoted, bulleted or fenced. Each is a citation, not the record's line, diff --git a/scripts/pm/post-stamped.mjs b/scripts/pm/post-stamped.mjs index 93a4bcce686..5f12ee6104b 100644 --- a/scripts/pm/post-stamped.mjs +++ b/scripts/pm/post-stamped.mjs @@ -2060,17 +2060,20 @@ export function keyedLineRefusalText(rows) { /** What the record's owner accepts, printed BY the refusal so the fix is one line away. */ export const RECORD_KEY_SPELLINGS = Object.freeze({ - 'Head-sha': '`Head-sha: ` then the reviewed head as 7 to 40 hex in a code span of its OWN, at the START of a line BELOW the `## Contract review` heading — not blockquoted, not bulleted, not inside a fence, not before the heading; the first such line is the head the record names, and a record written before the line existed may omit it', + 'Head-sha': '`Head-sha: ` then the reviewed head as 7 to 40 hex in a code span of its OWN, at the START of a line (up to three spaces of indent) BELOW an unquoted, unindented `## Contract review` heading — not blockquoted, not bulleted, not inside a fence, not indented as code (four spaces or a tab), not before the heading, not under a quoted or code-indented heading; the first such line is the head the record names, and a record written before the line existed may omit it', 'Local-runs': '`Local-runs: none` at the START of a line, or `Local-runs: probe — REASON` naming the ONE local run the read-only rule was broken for and why; a record written before the line existed may omit it', }); -/** PRESENCE only, case-blind: a `Head-sha` key anywhere in the body, however decorated or placed. Consulted solely to - * tell "the author wrote the key and the reader cannot read it" (refused) from "no key at all" (not re-judged). */ -const HEAD_SHA_KEY_ANYWHERE = /head-sha[ \t`*]*:/i; +/** PRESENCE only, case-blind: a `Head-sha` key opening a LINE, however decorated or placed — any indent, blockquote + * markers, a bullet or a list number, bold or backticks around the key — so a decorated, fenced, code-indented or + * pre-heading key line still counts as written. Consulted solely to tell "the author wrote the key and the reader + * cannot read it" (refused) from "no key at all" (not re-judged). ⛔ Not mid-sentence: a comment that quotes a record's + * heading and then names the key in prose wrote no key line, and was refused for it until 2026-10-30 (loudness only). */ +const HEAD_SHA_KEY_LINE = /^[ \t]*(?:>[ \t]*)*(?:(?:[-*+]|\d+[.)])[ \t]+)?[`*]*head-sha[ \t`*]*:/im; /** The first line carrying a `Head-sha` key, however spelled — a LOCATOR for the writer's eye, ⛔ never the judgement. */ function headShaKeyLine(text) { - const hit = text.split(/\r?\n/).find((line) => HEAD_SHA_KEY_ANYWHERE.test(line)); + const hit = text.split(/\r?\n/).find((line) => HEAD_SHA_KEY_LINE.test(line)); return hit === undefined ? '(no single line carries the key)' : offendingSpan(hit.trim(), 160); } @@ -2081,7 +2084,8 @@ function headShaKeyLine(text) { * verdict over a line nobody had been asked to write. Scoped by the record's own heading marker, on `--comment` * alone — a record IS a comment. * Head-sha `contractReviewHeadSha` (`check-half-states.mjs`) is the ONE reader every guard reaches the record's head - * through: the first plain `Head-sha:` line below the heading, outside any blockquote, bullet or fence. A + * through: the first plain `Head-sha:` line below an unquoted, unindented heading, outside any blockquote, + * bullet, fence or indented code. A * body that carries the key somewhere and reads `null` there would be filed by every reader as a review of * NO head — absent on the owed list, refused at the queue — so it is refused here, before the write. The * template's null-oid placeholder reads as a head (it is 40 hex) and is not this row's: C6 refuses it. @@ -2093,8 +2097,8 @@ export function recordKeyedLineRefusals(body) { const text = String(body ?? ''); if (!CONTRACT_REVIEW_HEADING_MARKER.test(text)) return []; const rows = []; - if (HEAD_SHA_KEY_ANYWHERE.test(text) && contractReviewHeadSha(text) === null) - rows.push({ key: 'Head-sha', line: headShaKeyLine(text), why: '`contractReviewHeadSha` reads NO head here — a `Head-sha` key is present, but no line below the `## Contract review` heading carries it plainly with a hex span of its own (a blockquoted, bulleted, fenced or pre-heading line, the key folded into the span, a bare hex value, a lowercase key and a sub-floor span all read as nothing). Every reader would file this record as a review of NO head.' }); + if (HEAD_SHA_KEY_LINE.test(text) && contractReviewHeadSha(text) === null) + rows.push({ key: 'Head-sha', line: headShaKeyLine(text), why: '`contractReviewHeadSha` reads NO head here — a `Head-sha` key opens a line, but no line below an unquoted, unindented `## Contract review` heading carries it plainly with a hex span of its own (a blockquoted, bulleted, fenced or pre-heading line, a code-indented line — four spaces or a tab — a line under a quoted or code-indented heading, the key folded into the span, a bare hex value, a lowercase key and a sub-floor span all read as nothing). Every reader would file this record as a review of NO head.' }); const runs = readLocalRuns(text); if (!localRunsStand(runs)) rows.push({ key: 'Local-runs', line: runs.line, why: '`readLocalRuns` reads the line and cannot grade its value — `none` and `probe — REASON` are the closed set, and a probe with no reason names no deviation.' }); @@ -5319,6 +5323,12 @@ export function selfTest() { t('⭐ a quoted line ABOVE the record\'s own passes — the reader skips the quotation and reads the line below it', rkeys(SHA_WITH([`> Head-sha: \`${'f'.repeat(40)}\``, `Head-sha: \`${SHA_HEAD}\``])) === ''); t('⛔ ABSENT is NOT re-judged for this key either: a record with no `Head-sha` key at all passes untouched — the retired-dialect records carry none', rkeys(SHA_WITH([])) === ''); t('⛔ a fully QUOTED record, and a code-indented (four spaces or tab) line alone, are refused — a quoted heading opens no scope and an indented line is code', rkeys(SHA_WITH([`Head-sha: \`${SHA_HEAD}\``]).split('\n').map((l) => `> ${l}`).join('\n')) === 'Head-sha' && rkeys(SHA_WITH([` Head-sha: \`${SHA_HEAD}\``])) === 'Head-sha' && rkeys(SHA_WITH([`\tHead-sha: \`${SHA_HEAD}\``])) === 'Head-sha'); + // ⛔ PRESENCE is a key LINE, however decorated, ⛔ never a mid-sentence mention: a record-shaped comment that names the key + // in prose wrote no line and is untouched; a key under a code-indented heading, or opening a numbered, doubly quoted + // or `+`-bulleted line, is a written line the reader refuses, and is refused here. + t('⭐ NOT a key line: a record-shaped comment that names `Head-sha:` mid-sentence, with no line opening on the key, is untouched — presence is a key LINE', rkeys('## Contract review\n\nThe seat\'s own record carries its Head-sha: line plainly, below the heading; see `Head-sha:` in the template.') === '' && rkeys(`> ## Contract review\n\nquoted above; the record\'s Head-sha: line is \`${SHA_HEAD}\` there`) === ''); + t('⛔ …while a decorated key LINE still counts as written and is refused when the reader names no head: numbered, doubly quoted, `+`-bulleted, and one under a CODE-INDENTED heading', rkeys(SHA_WITH([`1. Head-sha: \`${SHA_HEAD}\``])) === 'Head-sha' && rkeys(SHA_WITH([`> > **Head-sha:** \`${SHA_HEAD}\``])) === 'Head-sha' && rkeys(SHA_WITH([`+ Head-sha: \`${SHA_HEAD}\``])) === 'Head-sha' && rkeys(` ## Contract review\nHead-sha: \`${SHA_HEAD}\``) === 'Head-sha'); + t('the `Head-sha` refusal and the printed spelling NAME the two causes the reader added — a code-indented line and a line under a quoted or code-indented heading', (() => { const text = recordLineRefusalText(recordKeyedLineRefusals(SHA_WITH([`\tHead-sha: \`${SHA_HEAD}\``]))); const rule = RECORD_KEY_SPELLINGS['Head-sha']; return text.includes('a code-indented line — four spaces or a tab') && text.includes('a line under a quoted or code-indented heading') && rule.includes('not indented as code (four spaces or a tab)') && rule.includes('not under a quoted or code-indented heading') && rule.includes('BELOW an unquoted, unindented'); })()); t('both lines unreadable ⇒ both rows, `Head-sha` first', rkeys(SHA_WITH([`> Head-sha: \`${SHA_HEAD}\``], 'Local-runs: probe')) === 'Head-sha,Local-runs'); t('the `Head-sha` refusal prints the spelling that reads, names the owner, and quotes the offending line', (() => { const rows = recordKeyedLineRefusals(SHA_WITH([`> Head-sha: \`${SHA_HEAD}\``])); const text = recordLineRefusalText(rows); return text.includes(RECORD_KEY_SPELLINGS['Head-sha']) && text.includes('contractReviewHeadSha') && rows[0].line.includes('> Head-sha'); })()); t('⛔ NOT a record: a claim or a report carrying a quoted `Head-sha:` line is untouched — the reader runs on the record\'s heading only', rkeys(`Claim: x\n> Head-sha: \`${SHA_HEAD}\``) === '' && rkeys(`os-dev-report\n\n> Head-sha: \`${SHA_HEAD}\``) === ''); diff --git a/scripts/pm/record-recognisers.mjs b/scripts/pm/record-recognisers.mjs index 8f8eb3d85e9..de90897e05e 100644 --- a/scripts/pm/record-recognisers.mjs +++ b/scripts/pm/record-recognisers.mjs @@ -1720,10 +1720,11 @@ export function contractReviewTemplateLines(values = {}) { ` the review owed by the \`Clause-②: yes\` line alone ⇒ ${TIER_DEFAULT_NAME} (--face declaration).`, faceLine, ' An at-tier/total stamp control may precede it — 75/75, then the constant.', - ' · Head-sha the head you reviewed, 7 to 40 hex in a span of ITS OWN, on THIS line: it is', - ' the only place the head is read, so a sha in the heading or in the prose names', - ' nothing; a span holding the key as well is not a sha, and the record then names', - ' no head.', + ' · Head-sha the head you reviewed, 7 to 40 hex in a span of ITS OWN, on THIS line, plain and', + ' BELOW the heading: it is the only place the head is read, so a sha in the heading', + ' or in the prose names nothing; a span holding the key as well is not a sha, and a', + ' quoted, bulleted, fenced, code-indented or pre-heading line is prose — the record', + ' then names no head.', ' · Local-runs `none` — the read-only shape held (the diff, the card and the head\'s check-runs;', ' nothing built, run or re-run locally); or `probe — REASON`, naming the ONE local run', ' the rule was broken for. A record without the line predates it and is not re-judged;',