From d7dffeb04542f6f2b09ef8b3615490cb39d4f508 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 16:47:59 +0000 Subject: [PATCH] =?UTF-8?q?fix(pm):=20make=20the=20stamp=20contract=20quot?= =?UTF-8?q?able=20=E2=80=94=20Markdown=20code=20is=20a=20quotation,=20rend?= =?UTF-8?q?ered=20as=20written?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `post-stamped` substituted the act-clock token wherever it appeared, a passage QUOTING the token included, so a seat could not quote the tool's own diagnostic through the tool. Measured three times inside one hour on live artefacts: a false quotation published on #14251 (an inline code span inside a blockquote), the same hit in the sentence describing it and again in the comment reporting that, plus a dispatch claim refused `[quoted-not-a-stamp]` for spelling the contract's own documentation placeholder. The remedy is structural, not a third spelling and not a flag: inside Markdown code — a fenced block or a backtick span — this tool renders TEXT. Substitution, the opener scan and the quoted-stamp validation all stand down there; a reader of the stored artefact sees the rule without knowing the tool exists. It suppresses nothing that judges a stamp a human typed. The positional scan reads every line, code included; the MIXED trigger stays deliberately quote-unaware; and `maskQuotedStamps` blanks a quoted-stamp token only where it IS a token, so no stamp can hide from the contract behind backticks. The status line now reports how many openers were left verbatim beside how many were substituted, so the count is no longer the only signal an author has to compare against intent. Self-test: 257 cases across 11 batteries -> 308 across 12. Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu Co-authored-by: Claude --- scripts/pm/post-stamped.mjs | 552 +++++++++++++++++++++++++++++++++--- 1 file changed, 514 insertions(+), 38 deletions(-) diff --git a/scripts/pm/post-stamped.mjs b/scripts/pm/post-stamped.mjs index d0dc60f58e4..baba105b562 100644 --- a/scripts/pm/post-stamped.mjs +++ b/scripts/pm/post-stamped.mjs @@ -192,10 +192,100 @@ * so the BARE-stamp scan underneath it is reading prose and not the inside of a * token nobody could parse. * - * ⛔ And the scan opens no escape hatch. There has never been one — no - * backslash form, no entity form — and a token inside backticks is still - * substituted, because a fence is a rendering instruction and the substitution - * runs on bytes. A body that must SHOW a token spells it some other way. + * ⛔ And the scan opens no escape hatch OUTSIDE a quoted span — no backslash + * form, no entity form, no flag. What it does open is the next section, which + * is not a spelling of the token contract at all. + * + * ## The contract is QUOTABLE: inside Markdown code, this tool renders text (#18543) + * + * The substitution used to run on bytes, so a passage QUOTING the token was + * rewritten like any other. Measured three times inside one hour, by two + * seats, on live artefacts: + * + * ① #14251 carried a verbatim quote of this tool's OWN status line, in an + * inline code span inside a blockquote. The token inside the quotation was + * substituted and a sentence this tool never printed was published as a + * quotation of it. The only signal was `substitutions: 2` where the author + * meant 1 — a count, not a warning, and nothing compared it to intent. + * ② The seat filing ① hit it again in the sentence DESCRIBING ①, and a third + * time in the comment reporting ②. Care is not a remedy: every one of the + * three was written by an author who was thinking about this exact defect. + * ③ The dispatch claim for this card spelled both token forms inside + * backticks and was REFUSED `[quoted-not-a-stamp]` — the quoted route read + * its own documentation placeholder as a declaration. So the contract + * could not be quoted through this tool, nor explained through it. + * + * ⛔ The remedy is NOT a third spelling, and ⛔ NOT a flag. Markdown already + * has one construct that means "this is text, not instructions", and it has it + * in two forms — a fenced code block and a backtick code span. So: + * + * INSIDE A QUOTED SPAN THIS TOOL RENDERS TEXT, NOT TOKENS. + * + * A reader of the stored artefact sees the rule without knowing the tool + * exists: the backticks are right there, on the page, in the spelling every + * other quotation on the board already uses. Nothing was added to the token + * contract — it still has exactly two spellings — and no caller has to + * remember a magic word it would itself have to quote to document. + * + * What a quoted span suppresses is exactly the three rules that READ a token: + * substitution, the opener scan, and the quoted-stamp validation. An opener + * inside one is not refused, a `{{WAS:…}}` inside one is not judged against + * the calendar or the clock, and both are written out exactly as the author + * typed them. + * + * ⛔ And it suppresses NOTHING that judges a stamp a human typed. This is the + * load-bearing asymmetry, and it is the whole reason the rule is safe: quoting + * changes what is RENDERED, never what was AUTHORED. A stamp inside a fence is + * still digits on the board. + * + * POSITIONAL reads every line, code included. A bare stamp in a code span on + * the opening line is refused exactly as in prose. + * MIXED triggers on the act-clock token appearing ANYWHERE in the body, + * quoted or not. An author who spells the token knows it exists, + * and a bare stamp elsewhere is ambiguous to a reader whatever + * backticks sit around the other one. ⛔ Deliberately NOT made + * quote-aware: that is the one direction this change could have + * weakened a refusal, and it does not take it. + * MASKING `maskQuotedStamps` blanks a `{{WAS:…}}` only where it is a + * TOKEN. Inside a quoted span it is text, so the digits it + * carries stay visible to the bare-stamp scan — otherwise a + * stamp could hide from the contract behind backticks, which is + * the accident this rule must never buy. + * + * So the refusal surface is unchanged or STRICTER everywhere except the three + * token rules inside a quoted span, which is the deliverable. One body changes + * direction: `` `{{WAS:}}` `` beside the act-clock token used to + * be accepted and rendered as bare digits — that acceptance WAS defect ① — and + * is now MIXED-refused, with the refusal saying that the stamp sits inside a + * quotation so neither spelling will render there. + * + * ⛔ The count is no longer the only signal. The status line reports how many + * openers were left VERBATIM inside quoted spans beside how many were + * substituted, so an author who meant to quote one and stamp one reads both + * numbers and can compare them to intent — which is what ① had no way to do. + * + * What a quoted span IS, exactly (`quotedSpans`), and what it deliberately is + * not: + * + * FENCED a line opening with three or more backticks or tildes (up to + * three leading spaces), through its closing fence — or the end + * of the body, the way CommonMark ends an unclosed one. Tracked + * through blockquote markers, since a seat quoting a tool's + * output inside a quote is the shape ① was written in. + * CODE SPAN a backtick run closed by a run of the SAME length, ⛔ searched + * within one line only. CommonMark lets a span cross lines; this + * does not, on purpose — under-detecting leaves today's + * behaviour, and today's behaviour is what every existing caller + * already has. + * ⛔ NOT a four-space indented block. Indentation is load-bearing in + * lists and continuations, so reading it as a quotation would + * make the rule fire where no reader sees a quotation. + * + * The two failure directions are not symmetric, which is why that scanner is + * conservative: under-detecting substitutes a token the author wanted verbatim + * — the state before this rule — while over-detecting leaves an artefact + * UNSTAMPED. The status line's verbatim count is what makes the second one + * visible in the same breath. * * ## ⚖️ Why this ACTS by default, where `sweep-closed-cards.mjs` dry-runs * @@ -476,6 +566,175 @@ const TOKEN_CLOSER = '}}'; /** How much of an offending span a refusal prints. */ export const SPAN_BYTES = 60; +// --------------------------------------------------------------------------- +// The quoting spelling — Markdown's own "this is text, not instructions". +// The header's quotable-contract section is the authority on why this is a +// STRUCTURAL rule and not a third token. +// --------------------------------------------------------------------------- + +/** The two constructs a quoted span can be, in the words a reader would use. */ +export const QUOTED_SPAN_KINDS = Object.freeze({ + fenced: 'a fenced code block', + 'code-span': 'a backtick code span', +}); + +/** + * A blockquote prefix, as CommonMark reads one: any number of `>` markers, each + * allowed up to three leading spaces and one trailing space. Returned as the + * DEPTH and the line that is left, because a fenced block inside a quote ends + * when the quote does — and a seat quoting a tool's output inside a blockquote + * is the exact shape the filed instance was written in. + */ +function blockquotePrefix(line) { + let i = 0; + let depth = 0; + for (;;) { + let j = i; + let spaces = 0; + while (j < line.length && line[j] === ' ' && spaces < 3) { + j += 1; + spaces += 1; + } + if (line[j] !== '>') break; + j += 1; + if (line[j] === ' ') j += 1; + depth += 1; + i = j; + } + return { depth, rest: line.slice(i) }; +} + +/** + * The fence this line opens, or null. A backtick fence's info string may not + * carry a backtick (CommonMark's rule, and the one that keeps `` `a` `` on a + * line of prose from reading as a fence); a tilde fence's may. + */ +function fenceOpenedBy(line) { + const m = /^ {0,3}(`{3,}|~{3,})(.*)$/u.exec(line); + if (!m) return null; + if (m[1][0] === '`' && m[2].includes('`')) return null; + return { char: m[1][0], length: m[1].length }; +} + +/** Whether this line is a closing fence for `open` — same character, at least as long, nothing else on it. */ +function fenceClosedBy(line, open) { + const m = /^ {0,3}(`{3,}|~{3,})[ \t]*$/u.exec(line); + return m !== null && m[1][0] === open.char && m[1].length >= open.length; +} + +/** + * Every backtick code span on one line, as offsets into the whole body. + * + * A run of N backticks opens; the span ends at the next run of EXACTLY N. A run + * of a different length is content and is stepped over, and a run with no + * matching closer is literal backticks — so `` don't use `foo `` is prose, not + * an unterminated quotation swallowing the rest of the artefact. + */ +function codeSpansOnLine(line, base) { + const out = []; + let i = 0; + while (i < line.length) { + if (line[i] !== '`') { + i += 1; + continue; + } + let n = 0; + while (i + n < line.length && line[i + n] === '`') n += 1; + let j = i + n; + let found = -1; + while (j < line.length) { + if (line[j] !== '`') { + j += 1; + continue; + } + let m = 0; + while (j + m < line.length && line[j + m] === '`') m += 1; + if (m === n) { + found = j; + break; + } + j += m; + } + if (found === -1) { + i += n; + continue; + } + out.push({ from: base + i, to: base + found + n, kind: 'code-span' }); + i = found + n; + } + return out; +} + +/** + * Every quoted span in this body, in order and non-overlapping: the ranges + * inside which this tool renders text and reads no token at all. + * + * Fenced blocks are resolved first, at the line level, because Markdown parses + * block structure before inline structure — so a backtick run inside a fence is + * fence CONTENT and never opens a span of its own. + */ +export function quotedSpans(text) { + const raw = String(text ?? ''); + const lines = raw.split('\n'); + const fenced = []; + const fencedLines = new Set(); + let open = null; + let offset = 0; + + for (let n = 0; n < lines.length; n += 1) { + const line = lines[n]; + const lineFrom = offset; + const lineTo = offset + line.length; + offset = lineTo + 1; + const { depth, rest } = blockquotePrefix(line); + + if (open) { + if (depth < open.depth) { + // The blockquote holding the fence ended, so the block ended with it. + fenced.push({ from: open.from, to: lineFrom, kind: 'fenced' }); + open = null; + } else { + fencedLines.add(n); + if (fenceClosedBy(rest, open)) { + fenced.push({ from: open.from, to: lineTo, kind: 'fenced' }); + open = null; + } + continue; + } + } + + const opened = fenceOpenedBy(rest); + if (opened) { + open = { ...opened, depth, from: lineFrom }; + fencedLines.add(n); + } + } + if (open) fenced.push({ from: open.from, to: raw.length, kind: 'fenced' }); + + const spans = [...fenced]; + offset = 0; + for (let n = 0; n < lines.length; n += 1) { + const line = lines[n]; + const lineFrom = offset; + offset = lineFrom + line.length + 1; + if (fencedLines.has(n)) continue; + spans.push(...codeSpansOnLine(line, lineFrom)); + } + return spans.sort((a, b) => a.from - b.from); +} + +/** + * Whether the character at `at` is inside one of `spans`. + * + * An opener is judged by WHERE IT STARTS — a `{{` that begins inside a + * quotation is quoted, whatever happens to fall after it. One position, one + * answer, so the scan, the mask and the substitution cannot come to disagree + * about the same brace. + */ +export function insideQuotedSpan(spans, at) { + return (spans ?? []).some((s) => at >= s.from && at < s.to); +} + /** * The C0 controls that have a spelling everybody reads; the rest get `\xNN`. * ⛔ Written as escapes rather than as the bytes themselves — a raw control @@ -529,8 +788,13 @@ export const OPENER_REASONS = Object.freeze({ * those same regexes would walk straight past it again. What the regexes are * still used for is recognition at a known position — anchored, so the one * definition of "a quoted token" serves both the scan and the substitution. + * + * An opener inside a QUOTED SPAN is skipped: there it is text the author is + * showing, and refusing it is how the contract became unquotable. It is still + * counted — `substituteTokens` reports it as verbatim — so a skip is never + * silent. */ -export function unrecognisedOpeners(text) { +export function unrecognisedOpeners(text, spans = quotedSpans(text)) { const raw = String(text ?? ''); const quotedHere = anchoredOf(QUOTED_TOKEN_RE); const anyHere = anchoredOf(ANY_TOKEN_RE); @@ -541,6 +805,11 @@ export function unrecognisedOpeners(text) { if (at === -1) return out; const rest = raw.slice(at); + if (insideQuotedSpan(spans, at)) { + i = at + TOKEN_OPENER.length; + continue; + } + if (rest.startsWith(STAMP_TOKEN)) { i = at + STAMP_TOKEN.length; continue; @@ -603,17 +872,31 @@ export function stampNow(ms = Date.now()) { * The text with every declared quoted stamp blanked to spaces of equal length, * so a scan for BARE stamps sees only the ones nobody declared — and so line * numbers, columns and the opening line are all still where they were. + * + * ⛔ A `{{WAS:…}}` inside a QUOTED SPAN is NOT blanked: there it is not a + * declaration, it is text showing what a declaration looks like, and the digits + * it carries are digits on the board like any others. Blanking them would let a + * hand-typed stamp hide from the bare-stamp scan behind a pair of backticks — + * the one accident this rule may never buy. */ -export function maskQuotedStamps(text) { - return String(text ?? '').replace(globalOf(QUOTED_TOKEN_RE), (m) => ' '.repeat(m.length)); +export function maskQuotedStamps(text, spans = quotedSpans(text)) { + return String(text ?? '').replace(globalOf(QUOTED_TOKEN_RE), (m, _inner, at) => + insideQuotedSpan(spans, at) ? m : ' '.repeat(m.length), + ); } -/** The values inside every `{{WAS:…}}` in this text. */ -export function quotedStampValues(text) { +/** + * The values inside every `{{WAS:…}}` in this text that is a TOKEN — so the + * ones inside a quoted span are left out, because the calendar and direction + * rules judge a declaration and there is none there. + */ +export function quotedStampValues(text, spans = quotedSpans(text)) { const re = globalOf(QUOTED_TOKEN_RE); const out = []; let m; - while ((m = re.exec(String(text ?? '')))) out.push(m[1]); + while ((m = re.exec(String(text ?? '')))) { + if (!insideQuotedSpan(spans, m.index)) out.push(m[1]); + } return out; } @@ -688,6 +971,31 @@ function quotedRouteClosed(stamp, nowMs) { return null; } +/** + * The clause a remedy appends when EVERY occurrence of the offending stamp sits + * inside a quoted span — empty when at least one of them does not. + * + * Without it the two remedies prescribe a route that cannot work there: inside + * a quotation neither spelling is substituted, so a seat that follows the text + * literally gets the token printed where it wanted a time and reads the same + * refusal again. A refusal text prescribing a refused remedy is a tool arguing + * with itself — the rule the `{{WAS:…}}` direction check already states, taken + * one step further now that a quotation can hold a stamp. + */ +function quotedSpanClause(raw, spans, stamp) { + const text = String(raw ?? ''); + const hits = []; + for (let at = text.indexOf(stamp); at !== -1; at = text.indexOf(stamp, at + 1)) hits.push(at); + if (hits.length === 0 || !hits.every((at) => insideQuotedSpan(spans, at))) return ''; + return ( + ' ⚠️ Every occurrence of this stamp sits inside a QUOTED SPAN, where this tool renders text and ' + + 'substitutes nothing — so writing either spelling there prints the token itself, not a time. Move ' + + 'the stamp out of the quotation to declare it, or, if the quotation is an EXAMPLE, quote the ' + + 'placeholder form (`YYYY-MM-DDThh:mmZ`) instead of digits: quoting changes what is rendered, never ' + + 'what was typed onto the board.' + ); +} + /** * Every reason this body may not be posted, in the order a reader should fix * them. An empty array is a body that may be written. @@ -696,13 +1004,13 @@ function quotedRouteClosed(stamp, nowMs) { * substitutes, passed through so the direction check judges against the instant * this body is being written at, never a second read taken later. */ -export function stampRefusals(text, nowMs = Date.now()) { +export function stampRefusals(text, nowMs = Date.now(), spans = quotedSpans(text)) { const raw = String(text ?? ''); - const masked = maskQuotedStamps(raw); + const masked = maskQuotedStamps(raw, spans); const refusals = []; const now = stampNow(nowMs); - for (const value of quotedStampValues(raw)) { + for (const value of quotedStampValues(raw, spans)) { if (protocolStamps(value).length !== 1 || protocolStamps(value)[0] !== value.trim()) { refusals.push({ kind: 'quoted-not-a-stamp', @@ -752,10 +1060,11 @@ export function stampRefusals(text, nowMs = Date.now()) { const closed = quotedRouteClosed(hit.stamp, nowMs); refusals.push({ kind: 'positional', - detail: closed - ? `${opener}Write \`${STAMP_TOKEN}\` there. The quoted route is NOT open to this one: ${closed}.` - : `${opener}Write \`${STAMP_TOKEN}\` there, or \`{{WAS:${hit.stamp}}}\` if it ` + - 'is genuinely a reading of something else.', + detail: + (closed + ? `${opener}Write \`${STAMP_TOKEN}\` there. The quoted route is NOT open to this one: ${closed}.` + : `${opener}Write \`${STAMP_TOKEN}\` there, or \`{{WAS:${hit.stamp}}}\` if it ` + + 'is genuinely a reading of something else.') + quotedSpanClause(raw, spans, hit.stamp), }); } @@ -770,12 +1079,13 @@ export function stampRefusals(text, nowMs = Date.now()) { const closed = quotedRouteClosed(stamp, nowMs); refusals.push({ kind: 'mixed', - detail: closed - ? `${opener}Make it \`${STAMP_TOKEN}\` if it is this act's own. The quoted route is NOT open to ` + - `it: ${closed}.` - : `${opener}Declare ` + - `it with \`{{WAS:${stamp}}}\` if it is a quoted reading, or make it \`${STAMP_TOKEN}\` if it ` + - 'is this act\'s own.', + detail: + (closed + ? `${opener}Make it \`${STAMP_TOKEN}\` if it is this act's own. The quoted route is NOT open to ` + + `it: ${closed}.` + : `${opener}Declare ` + + `it with \`{{WAS:${stamp}}}\` if it is a quoted reading, or make it \`${STAMP_TOKEN}\` if it ` + + 'is this act\'s own.') + quotedSpanClause(raw, spans, stamp), }); } } @@ -796,6 +1106,67 @@ export function refusalText(refusals) { ); } +/** + * The body as it goes to the platform, and the three counts a reader compares + * to intent: tokens SUBSTITUTED with this act's clock, quoted stamps RENDERED + * from their declaration, and openers left VERBATIM because they sit inside a + * quoted span. + * + * One left-to-right walk rather than two regex sweeps, so every brace in the + * body is judged against the same span map that `unrecognisedOpeners` and + * `maskQuotedStamps` were given — three passes disagreeing about which `{{` is + * quoted would be three spellings of one decision. + */ +export function substituteTokens(raw, stamp, spans = quotedSpans(raw)) { + const text = String(raw ?? ''); + const quotedHere = anchoredOf(QUOTED_TOKEN_RE); + let out = ''; + let i = 0; + let substituted = 0; + let quoted = 0; + let verbatim = 0; + for (;;) { + const at = text.indexOf(TOKEN_OPENER, i); + if (at === -1) { + out += text.slice(i); + break; + } + out += text.slice(i, at); + const rest = text.slice(at); + const isQuoted = insideQuotedSpan(spans, at); + if (rest.startsWith(STAMP_TOKEN)) { + if (isQuoted) { + verbatim += 1; + out += STAMP_TOKEN; + } else { + substituted += 1; + out += stamp; + } + i = at + STAMP_TOKEN.length; + continue; + } + const was = quotedHere.exec(rest); + if (was) { + if (isQuoted) { + verbatim += 1; + out += was[0]; + } else { + quoted += 1; + out += was[1]; + } + i = at + was[0].length; + continue; + } + // Not a token at all. Outside a quoted span `unrecognisedOpeners` has + // already refused the body, so this branch only ever runs inside one — + // where the braces are text and are counted as left-as-written. + if (isQuoted) verbatim += 1; + out += TOKEN_OPENER; + i = at + TOKEN_OPENER.length; + } + return { body: out, substituted, quoted, verbatim }; +} + /** * The body as it will be written, or the refusal. Pure, so every branch that * decides whether a write happens at all is pinned offline. @@ -811,27 +1182,25 @@ export function renderBody(text, nowMs = Date.now()) { ' reader has to judge; supply a body with --file=PATH or on stdin.', }; } + // ONE span map, read by the opener scan, the mask, the quoted-stamp + // validation and the substitution. Computed here rather than four times + // below so no two of them can come to disagree about which `{{` is quoted. + const spans = quotedSpans(raw); + // ⛔ Ahead of `stampRefusals`, and not folded into it. Its bare-stamp scan // reads `maskQuotedStamps`, which is built out of the very regex an // unrecognised opener defeats — so until every opener is a token, what that // scan calls "a bare stamp in the opening line" may be the inside of a token // nobody could parse. One opener, one refusal: the shape first, alone. - const openers = unrecognisedOpeners(raw); + const openers = unrecognisedOpeners(raw, spans); if (openers.length > 0) { return { ok: false, kind: 'unknown-token', openers, error: unrecognisedOpenerText(openers) }; } - const refusals = stampRefusals(raw, nowMs); + const refusals = stampRefusals(raw, nowMs, spans); if (refusals.length > 0) return { ok: false, kind: 'stamp-contract', refusals, error: refusalText(refusals) }; const stamp = stampNow(nowMs); - let quoted = 0; - let body = raw.replace(globalOf(QUOTED_TOKEN_RE), (_m, inner) => { - quoted += 1; - return inner; - }); - const substituted = body.split(STAMP_TOKEN).length - 1; - body = body.split(STAMP_TOKEN).join(stamp); // ⛔ No second leftover scan here. The one that used to sit at this line // matched `{{…}}` AFTER substitution, which is both too late and too narrow: @@ -841,7 +1210,26 @@ export function renderBody(text, nowMs = Date.now()) { // instead, and the two stamps substituted here carry no braces — so a second // check at this line could never fire, and a check that cannot fire is a // check nobody maintains. - return { ok: true, body, stamp, substituted, quoted }; + const { body, substituted, quoted, verbatim } = substituteTokens(raw, stamp, spans); + return { ok: true, body, stamp, substituted, quoted, verbatim }; +} + +/** + * The three counts, in one spelling, so the DRY RUN line, the status line and + * `--json` cannot come to describe the same render three ways. + * + * The VERBATIM count is the half this file was missing: before it, a body that + * quoted the token and one that used it were distinguishable only by a + * substitution count nothing compared to intent — which is exactly how a false + * quotation reached #14251 at exit 0. An author who meant "stamp one, quote + * one" now reads both numbers and sees at a glance which happened. + */ +export function substitutionSummary({ substituted = 0, quoted = 0, verbatim = 0 } = {}) { + return ( + `${substituted} ${STAMP_TOKEN}, ${quoted} quoted` + + ` · verbatim: ${verbatim} opener(s) inside a quoted span, left exactly as written` + + (verbatim === 0 ? ' (none)' : '') + ); } /** @@ -1421,6 +1809,11 @@ const USAGE = [ ' than the clock this run reads is REFUSED too — the future is not a thing anyone read — and so is', ' one whose digits name no instant the calendar has (month 13, 99:99, 31 April), whether it fails to', ' parse at all or rolls over silently to another date.', + ' To QUOTE the contract rather than use it, put it in Markdown code — a fenced block or a backtick', + ' span. Inside one this tool renders TEXT: nothing is substituted, no opener is refused, no quoted', + ' stamp is validated, and the status line reports how many openers were left verbatim beside how many', + ' were substituted. ⛔ A bare stamp is judged inside a quotation exactly as in prose — quoting changes', + ' what is rendered, never what was typed onto the board.', ' A body refresh is REFUSED while comments newer than the body\'s last write stamp exist and', ' --ack-through=ID does not name the newest of them — a refresh must not void an unread knock.', ' The attribution footer is the caller\'s: its form differs by channel and act, so this tool adds none.', @@ -1468,8 +1861,8 @@ async function main(argv) { if (options.dryRun) { console.error( - `post-stamped: DRY RUN — nothing was written. ${rendered.substituted} token(s) substituted with ` + - `\`${rendered.stamp}\`, ${rendered.quoted} quoted stamp(s) rendered verbatim. Target would be ` + + `post-stamped: DRY RUN — nothing was written. Substituted with \`${rendered.stamp}\` — ` + + `${substitutionSummary(rendered)}. Target would be ` + `${repoRes.repo}#${options.number} (${options.mode}).` + (options.mode === 'body' ? ' The unread-comment check reads the card and runs only on a live write.' : ''), ); @@ -1524,6 +1917,7 @@ async function main(argv) { stamp: rendered.stamp, substituted: rendered.substituted, quoted: rendered.quoted, + verbatim: rendered.verbatim, written_at: written.writtenAt, drift_minutes: verdict.drift, body_mutated: verdict.mutated, @@ -1555,7 +1949,7 @@ async function main(argv) { ` ${options.mode === 'comment' ? 'comment' : 'card'}: ${written.id} ${written.url ?? '(no url returned)'}`, ...verdict.lines, ...(unread ? [unreadPassText(unread)] : []), - ` substitutions: ${rendered.substituted} ${STAMP_TOKEN}, ${rendered.quoted} quoted`, + ` substitutions: ${substitutionSummary(rendered)}`, ].join('\n'), ); } @@ -1582,6 +1976,7 @@ const SELF_TEST_BATTERIES = Object.freeze({ 'the token contract: the two spellings, and nothing else': 9, 'the refusals: every route that must not reach the board': 20, 'the opener scan: every `{{` is a token this tool renders, or the body is refused': 35, + 'the quoting spelling: Markdown code is a quotation, and a quotation is rendered as written': 51, 'the calendar rule: a stamp shaped like an instant the calendar does not have': 34, 'the direction check: a stamp no act can have read': 22, 'the substitution: one clock, read once, written everywhere': 9, @@ -1591,7 +1986,7 @@ const SELF_TEST_BATTERIES = Object.freeze({ 'the unread-knock check: a refresh cannot void what nobody read': 49, 'the shared rule: this tool and H56 cannot come to disagree': 6, }); -const SELF_TEST_BATTERY_FLOOR = 11; +const SELF_TEST_BATTERY_FLOOR = 12; const UNATTRIBUTED_BATTERY = '(unattributed)'; let selfTestReachedVerdict = false; @@ -1691,7 +2086,7 @@ export function selfTest() { t('⛔ …and the seconds grain still clears it', unrecognisedOpeners('read {{WAS:2026-09-08T14:00:30Z}}').length === 0); t('⛔ …and a bare stamp in prose is no opener\'s business', unrecognisedOpeners('The 2026-09-08T14:00Z ruling stands.').length === 0); t('⛔ the scan opens NO escape hatch: the entity spelling is not an opener, so it is prose', unrecognisedOpeners('the token {{NOW}}').length === 0); - t('⛔ …and a token inside backticks is STILL substituted — a fence is not an escape', renderBody('Write `{{NOW}}` there.', NOW_MS).body === 'Write `2026-09-10T06:37Z` there.'); + t('⭐ a token inside backticks is a QUOTATION and is left as written — the next battery owns this rule', renderBody('Write `{{NOW}}` there.', NOW_MS).body === 'Write `{{NOW}}` there.'); t('the span renderer escapes a newline', offendingSpan('a\nb') === 'a\\nb'); t('…a carriage return and a tab too', offendingSpan('a\r\tb') === 'a\\r\\tb'); @@ -1701,6 +2096,87 @@ export function selfTest() { t('⛔ …and never cuts a multi-byte character in half', offendingSpan(`${'x'.repeat(SPAN_BYTES - 1)}€€`) === `${'x'.repeat(SPAN_BYTES - 1)}…`); t('the budget is counted in BYTES, which is the unit a dump arrives in', SPAN_BYTES === 60 && Buffer.byteLength(offendingSpan('€'.repeat(40)), 'utf8') <= SPAN_BYTES + 3); + // The filed instance, in kind: a seat quoted this tool's OWN status line + // verbatim — an inline code span inside a blockquote — and the token inside + // the quotation was substituted, publishing a sentence the tool never + // printed. Three live hits in one hour, by two seats, plus a claim the tool + // refused for spelling its own documentation placeholder. + battery('the quoting spelling: Markdown code is a quotation, and a quotation is rendered as written'); + const CARD_QUOTE = + 'Addendum.\n\n' + + '> `post-stamped` reported it faithfully: 「*stamp: none substituted — this body carried no `{{NOW}}`, so there is no clock to check*」\n\n' + + 'read {{NOW}}.\n'; + const cardQuote = renderBody(CARD_QUOTE, NOW_MS); + t('⭐ THE FILED INSTANCE: the quoted token inside an inline span inside a blockquote is left as written', cardQuote.ok === true && cardQuote.body.includes('carried no `{{NOW}}`')); + t('⭐ …so the quotation is no longer falsified — the stamp does not appear inside it', cardQuote.body.includes('carried no `2026-09-10T06:37Z`') === false); + t('⭐ …and the act\'s OWN token, outside the quotation, still gets the clock', cardQuote.body.includes('read 2026-09-10T06:37Z.')); + t('⭐ …the counts now separate the two: 1 substituted, 1 verbatim, where the filed run could only say 2', cardQuote.substituted === 1 && cardQuote.verbatim === 1, `sub=${cardQuote.substituted} verb=${cardQuote.verbatim}`); + t('⭐ …and the status line SAYS both, so a reader can compare them to intent', substitutionSummary(cardQuote).includes('1 {{NOW}}') && substitutionSummary(cardQuote).includes('verbatim: 1 opener(s)')); + t('⛔ THE BEFORE-READING, kept as a control: the same body with the backticks removed IS substituted', renderBody(CARD_QUOTE.replace(/`\{\{NOW\}\}`/u, '{{NOW}}'), NOW_MS).substituted === 2); + + const FENCED = 'Re-check:\n\n```\nprintf \'{{NOW}}\' | node scripts/pm/post-stamped.mjs --dry-run\n```\n\nread {{NOW}}.\n'; + const fenced = renderBody(FENCED, NOW_MS); + t('⭐ THE UNESTABLISHED POINT, measured: a FENCED block is a quotation too', fenced.ok === true && fenced.body.includes("printf '{{NOW}}'")); + t('…with the token outside it still substituted, so fencing quotes one and not the other', fenced.substituted === 1 && fenced.verbatim === 1); + t('⛔ …and the before-reading it replaces: the tree substituted inside a fence exactly like prose', quotedSpans('```\n{{NOW}}\n```').length === 1); + t('a tilde fence is a fence', quotedSpans('~~~\n{{NOW}}\n~~~\n').length === 1); + t('an info string does not stop a fence opening', renderBody('```bash\necho {{NOW}}\n```\n\nread {{NOW}}\n', NOW_MS).verbatim === 1); + t('⛔ a backtick fence whose info string carries a backtick is NOT a fence — CommonMark\'s own rule', renderBody('```a`b\n{{NOW}}\n', NOW_MS).substituted === 1); + t('an UNCLOSED fence quotes to the end of the body, the way CommonMark ends one', renderBody('read {{NOW}}\n\n```\ntail {{NOW}}\n', NOW_MS).verbatim === 1); + t('⭐ a fence INSIDE a blockquote is tracked through the quote marker — the shape a seat quotes tool output in', renderBody('Tool said:\n\n> ```\n> substitutions: 2 {{NOW}}, 0 quoted\n> ```\n\nread {{NOW}}\n', NOW_MS).verbatim === 1); + t('…and the quote ending ends the block with it, so prose after it is prose again', renderBody('> ```\n> {{NOW}}\n\nread {{NOW}}\n', NOW_MS).substituted === 1); + t('⛔ a four-space indented block is NOT the quoting spelling — indentation is load-bearing in lists', renderBody('read {{NOW}}\n\n {{NOW}}\n', NOW_MS).substituted === 2); + + const ELLIPSIS = 'Claim: PM loop round 1.\n\nFile surface: the tool substitutes `{{NOW}}`, and `{{WAS:...}}` cannot be quoted either.\n\nread {{NOW}}.\n'; + const ellipsis = renderBody(ELLIPSIS, NOW_MS); + t('⭐ THE CLAIM\'S OWN REFUSAL, retired: an ellipsis payload inside backticks is quoted VERBATIM, not refused', ellipsis.ok === true && ellipsis.body.includes('`{{WAS:...}}`')); + t('…because the quoted-stamp validation reads a DECLARATION, and inside a quotation there is none', quotedStampValues('`{{WAS:...}}`').length === 0); + t('⛔ …the before-reading it replaces: the same payload in PROSE is still refused as not-a-stamp', kinds('the form is {{WAS:...}}', NOW_MS).join() === 'quoted-not-a-stamp'); + t('⭐ the documentation placeholder is quotable now — which is what this file\'s own refusal text spells', renderBody('The quoted route is `{{WAS:YYYY-MM-DDThh:mmZ}}`.\n\nread {{NOW}}\n', NOW_MS).ok === true); + t('⛔ …and in prose it is still refused, so the contract did not widen by one case', kinds('The quoted route is {{WAS:YYYY-MM-DDThh:mmZ}}.', NOW_MS).join() === 'quoted-not-a-stamp'); + const QUOTED_WAS_DIGITS = 'Note on the contract.\n\nExample: `{{WAS:2026-09-08T14:00Z}}` is the form.\n'; + t('a WAS token with REAL digits inside a quotation renders as the TOKEN, braces and all', renderBody(QUOTED_WAS_DIGITS, NOW_MS).body.includes('`{{WAS:2026-09-08T14:00Z}}`')); + t('…and is counted verbatim rather than as a quoted stamp — it declared nothing', renderBody(QUOTED_WAS_DIGITS, NOW_MS).quoted === 0 && renderBody(QUOTED_WAS_DIGITS, NOW_MS).verbatim === 1); + t('⛔ …and the same body in PROSE still renders the stamp from its declaration, unchanged', renderBody(QUOTED_WAS_DIGITS.replace(/`/gu, ''), NOW_MS).quoted === 1); + t('an unknown token NAME inside a quotation is text, not a refusal', renderBody('The typo `{{now}}` is refused.\n\nread {{NOW}}\n', NOW_MS).ok === true); + t('…and an UNCLOSED opener inside one is text too', renderBody('Quoting `{{WAS:2026` mid-edit.\n\nread {{NOW}}\n', NOW_MS).ok === true); + t('⛔ both are still refused in prose — the opener scan narrowed nowhere else', renderBody('The typo {{now}} is refused.', NOW_MS).ok === false && renderBody('Quoting {{WAS:2026 mid-edit.', NOW_MS).ok === false); + + // ⛔ The asymmetry this rule stands on: a quotation suppresses what RENDERS a + // token, never what judges a stamp a human typed. Quoting changes what is + // rendered, never what was authored — so nothing can hide a stamp behind + // backticks. + t('⭐ A BARE STAMP INSIDE THE QUOTING SPELLING IS REFUSED, exactly as in prose — the opening line', kinds('Claim: seat `2026-09-10T06:37Z` — dispatched.', NOW_MS).join() === 'positional'); + t('⭐ …and inside a FENCE beside the act-clock token, the MIXED refusal still fires', kinds('Verdict {{NOW}}.\n\n```\nread 2026-09-08T14:00Z\n```\n', NOW_MS).join() === 'mixed'); + t('⭐ …because `maskQuotedStamps` blanks a WAS token only where it IS a token', maskQuotedStamps('`{{WAS:2026-09-08T14:00Z}}`').includes('2026-09-08T14:00Z') && maskQuotedStamps('{{WAS:2026-09-08T14:00Z}}').includes('2026-09-08T14:00Z') === false); + t('⭐ …so a stamp cannot hide from the contract behind backticks, which is the accident this may never buy', kinds('Verdict {{NOW}} — write `{{WAS:2026-09-08T14:00Z}}` for a quoted reading.', NOW_MS).length > 0); + t('⛔ …and the body that refusal replaces used to be ACCEPTED and rendered as bare digits — the defect, not a feature', unrecognisedOpeners('Verdict {{NOW}} — write `{{WAS:2026-09-08T14:00Z}}` for a quoted reading.').length === 0); + const QUOTED_TOKEN_PLUS_STAMP = 'Here is the token: `{{NOW}}`.\n\nThe board was read at 2026-09-08T14:00Z.\n'; + t('⛔ the MIXED trigger is deliberately NOT quote-aware: spelling the token anywhere means the author knows it', kinds(QUOTED_TOKEN_PLUS_STAMP, NOW_MS).join() === 'mixed'); + t('…so this change weakened no refusal — it is the one direction it could have', stampRefusals(QUOTED_TOKEN_PLUS_STAMP, NOW_MS).length === 1); + const QUOTED_REMEDY = stampRefusals('Verdict {{NOW}}.\n\n```\nread 2026-09-08T14:00Z\n```\n', NOW_MS)[0].detail; + t('⭐ the remedy SAYS the stamp sits inside a quotation, rather than prescribing a route that cannot work there', QUOTED_REMEDY.includes('sits inside a QUOTED SPAN')); + t('…and names the placeholder as the way to quote an example', QUOTED_REMEDY.includes('YYYY-MM-DDThh:mmZ')); + t('⛔ …and a stamp with even ONE unquoted occurrence gets the ordinary remedy, with no such clause', stampRefusals('Verdict {{NOW}}.\n\nread 2026-09-08T14:00Z and `2026-09-08T14:00Z`.\n', NOW_MS)[0].detail.includes('QUOTED SPAN') === false); + + const UNMATCHED = 'Write `{{NOW}} there.'; + t('⛔ an unmatched backtick opens NO span — the run needs a closer of the same length on the line', renderBody(UNMATCHED, NOW_MS).body === 'Write `2026-09-10T06:37Z there.'); + t('a double-backtick run closes on a double-backtick run', quotedSpans('``{{NOW}}``').length === 1); + t('…and a single run inside a double one is content, not a closer', renderBody('``a `b` {{NOW}}`` read {{NOW}}', NOW_MS).substituted === 1); + t('⛔ a code span is searched within ONE line only — conservative on purpose, so under-detection is today\'s behaviour', renderBody('`{{NOW}}\n{{NOW}}`', NOW_MS).substituted === 2); + t('two spans on one line are two spans', quotedSpans('`a` and `b`').length === 2); + t('a backtick run inside a FENCE is fence content, never a span of its own', quotedSpans('```\n`a` `b`\n```').length === 1); + t('the span kinds are declared, so a reader and the code share one vocabulary', Object.keys(QUOTED_SPAN_KINDS).join() === 'fenced,code-span'); + t('an opener is judged by where it STARTS — one position, one answer for every rule', insideQuotedSpan([{ from: 0, to: 5 }], 4) === true && insideQuotedSpan([{ from: 0, to: 5 }], 5) === false); + + const PROSE_CONTROL = 'Verdict {{NOW}} — on the board read {{WAS:2026-09-08T14:00Z}}.'; + t('⭐ THE CONTROL: prose substitution is byte-identical — the whole point of a structural rule', renderBody(PROSE_CONTROL, NOW_MS).body === 'Verdict 2026-09-10T06:37Z — on the board read 2026-09-08T14:00Z.'); + t('…with no opener left verbatim, because no quotation is there', renderBody(PROSE_CONTROL, NOW_MS).verbatim === 0); + t('…and the summary says so in words rather than leaving a bare zero to read', substitutionSummary(renderBody(PROSE_CONTROL, NOW_MS)).endsWith('(none)')); + t('a body with no backtick at all has no span, so `quotedSpans` costs it nothing', quotedSpans('Claim: seat, {{NOW}} — dispatched.').length === 0); + t('⛔ NO THIRD SPELLING was added: the tokens are still exactly two', STAMP_TOKEN === '{{NOW}}' && QUOTED_TOKEN_RE.source === '\\{\\{WAS:([^{}]*)\\}\\}'); + t('⛔ and NO flag turns substitution off — the quoting spelling lives in the body, where a reader sees it', KNOWN_FLAGS.includes('--no-substitute') === false && KNOWN_OPTIONS.includes('expect-now') === false); + // The filed repro: the protocol's own digit shape, filled with an instant no // calendar has. `Date.parse` answers NaN, the span is null, and a null span // is "not future" — so before this rule the value was RENDERED, and