Skip to content

Commit 2f11e2d

Browse files
os-warrenclaude
andauthored
fix(lint): read a framed placeholder as the mention it is, in line as in a heading (#18484)
Fixes #17864 Clause-②: no — a repo-root gate script exports no published symbol and adds no key to a published payload. ## The defect, by symbol `findMigrationPrescription`'s branch 1 tells a placeholder **used** as the convention's label from one merely **mentioned** by asking whether an ordinary word directly governs it. A framing word (`Migration`, `迁移`, `rename`, `upgrade`) *frames* rather than governs, so `FRAMING_TAIL_RE` strips it to expose the word behind it. `labelPositioned`'s in-line arm never asked the question of that word. The strip removes the framing word and **leaves the space it sat behind**, and `GOVERNING_WORD_RE` is end-anchored on a letter or `的` — neither of which a trailing space can be. So the question was asked of a space, answered "nothing governs this", and the occurrence read as a LABEL. ⇒ inserting one framing word between a governor and the placeholder flipped a mention into a label. That is the #6967 false-positive direction, where the cost is a **hard-blocked PR** rather than a misreport: the catch-all is refused as contradicted, `registered` has no entry to name, and `@objectstack/spec` is published so `unpublished` is false — the author is left with no legal disposition at all. The heading arm has composed the two replacements correctly since #17357 (`.replace(FRAMING_TAIL_RE, '').replace(/\s+$/, '')`) and nothing held the two arms equal. ## The card's table, re-derived on the current tree ⛔ Not trusted from the card, which read at `d87a6936ef`. Re-run at `8fe5cb8e51` (`origin/main` at 2026-09-16T14:19:18Z), then again after the repair at `39b7dd7e71` (2026-09-16T15:17Z): | input | before | after | |:---|:---|:---| | **probe** — `the Migration FROM → TO is documented elsewhere` | `from-to-label` (the bug) | `null` | | **control A** — the same sentence, framing word removed | `null` | `null` | | **control B** — framing word, no governor: `Migration FROM → TO: delete the block` | `from-to-label` | `from-to-label` | One framing word is the only difference between the probe and control A, and the verdict flips: the probe is a reading, not an assertion. Control B shows the label arm still fires. ## The repair Both arms now go through one `withoutFramingTail` helper and differ only in the question they ask of the result — one definition rather than two that can drift, for the reason `HEADING_RE` is hoisted. The helper's docblock carries the mechanism, and what the strip deliberately does not reach. ## Judged in BOTH directions The fix direction is narrowing, so the dangerous direction is the other one: does anything that **should** read as a label now read as a mention? **Structurally, no.** The right-trim only reaches a prefix where whitespace precedes the framing word, so what the strip exposes is asked the ordinary in-line question. Every non-prose boundary survives, pinned as floors: a finished sentence (`it. Migration FROM → TO:`, F5), markup (`**Migration FROM → TO:**`, F6), a bullet (F7), a heading (the heading arm, unchanged, D2/D4/D6/D7/D9), and an identifier tail with no space to trim (F9). Every occurrence this moves is one where an ordinary word governs a framed placeholder — and each is still granted back by `carriesConcreteRewrite` the moment the body SHOWS a rewrite (F8, F-E2E-R): the narrowing removes no hit whose body ships the goods. **Adversarial cases beyond the card's three**, run at `39b7dd7e71`: | constructed input | verdict | |:---|:---| | `the sys_migration FROM → TO is documented elsewhere` | `from-to-label` — a **separate** false positive, see acceptance notes | | `sys_migration FROM → TO: delete the block` | `from-to-label` (unchanged) | | `the upgrade migration FROM → TO is documented elsewhere` | `null` | | `the Migration FROM → TO …` (two spaces) | `null` | | `**Migration** FROM → TO: delete the block` | `from-to-label` | | `## The Migration FROM → TO` | `from-to-label` (headings are labels by construction) | | `their rename FROM → TO guide` + a concrete rewrite in the body | `from-to-label` | | `their rename FROM → TO guide lives in the release notes` | `null` | | `the⟨tab⟩Migration FROM → TO …` | `null` | | `Migration. FROM → TO: delete the block` | `from-to-label` | ## The in-tree population that changes verdict: ZERO, with controls - **The gate's own verdict over the live stock is byte-identical**: `findMigrationPrescription` over all **343** changesets in `.changeset/`, before and after, diff empty — **24** non-null verdicts, same branch and same evidence line for each. - **The shape this narrowing reaches occurs 0 times** across **1160** tracked markdown files (`.changeset/`, `packages/**/CHANGELOG.md` — which is where changeset bodies ship verbatim — `content/docs/`, `docs/`, `skills/`). - **Lit control, same scanner, same corpora: 892 lines** carry the placeholder at all (22 · 851 · 2 · 13 · 4). **Seeded positive control:** the scanner matches the card's probe sentence and its CJK spelling. **Dark control:** a fabricated token matches 0. - So the answer to "are any of them currently correct?" is that there are none to be correct or incorrect; nothing in this tree moves. ## Reverse verification The repair is committed first; the ablation drops the right-trim from `withoutFramingTail`, proves the mutation reached disk (anchor count 1→0, injected count 0→1, blob `9c46cb6a91` → `87b8997af6`), and restores under a `trap` to `git checkout HEAD --` naming the file, proven by `git diff HEAD` empty and the on-disk blob hash equal to the HEAD blob again. Ablated: `--self-test` **exit 1, 4 failures** — `F1`, `F3`, `F-E2E-G` (this repair's red set) **and `D7`**, the existing #17357 heading case, which turns out to depend on exactly the same right-trim. Restored: exit 0, 384 assertions. Every floor stayed green under the mutation, which is what makes them floors rather than specimens. ## Gates At `39b7dd7e71`: **33 of the 34** families derived by `scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` exit 0, plus repo-wide `pnpm lint` (`eslint . --no-inline-config`) exit 0 — not narrowed, the whole tree. `node scripts/check-adr-0087-registration.mjs --base origin/main` exit 0; `--self-test` exit 0, 384 assertions. ⊘ **NOT MEASURED — `pnpm check:pm-dispatch-gates`**: it does not finish inside this container's foreground limit (killed at 300 s and again at 540 s). It was progressing rather than hung — 1551 then 1737 lines of its own passing output — and it grades `check-dispatch-gates`'s fixtures, which this diff does not touch. CI runs it. No changeset: this diff publishes nothing from any released package — the root manifest is `private` with no `files` list, and a repo-root gate script cannot sit inside any `packages/*` tarball. Labelled `skip-changeset`. ## Acceptance notes - **To file (a reproducible defect, same family, different trigger):** `FRAMING_TAIL_RE` is not word-anchored while its sibling `MIGRATION_FRAMING_RE` is, so it strips a framing word out of an **identifier**. Probe: `findMigrationPrescription('the sys_migration FROM → TO is documented elsewhere')` answers `from-to-label` at `39b7dd7e71`; the occurrence is governed by `the`, and the strip exposes `_` rather than the letter that was there. Out of scope here because word-anchoring changes WHICH prefixes are stripped at all — a wider claim than transparency, owing its own measurement over the changeset stock, exactly as this card warned. Stated in the helper's docblock rather than hidden. Dedupe words: `FRAMING_TAIL_RE`, word-anchor, identifier, `sys_migration`, `labelPositioned`. - **Noted, not filed:** `SELF_TEST_BATTERY_FLOOR` is 50 while the roster now declares 55 batteries, so five entries could be deleted before the roster-size floor reds — the floor is documented as a floor, and tightening it is a shared-ratchet edit this card did not ask for. Successor: the next author adding a battery to this file. --- _Generated by [Claude Code](https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8fa6b97 commit 2f11e2d

1 file changed

Lines changed: 155 additions & 2 deletions

File tree

scripts/check-adr-0087-registration.mjs

Lines changed: 155 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
403403
'R8: an empty justification is refused': 2,
404404
'G5: the catch-all, on a changeset carrying no prescription': 1,
405405
'D-E2E (#17357): the denial heading, END TO END through `scan()`': 6,
406+
'F-E2E (#17864): the framed MENTION, END TO END through `scan()`': 2,
406407
'R9: two markers is ambiguous, not "the first one wins"': 2,
407408
'R10: THE #6419 SHAPE -- a REAL prescription, written in Chinese with -': 4,
408409
'R11: the same, framed by a HEADING instead of an inline label': 3,
@@ -432,6 +433,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
432433
'the floors: what the new vocabulary must refuse': 9,
433434
'the floors: labels that are NOT mentions, and mentions that ARE evidenced': 9,
434435
'D1-D9 (#17357): a heading that DENIES a prescription is not evidence of one': 9,
436+
'F1-F9 (#17864): a framing word between the governor and the placeholder is': 9,
435437
'P51-P60: the HARD-WRAPPED mention, and the floors that keep the cure from': 11,
436438
'P62-P68: the framed region closes at the same or a SHALLOWER heading, not': 7,
437439
'U1-U12 (#8299): unit pins on the runtime-interface-only primitives': 13,
@@ -1217,10 +1219,59 @@ const GOVERNING_WORD_RE = /(?:[A-Za-z]|的)$/;
12171219
* A migration-framing word sitting immediately before the placeholder FRAMES it; it
12181220
* does not govern it (`Migration FROM → TO`, `迁移 FROM → TO`). Anchored at the end
12191221
* because that is the only position where the distinction can arise.
1222+
*
1223+
* ⛔ Never apply this pattern directly — go through `withoutFramingTail` below. On
1224+
* its own it removes the word and LEAVES THE SPACE the word sat behind, which is a
1225+
* prefix no end-anchored predicate can read.
12201226
*/
12211227
const FRAMING_TAIL_RE =
12221228
/(?:||||migrat(?:e|es|ed|ing|ion|ions)|rename[sd]?|rewrit(?:e|es|ten|ing)|upgrade[sd]?)$/i;
12231229

1230+
/**
1231+
* The prefix with its framing word taken off -- and the SPACE that word sat behind
1232+
* taken off with it, so that what is EXPOSED is the word the framing word was
1233+
* standing in front of.
1234+
*
1235+
* Both readers of `FRAMING_TAIL_RE` ask an END-ANCHORED question of the result
1236+
* (`GOVERNING_WORD_RE`, `HEADING_DENIAL_RE`), and stripping alone leaves a trailing
1237+
* space, which is neither a letter nor `的` nor a negator. The strip therefore does
1238+
* not expose the word behind the framing word; it exposes a space, every
1239+
* end-anchored question answers "no", and the occurrence is read as a LABEL. That
1240+
* is the false-positive direction (#17864 · #6967): inserting a framing word between
1241+
* a governor and the placeholder flipped `the Migration FROM → TO is documented
1242+
* elsewhere` -- a plain MENTION -- into evidence of a prescription, and a false
1243+
* positive on this gate hard-blocks a PR rather than merely misreporting.
1244+
*
1245+
* One definition rather than two that can drift, for the reason `HEADING_RE` is
1246+
* hoisted: the heading arm composed the two replacements correctly and the in-line
1247+
* arm did not, and nothing held them equal. The two arms now differ only in the
1248+
* question they ask of the result.
1249+
*
1250+
* ⚠️ ONE framing word is stripped, deliberately, and the composition is not
1251+
* iterated: `the upgrade migration FROM → TO` exposes `upgrade`, a letter, so it
1252+
* reads as governed -- the same answer the mention reading wants here, by a route
1253+
* that is an accident. No stock occurrence stacks two framing words; a repeated
1254+
* strip is a wider claim and would need its own measurement.
1255+
*
1256+
* ⚠️ The pattern is not word-anchored (`MIGRATION_FRAMING_RE` is; this one is not),
1257+
* so it strips a framing word out of an IDENTIFIER: `sys_migration FROM → TO`
1258+
* strips to `sys_`. That reading is UNCHANGED by the right-trim and cannot be
1259+
* changed by it -- with no whitespace before the framing word there is no
1260+
* whitespace left behind to trim -- so the repair keys on the SPACE and reaches
1261+
* exactly the shape where the framing word is a word of its own. The identifier
1262+
* shape is a SEPARATE false positive, stated rather than hidden and out of this
1263+
* repair's reach: `the sys_migration FROM → TO is documented elsewhere` is governed
1264+
* by `the` and still reads as a label, because what the strip exposes is `_` rather
1265+
* than the letter that was there. Word-anchoring this pattern changes WHICH
1266+
* prefixes are stripped at all, which is a wider claim than transparency and needs
1267+
* its own measurement over the changeset stock before it is believed.
1268+
*
1269+
* @param {string} prefix the text left of the placeholder, already right-trimmed
1270+
*/
1271+
function withoutFramingTail(prefix) {
1272+
return prefix.replace(FRAMING_TAIL_RE, '').replace(/\s+$/, '');
1273+
}
1274+
12241275
/**
12251276
* Does a NEGATOR directly govern the placeholder in this HEADING -- is the heading
12261277
* DENYING that a prescription exists rather than opening one? (#17357)
@@ -1368,6 +1419,19 @@ const VERTICAL_TO_RE = /^\s{0,3}(?:(?:\/\/|#|-|\*|>)\s*)*\**TO\**\s*(?::|—|-|$
13681419
* the closed class, the adjacency rule and why a denying heading is demoted to a
13691420
* mention rather than exempted.
13701421
*
1422+
* ⚠️ A FRAMING word between the governor and the placeholder is transparent, and
1423+
* transparency is a claim in BOTH directions (#17864). It does not make an
1424+
* occurrence a label -- `the Migration FROM → TO is documented elsewhere` is the
1425+
* same mention as `the FROM → TO is documented elsewhere`, one framing word apart --
1426+
* and it does not take a label away, because what the strip exposes is asked the
1427+
* ordinary question: `Migration FROM → TO:` exposes nothing at all and stays a
1428+
* label, `it. Migration FROM → TO:` exposes the sentence boundary that already made
1429+
* it one (P43), and `**Migration FROM → TO:**` exposes the markup. Every occurrence
1430+
* this moves is one where an ordinary word governs a framed placeholder, and each of
1431+
* those is still granted back by `carriesConcreteRewrite` the moment the body SHOWS
1432+
* a rewrite (P47). `withoutFramingTail` above carries the mechanism and what the
1433+
* strip deliberately does not reach.
1434+
*
13711435
* ⚠️ Prose in this repo is HARD-WRAPPED at ~80 columns, so "starts its line" is NOT
13721436
* the test and never could be -- `carry their\nFROM → TO migration` puts a mention
13731437
* at column 0 with nothing at all to its left. #7078 left that as a stated blind
@@ -1418,9 +1482,15 @@ function labelPositioned(line, col, prev) {
14181482
// takes the `carriesConcreteRewrite` path below, so a body that shows the goods
14191483
// anywhere is refused exactly as it was.
14201484
if (/^\s{0,3}#{1,6}\s/.test(line)) {
1421-
return !HEADING_DENIAL_RE.test(prefix.replace(FRAMING_TAIL_RE, '').replace(/\s+$/, ''));
1485+
return !HEADING_DENIAL_RE.test(withoutFramingTail(prefix));
14221486
}
1423-
if (prefix !== '') return !GOVERNING_WORD_RE.test(prefix.replace(FRAMING_TAIL_RE, ''));
1487+
// A framing word is transparent HERE exactly as it is in the heading arm above
1488+
// (#17864): it frames the placeholder, so the governance question is asked of
1489+
// whatever stands behind it -- which requires the space it sat behind to come off
1490+
// with it (`withoutFramingTail`). Asked of the bare strip, the question met a
1491+
// trailing space, answered "nothing governs this", and read every governed
1492+
// MENTION carrying a framing word as a label.
1493+
if (prefix !== '') return !GOVERNING_WORD_RE.test(withoutFramingTail(prefix));
14241494
// The placeholder OPENS its line -- bare or merely indented, so a wrapped list
14251495
// item counts. There is no character to its left, so the governing word, if there
14261496
// is one, is the last word of the line above; and only a line that is prose the
@@ -4290,6 +4360,34 @@ function selfTest() {
42904360
},
42914361
})), [/no `adr-0087:` disposition marker/]);
42924362

4363+
// ---- F-E2E (#17864): the framed MENTION, END TO END through `scan()` -------
4364+
//
4365+
// What the unit pins above read as a verdict, an author reads as a refusal they
4366+
// cannot answer: the catch-all is contradicted by "evidence" that is a sentence
4367+
// saying where the prescriptions live, `registered` has no entry to name, and
4368+
// `@objectstack/spec` is published so `unpublished` is false. Both directions run
4369+
// here, because the narrowing is only worth having if the second one holds.
4370+
battery('F-E2E (#17864): the framed MENTION, END TO END through `scan()`');
4371+
const FRAMED_MENTION = 'The retirement is announced in the release notes; the Migration FROM → TO is documented elsewhere.';
4372+
const FRAMED_WHY = 'a bare deletion on a non-strict schema refuses nothing and converts nothing';
4373+
green('F-E2E-G the framed mention is admitted -- a sentence ABOUT prescriptions is not one', run(mk({
4374+
files: {
4375+
'.changeset/x.md': CS({
4376+
body: '**BREAKING** the `x` key is deleted outright.\n\n' + FRAMED_MENTION + '\n\n'
4377+
+ '<!-- adr-0087: not-required (no-migration-prescription) ' + FRAMED_WHY + ' -->\n',
4378+
}),
4379+
},
4380+
})));
4381+
red('F-E2E-R the same sentence over a body that SHIPS the prescription still refuses', run(mk({
4382+
files: {
4383+
'.changeset/x.md': CS({
4384+
body: '**BREAKING** the `x` key is deleted outright.\n\n' + FRAMED_MENTION + '\n\n'
4385+
+ '- `App.x` → `App.y`\n\n'
4386+
+ '<!-- adr-0087: not-required (no-migration-prescription) ' + FRAMED_WHY + ' -->\n',
4387+
}),
4388+
},
4389+
})), [/contradicts the changeset's own body/, /Evidence \(from-to-label\)/]);
4390+
42934391
// ---- R9: two markers is ambiguous, not "the first one wins" ---------------
42944392
battery('R9: two markers is ambiguous, not "the first one wins"');
42954393
red('R9 two disposition markers', run(mk({
@@ -5943,6 +6041,61 @@ function selfTest() {
59436041
'D9: POSITIVE CONTROL -- the closed class is WORD-anchored, so `Nonstandard` is not `no` and this heading keeps its label reading',
59446042
);
59456043

6044+
// --- F1-F9 (#17864): a framing word between the governor and the placeholder is
6045+
// --- transparent, IN LINE as well as in a heading.
6046+
//
6047+
// `FRAMING_TAIL_RE` strips the framing word so the governance question reaches the
6048+
// word BEHIND it. In-line, nothing re-trimmed the space that word sat behind, and
6049+
// `GOVERNING_WORD_RE` is end-anchored -- so the question was asked of a trailing
6050+
// space, answered "nothing governs this", and every governed MENTION carrying a
6051+
// framing word read as a LABEL. Inserting one word flipped the verdict, in the
6052+
// #6967 false-positive direction, where the cost is a hard-blocked PR.
6053+
//
6054+
// F1/F3 are the RED set under reverse verification (drop the right-trim in
6055+
// `withoutFramingTail` and both go red). F2 is the positive control the specimen
6056+
// assertions are worthless without -- if it reds with F1 green the label arm has
6057+
// stopped seeing rather than started discriminating. F4 is the control that makes
6058+
// F1 a READING: one framing word is the only difference between them. F5-F9 are
6059+
// the floors in the dangerous direction -- a narrowing that turned a loud wrong
6060+
// answer into a quiet one would show up here, not in F1.
6061+
battery('F1-F9 (#17864): a framing word between the governor and the placeholder is');
6062+
assert(
6063+
!hasMigrationPrescription('the Migration FROM → TO is documented elsewhere\n'),
6064+
'F1: THE #17864 SHAPE -- `the Migration FROM → TO` is the mention `the FROM → TO` is, one framing word apart',
6065+
);
6066+
assert(
6067+
findMigrationPrescription('Migration FROM → TO: delete the block\n')?.branch === 'from-to-label',
6068+
'F2: POSITIVE CONTROL -- a framing word with NO governor in front of it opens a label, with no concrete rewrite in the body to fall back on',
6069+
);
6070+
assert(
6071+
!hasMigrationPrescription('唯一的 迁移 FROM → TO 落在部署方自己的代理配置上。\n'),
6072+
'F3: the Chinese spelling -- `的` governs across the framing word `迁移` exactly as `the` does across `Migration` (P40 framed)',
6073+
);
6074+
assert(
6075+
!hasMigrationPrescription('the FROM → TO is documented elsewhere\n'),
6076+
'F4: THE CONTROL F1 IS A READING AGAINST -- the same sentence without the framing word, which was already a mention',
6077+
);
6078+
assert(
6079+
findMigrationPrescription('The RLS compiler never read it. Migration FROM → TO: a set a policy needs is now supplied\n')?.branch === 'from-to-label',
6080+
'F5: FLOOR -- the strip exposes a sentence boundary, not a word, so a label following a finished sentence stays a label (P43 framed)',
6081+
);
6082+
assert(
6083+
findMigrationPrescription('**Migration FROM → TO:** delete the block\n')?.branch === 'from-to-label',
6084+
'F6: FLOOR -- it exposes MARKUP, which governs nothing',
6085+
);
6086+
assert(
6087+
findMigrationPrescription('- Migration FROM → TO: delete the block\n')?.branch === 'from-to-label',
6088+
'F7: FLOOR -- and a bullet marker, which is structure rather than prose',
6089+
);
6090+
assert(
6091+
findMigrationPrescription('the Migration FROM → TO mappings include:\n\n- `objectPermissions` → `objectPermission`\n')?.branch === 'from-to-label',
6092+
'F8: FLOOR -- a governed framed placeholder is still taken at face value once the body SHOWS a concrete rewrite (P47 framed); the narrowing removes no hit whose body ships the goods',
6093+
);
6094+
assert(
6095+
findMigrationPrescription('sys_migration FROM → TO: delete the block\n')?.branch === 'from-to-label',
6096+
'F9: FLOOR -- the repair keys on the SPACE, so an identifier ending in a framing word strips to `sys_` and reads exactly as it did',
6097+
);
6098+
59466099
// --- P51-P60: the HARD-WRAPPED mention, and the floors that keep the cure from
59476100
// --- being worse than the disease (#7094).
59486101
//

0 commit comments

Comments
 (0)