Skip to content

Commit 820cfab

Browse files
committed
fix(docs-audit): retire the lead-anchor widening family for a key-position allowlist
`declLead` anchored the ledger `route:`/`client:` key with a word boundary, which fails only against a preceding WORD character. Four cards each shrank one more character class out of the false-positive set and named the next residue, which is the wrong discipline for a bounded defect: the anchor admitted 12225 of the code points in 0..0x2FFF and the family was arguing about a handful at the edge of that set. The anchor is inverted. It no longer enumerates what may not precede the key; it names the positions where an object-literal property key may begin — start of input, whitespace, `{`, `,` — and rejects everything else. That closes `$`, `.`, `-` and Unicode identifier characters in one move, needs no `u` flag, and keeps the anchor spelled once in `declLead` with the `\s*:\s*` run untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
1 parent e170b0a commit 820cfab

1 file changed

Lines changed: 156 additions & 31 deletions

File tree

‎scripts/docs-audit/affected-docs.mjs‎

Lines changed: 156 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1391,24 +1391,65 @@ function typeDeclRegions(code) {
13911391
* explicitly for their own population moves, and it is made explicitly here for the same
13921392
* reason: the alternative is moving a reported number quietly.
13931393
*
1394-
* ⛔ THE RESIDUE THIS DOES NOT CLOSE, named rather than left to be discovered. `\b` fails only
1395-
* against a preceding WORD character, so `$route:` — a legal JS identifier — is still read as
1396-
* a declaration. It is no longer a DIVERGENCE (all eight now agree on it), which is what this
1397-
* card was about, but it is still a phantom, and tightening to `(?<![\w$])` would move
1398-
* `declarationsIn` as well — the one scan this card's before/after was priced to leave
1399-
* byte-identical — so it is a SECOND population move with its own before/after to price.
1400-
* Filed as #11630 rather than folded in, for the reason this card was filed rather than
1401-
* folded into #11494, and pinned in `--self-test` so that card MOVES a pin rather than
1402-
* finding none.
1403-
*
1404-
* @param {string} keys the key alternation ONLY — `(route|client)`, `route`,
1394+
* ⛔ THE FAMILY THIS RETIRES (#11717). `\b` fails only against a preceding WORD character, so
1395+
* every character that is NOT one still admitted a phantom: `$route:` (a legal JS identifier),
1396+
* `cond ? defaults.route : 'X'` (a member access whose colon belongs to a TERNARY), `-route:`,
1397+
* and — because `\w` is ASCII-only — any Unicode identifier character, so `éroute:` too. Four
1398+
* cards (#11494, #11542, #11630, #11711) each shrank ONE more character class out of that set
1399+
* and named the next residue as a pin for the next card to flip. That is a widening CHAIN, and
1400+
* it is the wrong discipline for a BOUNDED defect: over code points 0..0x2FFF the `\b` anchor
1401+
* admits 12225 characters, and the whole family was arguing about a handful at the edge of it.
1402+
*
1403+
* SO THE ANCHOR IS INVERTED, and that is the point of this card rather than a fifth shrink. It
1404+
* no longer enumerates what may not PRECEDE the key; it names the positions where an
1405+
* object-literal property key may BEGIN — start of input, whitespace, `{`, `,` — and rejects
1406+
* everything else. Spelled as a negative lookbehind over the COMPLEMENT of that allowlist,
1407+
* `(?<![^\s{,])`, which also gives start-of-input for free rather than as a second alternative.
1408+
* ⚠️ This DEPARTS from the three blocklist idioms below (`symbolRe` `(?<![\w$.])`, `dottedRe`
1409+
* `(?<![\w$])`, `rulePatternFor` `(?<![\w$.-])`) deliberately: each of those names a class it
1410+
* excludes and each therefore has a residue, which is exactly the chain this retires. The
1411+
* allowlist is bounded at three characters where the blocklist was not bounded at all, and it
1412+
* closes `$`, `.`, `-`, Unicode and every future escapee in ONE move instead of one per card.
1413+
* ⛔ It is also why NO `u` flag is needed — closing the Unicode residue as a blocklist meant a
1414+
* `\p{L}` class under `u`, which would change escape semantics for every source these leads
1415+
* are composed with at the eight call sites. An allowlist of ASCII positions needs none.
1416+
*
1417+
* ⛔ IT ADMITS STRICTLY LESS, NEVER MORE — swept, not argued. Over 0..0x2FFF the `\b` anchor
1418+
* admits 12225 and this one admits 25, and the count admitted by this one but NOT by `\b` is
1419+
* ZERO. All three numbers are pinned in `--self-test`, so a future widening that admits
1420+
* something `\b` did not fails there rather than reading as a tightening.
1421+
*
1422+
* ⛔ THIS MOVED BOTH MEASURED POPULATIONS — the ledger rows AND `declarationsIn`, the scan
1423+
* #11542's before/after was priced to leave byte-identical — which the header of
1424+
* `--bridge-coverage` attaches a before/after standard to. Priced on the tree where the move is
1425+
* provably free: across the seven live ledgers all 499 `route:`/`client:` lead occurrences are
1426+
* preceded by a SPACE — 0 by `$`, 0 by `.`, 0 by `-`, 0 by any character outside the allowlist
1427+
* at all — so all 499 survive and no row enters or leaves either population.
1428+
*
1429+
* WHY THERE IS NO RIGHT-HAND ANCHOR, since the idioms below all carry one: the `\s*:` that
1430+
* follows IS the right anchor, exactly. `routes:` cannot match — after `route` the `\s*` takes
1431+
* nothing and the `:` meets `s` — while `route :` legitimately does. A trailing class would be
1432+
* a second spelling of a constraint the colon already makes exact.
1433+
*
1434+
* ⛔ THE RESIDUE THIS DOES NOT CLOSE, named rather than left to be discovered — and it is the
1435+
* end of what ANY left-anchor can reach. A key in EXPRESSION position preceded by whitespace,
1436+
* `cond ? route : 'GET /api/v1/x'`, is byte-for-byte indistinguishable from a property key by
1437+
* anything that only looks left: the allowlist admits it because a space is exactly where a key
1438+
* may begin. #11630 named this same class as the one "no lookbehind can reach" and it is why
1439+
* `-` was left admitted there; the allowlist closes every spelling of it that wears a character
1440+
* (`-route`, `.route`, `$route`) and leaves the plainest one. Closing THAT needs the colon's
1441+
* enclosing expression, not its left neighbour — a parser question, not an anchor question, and
1442+
* a different card if a puller ever appears. Pinned in `--self-test` as deliberately unmoved.
1443+
*
1444+
* @param {string} keys the key alternation ONLY — `(route|client)`, `route`, `client`,
14051445
* `(?:route|client)`. The capture groups are the call site's question; the ANCHOR, the
14061446
* colon and the run between the colon and the value are this function's. ⛔ A call site must
1407-
* NOT restate the `\b`: a second copy is a second spelling, which is the entire defect this
1408-
* closes, and `--self-test` pins that no argument carries one.
1447+
* NOT restate the anchor in ANY spelling — neither the old `\b` nor a lookbehind of its own:
1448+
* a second copy is a second spelling, which is the entire defect this closes, and
1449+
* `--self-test` pins that no argument carries either.
14091450
*/
14101451
function declLead(keys) {
1411-
return String.raw`\b${keys}\s*:\s*`;
1452+
return String.raw`(?<![^\s{,])${keys}\s*:\s*`;
14121453
}
14131454

14141455
/**
@@ -1439,8 +1480,9 @@ function declarationsIn(text) {
14391480
const code = codeOnly(text);
14401481
const skip = typeDeclRegions(code);
14411482
const out = [];
1442-
// The `\b` this used to carry is now `declLead`'s (#11542) — it was the LAST spelling the
1443-
// eight scans decided for themselves, and this was the one scan that decided it correctly.
1483+
// The `\b` this used to carry is now `declLead`'s (#11542), and #11717 replaced it there
1484+
// with the key-POSITION allowlist — it was the LAST spelling the eight scans decided for
1485+
// themselves, and this was the one scan that decided it correctly.
14441486
const re = new RegExp(declLead('(route|client)'), 'g');
14451487
let m;
14461488
while ((m = re.exec(code)) !== null) {
@@ -2913,26 +2955,84 @@ function selfTest() {
29132955
'declared', '1 row / 1 route / 0 declined',
29142956
`${keyUnreadable.rows.length} row / ${keyUnreadable.routesDeclared} route / ${keyUnreadable.declined.length} declined`);
29152957

2916-
// ⛔ THE BOUNDARY THIS CARD DOES NOT CROSS. `\b` fails only against a preceding WORD
2917-
// character, so `$route:` — a legal JS identifier — is still read as a declaration. It is no
2918-
// longer a DIVERGENCE, which is what this card was about: all eight scans agree on it now,
2919-
// and they agree by reading the spelling `declarationsIn` already had. It is still a phantom
2920-
// row, and closing it means tightening to `(?<![\w$])`, which moves `declarationsIn` as well
2921-
// — a SECOND population move, with its own before/after to price against the header of
2922-
// `--bridge-coverage`. Filed as #11630 rather than folded in, for the reason this card was
2923-
// filed rather than folded into #11494, and pinned so that card MOVES this pin rather than
2924-
// finding none.
2958+
// ⛔ THE FAMILY IS RETIRED HERE (#11717), and these are the pins it FLIPS. `\b` failed only
2959+
// against a preceding WORD character, so every non-word character was a phantom waiting to
2960+
// be filed one card at a time — #11630 had `$` and `.` queued, #11711 had Unicode. The
2961+
// allowlist admits only where an object-literal key may BEGIN, so all four classes close at
2962+
// once and each gets its own fixture: a class closed without a fixture is a class that
2963+
// re-opens silently.
29252964
const dollarKey = parseLedgerSource([
29262965
'export const L = [',
29272966
" { $route: 'GET /api/v1/gone', family: 'metadata', disposition: 'sdk' },",
29282967
" { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' },",
29292968
'];',
29302969
].join('\n'));
2931-
check('parseLedgerSource', 'a `$route:` still mints a phantom row — #11630, deliberately unmoved',
2932-
'row count', 2, dollarKey.rows.length);
2970+
check('parseLedgerSource', 'a `$route:` mints NO row — `$` is not a place a KEY may begin (#11717)',
2971+
'row count', 1, dollarKey.rows.length);
2972+
check('parseLedgerSource', 'and the row that survives is the REAL one, carrying its binding',
2973+
'row', 'GET /api/v1/meta → meta.getTypes',
2974+
`${dollarKey.rows[0]?.route} → ${dollarKey.rows[0]?.client}`);
2975+
check('parseLedgerSource', 'and the DENOMINATOR drops it too, so no phantom gap opens', 'declared',
2976+
'1 route / 1 client / 0 declined',
2977+
`${dollarKey.routesDeclared} route / ${dollarKey.clientsDeclared} client / ${dollarKey.declined.length} declined`);
2978+
check('bridgeCoverageFrom', 'and the retirement carries no verdict on an accurate ledger', 'brokenScan',
2979+
0, bridgeCoverageFrom([{ file: 'p-route-ledger.ts', ...dollarKey }], ['/api/v1/meta']).brokenScan.length);
2980+
2981+
// …a MEMBER ACCESS, where the colon belongs to a TERNARY and never to a key (#11630's `.`).
2982+
const dottedKey = parseLedgerSource([
2983+
'export const L = [',
2984+
" { path: cond ? defaults.route : 'GET /api/v1/gone', family: 'metadata' },",
2985+
" { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' },",
2986+
'];',
2987+
].join('\n'));
2988+
check('parseLedgerSource', 'a member-access `.route :` in a TERNARY mints NO row (#11717)',
2989+
'row count', 1, dottedKey.rows.length);
2990+
check('parseLedgerSource', 'and it leaves the denominator alone as well', 'declared',
2991+
'1 route / 1 client / 0 declined',
2992+
`${dottedKey.routesDeclared} route / ${dottedKey.clientsDeclared} client / ${dottedKey.declined.length} declined`);
2993+
2994+
// …a `-`, which #11630 left ADMITTED deliberately because `a-route` really is the whole
2995+
// token `route`. The allowlist moves it for a different and better reason: whole token or
2996+
// not, a `-` before it means EXPRESSION position, where the colon is a ternary's.
2997+
const dashKey = parseLedgerSource([
2998+
'export const L = [',
2999+
" { a-route: 'GET /api/v1/gone', family: 'metadata' },",
3000+
" { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' },",
3001+
'];',
3002+
].join('\n'));
3003+
check('parseLedgerSource', 'a `-`-prefixed lead mints NO row — the pin #11630 left is FLIPPED (#11717)',
3004+
'row count', 1, dashKey.rows.length);
3005+
3006+
// …and a UNICODE identifier character, which `\w` could never reach because `\w` is
3007+
// ASCII-only. This is #11711's whole card, subsumed — and closed WITHOUT the `u` flag a
3008+
// `\p{L}` blocklist would have forced on all eight call sites.
3009+
const unicodeKey = parseLedgerSource([
3010+
'export const L = [',
3011+
" { \u00e9route: 'GET /api/v1/gone', family: 'metadata' },",
3012+
" { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' },",
3013+
'];',
3014+
].join('\n'));
3015+
check('parseLedgerSource', 'a UNICODE-prefixed lead mints NO row — #11711 subsumed, no `u` flag (#11717)',
3016+
'row count', 1, unicodeKey.rows.length);
3017+
3018+
// ⛔ THE RESIDUE THIS LEAVES, and it is the end of what any LEFT-ANCHOR can reach. A key in
3019+
// expression position preceded by WHITESPACE is byte-for-byte what a property key looks
3020+
// like, so the allowlist admits it — correctly, by its own rule. #11630 named this same
3021+
// class as the one "no lookbehind can reach", which is why it left `-` admitted; the
3022+
// allowlist closes every spelling of it that wears a character and leaves the plainest one.
3023+
// Closing THAT is a parser question, not an anchor question. Pinned as deliberately unmoved
3024+
// so a card that ever takes it MOVES a pin rather than finding none.
3025+
const bareExprKey = parseLedgerSource([
3026+
'export const L = [',
3027+
" { path: cond ? route : 'GET /api/v1/gone', family: 'metadata' },",
3028+
" { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' },",
3029+
'];',
3030+
].join('\n'));
3031+
check('parseLedgerSource', 'a bare `? route :` in EXPRESSION position still mints a phantom — deliberately unmoved',
3032+
'row count', 2, bareExprKey.rows.length);
29333033
check('parseLedgerSource', 'and it is still SILENT — all eight scans agree on it, so both terms move together',
29343034
'declared', '2 route / 0 declined',
2935-
`${dollarKey.routesDeclared} route / ${dollarKey.declined.length} declined`);
3035+
`${bareExprKey.routesDeclared} route / ${bareExprKey.declined.length} declined`);
29363036

29373037
// ⛔ REPORTED, NEVER A VERDICT. A comment explaining a retired row by quoting its old path
29383038
// is legitimate prose; reddening CI over it is the false red the #9747 family declines.
@@ -3568,7 +3668,7 @@ function selfTest() {
35683668
// copy is the same hole re-opening, and only a source pin can see it: every behavioural
35693669
// fixture above would keep passing while the new scan drifted on its own.
35703670
check('declLead', 'the run between a `route:`/`client:` colon and its value is spelled ONCE', 'affected-docs.mjs',
3571-
1, (ownSource.match(/String\.raw`\\b\$\{keys\}\\s\*:\\s\*`/g) || []).length);
3671+
1, (ownSource.match(/String\.raw`\(\?<!\[\^\\s\{,\]\)\$\{keys\}\\s\*:\\s\*`/g) || []).length);
35723672
check('declLead', 'and all eight lead scans are built from it, none inline', 'affected-docs.mjs',
35733673
8, (ownSource.match(/new RegExp\(declLead\(/g) || []).length);
35743674
// …AND THE KEY ANCHOR IS SPELLED ONCE TOO (#11542), which is the same pin one field over.
@@ -3579,14 +3679,39 @@ function selfTest() {
35793679
// side, and only a source pin can see it: every behavioural fixture above stays green while
35803680
// the argument drifts.
35813681
check('declLead', 'and the KEY anchor is spelled once too — no call site restates it', 'affected-docs.mjs',
3582-
0, (ownSource.match(/declLead\([^\n]*?\\b/g) || []).length);
3682+
0, (ownSource.match(/declLead\([^\n]*?(?:\\b|\(\?<)/g) || []).length);
35833683
// BEHAVIOURAL, not merely textual: the three key spellings the eight call sites pass all
35843684
// come back anchored, from the one place that spells the anchor. This is what "all eight
35853685
// read the same anchored spelling" means when checked rather than asserted.
35863686
check('declLead', 'every key spelling a call site passes comes back ANCHORED', 'declLead',
3587-
String.raw`\b(route|client)\s*:\s* | \broute\s*:\s* | \b(?:route|client)\s*:\s*`,
3687+
String.raw`(?<![^\s{,])(route|client)\s*:\s* | (?<![^\s{,])route\s*:\s* | (?<![^\s{,])(?:route|client)\s*:\s*`,
35883688
['(route|client)', 'route', '(?:route|client)'].map((k) => declLead(k)).join(' | '));
35893689

3690+
// ⛔ AND IT ONLY EVER REMOVES (#11717) — swept over code points, not argued. The allowlist
3691+
// is a strict SUBSET of the `\b` it replaced, which is what makes "retire the family" a
3692+
// tightening rather than a re-shuffle. A future card that widens the allowlist — adding `(`
3693+
// to admit a call-position key, say — fails HERE the moment it admits one character `\b`
3694+
// rejected, instead of reading as one more honest shrink.
3695+
const sweepOld = String.raw`\b(route|client)\s*:\s*`;
3696+
const sweepNew = declLead('(route|client)');
3697+
const admitsAfter = (src, ch) => {
3698+
for (const m of (ch + "route: 'x'").matchAll(new RegExp(src, 'g'))) if (m.index === ch.length) return true;
3699+
return false;
3700+
};
3701+
let sweptOld = 0, sweptNew = 0, sweptNewNotOld = 0;
3702+
for (let cp = 0; cp <= 0x2fff; cp++) {
3703+
const ch = String.fromCodePoint(cp);
3704+
const o = admitsAfter(sweepOld, ch);
3705+
const n = admitsAfter(sweepNew, ch);
3706+
if (o) sweptOld++;
3707+
if (n) sweptNew++;
3708+
if (n && !o) sweptNewNotOld++;
3709+
}
3710+
check('declLead', 'the anchor only ever REMOVES — it admits nothing the `\\b` it replaced rejected',
3711+
'sweep 0..0x2FFF', 0, sweptNewNotOld);
3712+
check('declLead', 'and the allowlist is the far SMALLER set, by the margin the sweep measures',
3713+
'sweep 0..0x2FFF', '12225 → 25', `${sweptOld} → ${sweptNew}`);
3714+
35903715

35913716
if (failed) {
35923717
console.error(`\n✗ affected-docs self-test failed (${failed} case(s)).`);

0 commit comments

Comments
 (0)