Skip to content

Commit e30a7e5

Browse files
claude[bot]claude
andauthored
fix(docs-audit): retire the lead-anchor widening family for a key-position allowlist (#11736)
`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. Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx Co-authored-by: Claude <noreply@anthropic.com>
1 parent a6e0010 commit e30a7e5

1 file changed

Lines changed: 119 additions & 61 deletions

File tree

scripts/docs-audit/affected-docs.mjs

Lines changed: 119 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1404,42 +1404,61 @@ function typeDeclRegions(code) {
14041404
* • `dottedRe` `(?<![\w$])` a DOTTED token, which must tolerate its own dots
14051405
* • `rulePatternFor`/`commandPatternFor` `(?<![\w$.-])` a doc-side PROSE span, where `-`
14061406
* glues tokens in English
1407-
* `declLead`'s key is a BARE token, so `symbolRe`'s set is the analogue: `$` continues an
1408-
* identifier (`$route`), and `.` makes the token a MEMBER ACCESS whose colon belongs to a
1409-
* ternary and never to a key (`cond ? obj.route : x`). Both mint the same phantom, and a
1410-
* character class closes the class rather than enumerating the escapees.
1407+
* ⛔ AND THAT TABLE IS WHY THIS IS NOW AN ALLOWLIST (#11717). Every idiom above names a class
1408+
* it EXCLUDES, so every one of them has a residue — and this key's residue was worked through
1409+
* one card at a time: #11494 the colon run, #11542 the word boundary (`subroute:`), #11630 `$`
1410+
* and `.` (`$route:`, `cond ? obj.route : x`), #11711 queued for Unicode because `\w` is
1411+
* ASCII-only. Each card was small, provably free and honestly priced, and each named the next
1412+
* residue as a pin for the next card to flip. That is a good discipline for an open-ended
1413+
* defect and the wrong one for a BOUNDED defect: over code points 0..0x2FFF the `\b` anchor
1414+
* admitted 12225 characters and `(?<![\w$.])` admitted 12223, so the whole family was arguing
1415+
* about a handful at the edge of a set of twelve thousand.
14111416
*
1412-
* ⛔ `-` IS DELIBERATELY NOT IN THE SET, which is the one place this departs from
1413-
* `rulePatternFor`. `a-route` is two tokens (`a - route`), so the `route` there IS the whole
1414-
* token `route` — it is not a declaration for a DIFFERENT reason (expression position), and
1415-
* that reason is shared with the bare `cond ? route : x` that NO lookbehind can exclude.
1416-
* Excluding `-` would close one spelling of that class while leaving its plainest spelling
1417-
* open, which is enumerating escapees wearing a character class.
1417+
* SO THE ANCHOR IS INVERTED rather than shrunk a fifth time. It no longer enumerates what may
1418+
* not PRECEDE the key; it names the positions where an object-literal property key may BEGIN —
1419+
* start of input, whitespace, `{`, `,` — and rejects everything else. Spelled as a negative
1420+
* lookbehind over the COMPLEMENT of that allowlist, `(?<![^\s{,])`, which also gives
1421+
* start-of-input for free rather than as a second alternative. The set is bounded at three
1422+
* characters where a blocklist is not bounded at all, and it closes `$`, `.`, `-`, Unicode and
1423+
* every future escapee of that shape in ONE move instead of one per card.
14181424
*
1419-
* ⛔ THE RESIDUE THIS STILL DOES NOT CLOSE, named rather than left to be discovered. `\w` is
1420-
* ASCII-only, so a Unicode identifier character still passes: `éroute:` is admitted by the
1421-
* lookbehind exactly as it was by `\b` (measured — both admit it). Closing it means a
1422-
* `\p{L}`-class lookbehind under the `u` flag, which changes the escape semantics of every
1423-
* source these leads are COMPOSED with at the eight call sites. 0 occurrences across the
1424-
* seven live ledgers, and pinned in `--self-test` as deliberately unmoved so the next card
1425-
* of this shape MOVES a pin rather than finding none.
1425+
* ⛔ AND IT NEEDS NO `u` FLAG, which is the concrete cost the blocklist route was carrying:
1426+
* closing the Unicode residue as a class meant `\p{L}` under `u`, which changes the escape
1427+
* semantics of every source these leads are COMPOSED with at the eight call sites. An
1428+
* allowlist of ASCII positions needs none, so #11711 is subsumed at no cost at all.
14261429
*
1427-
* ⛔ THIS IS THE SECOND POPULATION MOVE, and unlike #11542's it moves `declarationsIn` too —
1428-
* the one scan #11542's before/after was priced to leave byte-identical. It is priced with
1429-
* its OWN before/after against the header of `--bridge-coverage`, at ROW IDENTITY rather
1430-
* than counter equality: `--bridge-coverage --json` carries all 177 `unreachableRows` by
1431-
* `{file, route, client}`, and it hashes `d04a5cedfb613370e5b46ac4725db1d941e5dc88` on the
1432-
* base tree, on the ablated tree and on the fixed tree alike. Counters agreeing is consistent
1433-
* with two rows swapping places; row identity is not. Provably free for the direct reason:
1434-
* across the seven live ledgers, all 499 `route:`/`client:` leads are preceded by a SPACE —
1435-
* 0 preceded by `$`, 0 by `.`, 0 by any non-word character at all (positive control: the same
1436-
* scan reports both classes when a fixture carries them).
1430+
* ⛔ THE RESIDUE THIS LEAVES — and it is the end of what ANY left-anchor can reach, so it is a
1431+
* BOUNDARY rather than the next link in the chain. A key in EXPRESSION position preceded by
1432+
* whitespace, `cond ? route : 'GET /api/v1/x'`, is byte-for-byte what a property key looks
1433+
* like, so the allowlist admits it — correctly, by its own rule. #11630 named this same class
1434+
* as the one no lookbehind can reach, and that is exactly why it left `-` admitted: `a-route`
1435+
* really is the whole token `route`. The allowlist closes every spelling of the class that
1436+
* WEARS a character (`$route`, `.route`, `-route`, `éroute`) and leaves the plainest one.
1437+
* Closing THAT needs the colon's enclosing expression, not its left neighbour — a parser
1438+
* question, not an anchor question, and a different card if a puller ever appears. Pinned in
1439+
* `--self-test` as deliberately unmoved.
14371440
*
1438-
* ⛔ AND IT CAN ONLY EVER REMOVE. The key alternation always opens with a word character, so
1439-
* `\b` there fails exactly when the previous character is a word character — making
1440-
* `(?<![\w$.])` a STRICT SUBSET of `\b`. Swept over code points 0..0x2FFF: `\b` admits 12225,
1441-
* the lookbehind admits 12223, and the lookbehind admits 0 that `\b` does not. The move is
1442-
* exactly the 2 characters named above.
1441+
* ⛔ THIS IS THE THIRD POPULATION MOVE, and like #11630's it moves BOTH the ledger rows and
1442+
* `declarationsIn`. Priced with its own before/after against the header of `--bridge-coverage`,
1443+
* at ROW IDENTITY rather than counter equality, and re-derived against THIS base rather than
1444+
* inherited from #11630: `--bridge-coverage --json` carries all 177 `unreachableRows` by
1445+
* `{file, route, client}` and still hashes `d04a5cedfb613370e5b46ac4725db1d941e5dc88`, and the
1446+
* `declarationsIn` population is byte-identical across the change. Counters agreeing is
1447+
* consistent with two rows swapping places; row identity is not. Provably free for the direct
1448+
* reason: across the seven live ledgers all 499 `route:`/`client:` leads are preceded by a
1449+
* SPACE — 0 by `$`, 0 by `.`, 0 by `-`, 0 by `{`, 0 by `,`, 0 by anything outside the
1450+
* allowlist at all (positive control: the same scan reports every one of those classes the
1451+
* moment a fixture carries them, so the zeros are readings and not a blind scan).
1452+
*
1453+
* ⛔ AND IT CAN ONLY EVER REMOVE, swept rather than argued — against the anchor it actually
1454+
* replaces, not against the one two cards ago. Over code points 0..0x2FFF `(?<![\w$.])` admits
1455+
* 12223 and this allowlist admits 25, and the allowlist admits 0 that `(?<![\w$.])` does not.
1456+
* Both numbers and the zero are pinned in `--self-test`.
1457+
*
1458+
* WHY THERE IS NO RIGHT-HAND ANCHOR, since every idiom above carries one: the `\s*:` that
1459+
* follows IS the right anchor, exactly. `routes:` cannot match — after `route` the `\s*` takes
1460+
* nothing and the `:` meets `s` — while `route :` legitimately does. A trailing class would be
1461+
* a second spelling of a constraint the colon already makes exact.
14431462
*
14441463
* @param {string} keys the key alternation ONLY — `(route|client)`, `route`,
14451464
* `(?:route|client)`. The capture groups are the call site's question; the ANCHOR, the
@@ -1449,7 +1468,7 @@ function typeDeclRegions(code) {
14491468
* no argument carries either form.
14501469
*/
14511470
function declLead(keys) {
1452-
return String.raw`(?<![\w$.])${keys}\s*:\s*`;
1471+
return String.raw`(?<![^\s{,])${keys}\s*:\s*`;
14531472
}
14541473

14551474
/**
@@ -3092,33 +3111,59 @@ function selfTest() {
30923111
'1 route / 1 client / 0 declined',
30933112
`${dottedKey.routesDeclared} route / ${dottedKey.clientsDeclared} client / ${dottedKey.declined.length} declined`);
30943113

3095-
// ⛔ THE BOUNDARY THIS CARD DOES NOT CROSS, in its turn — both halves pinned so the next
3096-
// card of this shape MOVES a pin rather than finding none, exactly as this one did.
3114+
// ⛔ BOTH OF #11630'S BOUNDARY PINS ARE FLIPPED HERE (#11717), not deleted — the allowlist
3115+
// crosses both, and a green reached by removing an assertion is the one thing this gate
3116+
// cannot afford.
30973117
//
3098-
// (a) `-` is NOT in the set. `a-route` is two tokens, so that `route` IS the whole token —
3099-
// not a declaration for a DIFFERENT reason (expression position), one it shares with the
3100-
// bare `cond ? route : x` no lookbehind can reach. Excluding `-` would close one spelling of
3101-
// that class and leave its plainest spelling open.
3118+
// (a) `-`. #11630 left it admitted on the reasoning that `a-route` is two tokens, so that
3119+
// `route` IS the whole token and is a non-declaration for a DIFFERENT reason (expression
3120+
// position) — a reason it shares with the bare `cond ? route : x` no lookbehind can reach.
3121+
// The allowlist moves it, and for a reason that reads the same fact the other way round:
3122+
// whole token or not, `-` is not a place an object-literal KEY may begin. What #11630 called
3123+
// the shared class is now pinned directly, in its plainest spelling, at the end of this
3124+
// block — so the class is pinned rather than approximated by one of its spellings.
31023125
const minusKey = parseLedgerSource([
31033126
'export const L = [',
31043127
" { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' },",
31053128
'];',
31063129
"const n = cond ? a-route : 'GET /api/v1/gone';",
31073130
].join('\n'));
3108-
check('parseLedgerSource', 'a `-`-prefixed lead still mints a row — deliberately outside the set (#11630)',
3109-
'row count', 2, minusKey.rows.length);
3110-
// (b) `\w` is ASCII-only, so a UNICODE identifier character still passes — `éroute:` is
3111-
// admitted by the lookbehind exactly as it was by `\b`. Closing it means a `\p{L}` class
3112-
// under the `u` flag, which changes escape semantics for every source these leads are
3113-
// COMPOSED with at the eight call sites. 0 occurrences across the seven live ledgers.
3131+
check('parseLedgerSource', 'a `-`-prefixed lead mints NO row — the pin #11630 left is FLIPPED by the allowlist (#11717)',
3132+
'row count', 1, minusKey.rows.length);
3133+
// (b) UNICODE, which is #11711's whole card, subsumed. `\w` is ASCII-only, so `éroute:` was
3134+
// admitted by the lookbehind exactly as it was by `\b`. As a BLOCKLIST closing it meant a
3135+
// `\p{L}` class under the `u` flag, which changes escape semantics for every source these
3136+
// leads are COMPOSED with at the eight call sites — the concrete cost that kept it open. An
3137+
// allowlist of ASCII positions needs no flag, so it closes for free. 0 occurrences across
3138+
// the seven live ledgers either way.
31143139
const unicodeKey = parseLedgerSource([
31153140
'export const L = [',
31163141
" { éroute: 'GET /api/v1/gone', family: 'metadata', disposition: 'sdk' },",
31173142
" { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' },",
31183143
'];',
31193144
].join('\n'));
3120-
check('parseLedgerSource', 'a UNICODE-prefixed lead still mints a phantom row — residue, deliberately unmoved',
3121-
'row count', 2, unicodeKey.rows.length);
3145+
check('parseLedgerSource', 'a UNICODE-prefixed lead mints NO row — #11711 subsumed, and with NO `u` flag (#11717)',
3146+
'row count', 1, unicodeKey.rows.length);
3147+
3148+
// ⛔ (c) THE RESIDUE THIS LEAVES, and it is the END of what any left-anchor can reach — a
3149+
// boundary, not the next link in the chain. A key in EXPRESSION position preceded by
3150+
// WHITESPACE is byte-for-byte what a property key looks like, so the allowlist admits it,
3151+
// correctly by its own rule. This is the class #11630 named as unreachable by any
3152+
// lookbehind and used to justify leaving `-` open; the allowlist closes every spelling of it
3153+
// that WEARS a character and leaves this one. Closing it needs the colon's enclosing
3154+
// EXPRESSION, not its left neighbour — a parser question. Pinned deliberately unmoved so a
3155+
// card that ever takes it MOVES a pin rather than finding none.
3156+
const bareExprKey = parseLedgerSource([
3157+
'export const L = [',
3158+
" { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' },",
3159+
'];',
3160+
"const n = cond ? route : 'GET /api/v1/gone';",
3161+
].join('\n'));
3162+
check('parseLedgerSource', 'a bare `? route :` in EXPRESSION position still mints a phantom — deliberately unmoved',
3163+
'row count', 2, bareExprKey.rows.length);
3164+
check('parseLedgerSource', 'and it is still SILENT — all eight scans agree on it, so both terms move together',
3165+
'declared', '2 route / 1 client / 0 declined',
3166+
`${bareExprKey.routesDeclared} route / ${bareExprKey.clientsDeclared} client / ${bareExprKey.declined.length} declined`);
31223167

31233168
// ⛔ REPORTED, NEVER A VERDICT. A comment explaining a retired row by quoting its old path
31243169
// is legitimate prose; reddening CI over it is the false red the #9747 family declines.
@@ -3754,7 +3799,7 @@ function selfTest() {
37543799
// copy is the same hole re-opening, and only a source pin can see it: every behavioural
37553800
// fixture above would keep passing while the new scan drifted on its own.
37563801
check('declLead', 'the run between a `route:`/`client:` colon and its value is spelled ONCE', 'affected-docs.mjs',
3757-
1, (ownSource.match(/String\.raw`\(\?<!\[\\w\$\.\]\)\$\{keys\}\\s\*:\\s\*`/g) || []).length);
3802+
1, (ownSource.match(/String\.raw`\(\?<!\[\^\\s\{,\]\)\$\{keys\}\\s\*:\\s\*`/g) || []).length);
37583803
check('declLead', 'and all eight lead scans are built from it, none inline', 'affected-docs.mjs',
37593804
8, (ownSource.match(/new RegExp\(declLead\(/g) || []).length);
37603805
// …AND THE KEY ANCHOR IS SPELLED ONCE TOO (#11542), which is the same pin one field over.
@@ -3773,15 +3818,19 @@ function selfTest() {
37733818
// come back anchored, from the one place that spells the anchor. This is what "all eight
37743819
// read the same anchored spelling" means when checked rather than asserted.
37753820
check('declLead', 'every key spelling a call site passes comes back ANCHORED', 'declLead',
3776-
String.raw`(?<![\w$.])(route|client)\s*:\s* | (?<![\w$.])route\s*:\s* | (?<![\w$.])(?:route|client)\s*:\s*`,
3821+
String.raw`(?<![^\s{,])(route|client)\s*:\s* | (?<![^\s{,])route\s*:\s* | (?<![^\s{,])(?:route|client)\s*:\s*`,
37773822
['(route|client)', 'route', '(?:route|client)'].map((k) => declLead(k)).join(' | '));
3778-
// …and BEHAVIOURALLY the anchor is a strict TIGHTENING of the `\b` it replaced, which is the
3779-
// invariant the population pricing rests on: the key alternation always opens with a word
3780-
// character, so `\b` there fails exactly when the previous character is a word character,
3781-
// making `(?<![\w$.])` a strict SUBSET. Swept rather than argued — over code points
3782-
// 0..0x2FFF the lookbehind admits nothing `\b` does not, and exactly 2 characters move.
3823+
// …and BEHAVIOURALLY the allowlist is a strict TIGHTENING of BOTH anchors it has replaced,
3824+
// which is the invariant the population pricing rests on. Swept rather than argued, and
3825+
// swept against the anchor it ACTUALLY replaces (#11710's lookbehind) as well as against the
3826+
// `\b` two cards back: a sweep that only ever compares with the oldest spelling stops being
3827+
// evidence the moment two cards land in a row.
37833828
{
37843829
const bAnchor = new RegExp(String.raw`\b(route|client)\s*:\s*`);
3830+
// The lookbehind this card replaces, spelled here as a LITERAL rather than taken from
3831+
// `declLead` — it is the BEFORE state, so reading it from the function under test would
3832+
// make the comparison vacuous the moment the function changes.
3833+
const prevAnchor = new RegExp(String.raw`(?<![\w$.])(route|client)\s*:\s*`);
37853834
// Built through a NAMED intermediate on purpose: the pin above counts the eight
37863835
// production lead SCANS by the way each one compiles `declLead` directly, and this probe
37873836
// is a behavioural check rather than a ninth scan. Inflating that count to 9 would blunt
@@ -3791,19 +3840,28 @@ function selfTest() {
37913840
// quoted, for the same reason.
37923841
const leadSource = declLead('(route|client)');
37933842
const lead = new RegExp(leadSource);
3794-
let admitsMore = 0;
3795-
let moved = 0;
3843+
let admitsMoreThanB = 0;
3844+
let admitsMoreThanPrev = 0;
3845+
let admitsB = 0;
3846+
let admitsPrev = 0;
3847+
let admitsLead = 0;
37963848
for (let c = 0; c < 0x3000; c++) {
37973849
const s = String.fromCodePoint(c) + "route: 'x'";
37983850
const b = bAnchor.test(s);
3851+
const q = prevAnchor.test(s);
37993852
const l = lead.test(s);
3800-
if (l && !b) admitsMore++;
3801-
if (b && !l) moved++;
3853+
if (b) admitsB++;
3854+
if (q) admitsPrev++;
3855+
if (l) admitsLead++;
3856+
if (l && !b) admitsMoreThanB++;
3857+
if (l && !q) admitsMoreThanPrev++;
38023858
}
38033859
check('declLead', 'the anchor only ever REMOVES — it admits nothing `\\b` did not', 'code points 0..0x2FFF',
3804-
0, admitsMore);
3805-
check('declLead', 'and the characters it moves are exactly `$` and `.`', 'code points 0..0x2FFF',
3806-
2, moved);
3860+
0, admitsMoreThanB);
3861+
check('declLead', 'and nothing the LOOKBEHIND it replaces did not — the invariant against the real before-state',
3862+
'code points 0..0x2FFF', 0, admitsMoreThanPrev);
3863+
check('declLead', 'and the allowlist is the far smaller set, by the margin the sweep measures',
3864+
'code points 0..0x2FFF', '12225 / 12223 / 25', `${admitsB} / ${admitsPrev} / ${admitsLead}`);
38073865
}
38083866

38093867

0 commit comments

Comments
 (0)