Uh oh!
There was an error while loading. Please reload this page.
fix(docs-audit): retire the lead-anchor widening family for a key-position allowlist (#11717) - #11736
Conversation
…ition 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_015ahemw8RcTgqtxrj15PEZxResolved scripts/docs-audit/affected-docs.mjs in favour of the key-position allowlist, taking main's version as the resolution base so #11710 (#11630) is carried forward rather than reverted: all of its scan-by-scan `$route:` and member-access fixtures are kept, and its two boundary pins (`-` and Unicode, both left deliberately unmoved there) are FLIPPED rather than duplicated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. |
os-steve
commented
Aug 24, 2026
ACCEPT at The correction, and the sentence worth keeping
That is the whole lesson and you wrote it better than I did. The merge-queue detail is the part that makes it a trap rather than carelessness: The merge did what it had toResolving from main's version and re-applying on top is the only ordering that cannot silently revert #11710, and you proved it the right way: every one of #11710's Pin accounting checked at Three things I did not ask for and would not have thought toThe closure walk earned itself twice. On the merged base it picked up The sweep went three-way, for a stated reason.
⭐ That is a general finding about this whole family's method, not a detail of this PR. Two cards landing in sequence is exactly when a carried-forward baseline goes quietly stale. The positive control read correctly through a changed world. On the merged base the synthetic ledger reports 5 vs 3 with The boundary, accepted and correctly framed
Avoiding the #11711 — I will close it by hand as subsumed on merge. Writing no closing keyword for it was right: a closing keyword ignores any negation around it, so "does not close #11711" is unsafe to write. Flipping ready; arming once every check is green. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#11717
declLeadinscripts/docs-audit/affected-docs.mjsanchored the ledgerroute:/client:key with a blocklist — first a word boundary, then #11630's lookbehind over[\w$.]. Every blocklist names a class it excludes, so every one of them has a residue, and this key's residue was worked through one card at a time: #11494 the colon run, #11542 the word boundary (subroute:), #11630$and., #11711 queued for Unicode because\wis ASCII-only. Each card was small, provably free and honestly priced, and each named the next residue as a pin for the next card to flip. That is a good discipline for an open-ended defect and the wrong one for a bounded one.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. It stays spelled once, indeclLead, with the\s*:\s*run untouched. It is spelled verbatim indeclLead's docblock and in the--self-testboundary comment, and deliberately not here: GitHub's body sanitizer strips the less-than + bang digraph out of an issue or PR body — inside code spans and fenced blocks alike — and silently leaves a different, valid-looking regex behind. Measured twice on #11634, honoured by #11710.⛔ And it needs no
uflag, which is the concrete cost the blocklist route was carrying. Closing the Unicode residue as a character class meant\p{L}underu, which changes escape semantics for every source these leads are composed with at the eight call sites. An allowlist of ASCII positions needs none, so #11711 is subsumed at no cost.This branch is merged with
main, and it carries #11710 forward rather than replacing itAn earlier revision of this PR was cut from
e170b0ae5, which predates #11710 in graph order — merge-queue commit dates are not monotonic, and I wrongly reported that as "#11710 is not merged", which was a claim aboutmainmade from a reading of my own base.origin/mainhas been merged in (no rebase), and the conflict in this one file was resolved from main's version, so:$route:ones, the member-access.route :ones — are kept intact. The allowlist satisfies every one of them.-and Unicode, both left there as deliberately unmoved) are flipped, not duplicated and not deleted. There is exactly one pin per class, in the moved state.The premise was attacked before it was implemented
The card asked for its own ruling to be attacked. Both halves hold:
new RegExp(declLead(…))call sites pass string literals —(route|client)×3,route×3,client×1,(?:route|client)×1. Nothing is parameterised, computed or read from config.nextRouteResearchesrest.slice(1)andwindowClientRematcheswindow, which begins at the current row's ownroute. In both, any later key's preceding character is necessarily inside the slice, so neither can lose context. The other six read a whole string.One correction to the card's framing, which the PM has accepted. The card said an allowlist closes "
$/./ Unicode /-/ every future escapee at once". It closes every spelling that wears a character and leaves the plainest one.cond ? route : 'GET /api/v1/x'— a bare key in expression position preceded by whitespace — is byte-for-byte what a property key looks like, so the allowlist admits it, correctly by its own rule. #11630 named this same class as the one "no lookbehind can reach", and that is exactly why it left-admitted:a-routereally is the whole tokenroute. So this is a boundary, not the next link in the chain — closing it needs the colon's enclosing expression, not its left neighbour, which is a parser question. It is pinned as deliberately unmoved, in its plainest spelling, so the class is pinned rather than approximated by one of its spellings.It admits strictly LESS — swept against the anchor it actually replaces
Swept over code points
0..0x2FFF, start-of-input checked separately (admitted by all three). All of this is pinned in--self-test. The sweep now compares against both predecessors on purpose: a sweep that only ever compares with the oldest spelling stops being evidence the moment two cards land in a row — which is exactly what happened here.TWO populations priced, both at ROW IDENTITY, both re-derived on the merged base
Counters agreeing is consistent with two rows swapping places, and this ledger's own arithmetic is what hides the defect — so both are priced by hashing the row list and taking the set difference in both directions.
Population 1 — the ledger rows (
--bridge-coverage)945ffbea8fcd296bbdrowsParsed/routesDeclaredclientRows/clientsDeclaredreachable/unreachableleadsOutsideCodebrokenScanWhich rows move: none.
--bridge-coverage --jsoncarries all 177unreachableRowsby{file, route, client}, andgit hash-objectgivesd04a5cedfb613370e5b46ac4725db1d941e5dc88on the merged base and on this branch. Set difference over{file, route, client}is 0 in each direction (177 vs 177). The human-rendered output diffs empty.Population 2 —
declarationsIn, which moves tooMeasured by slicing
declarationsInand its dependency closure out of the file and running the file's own bodies over the seven live ledgers. The closure is walked fromdeclarationsIn, never hand-listed, and the walk follows imported edges as well — load-bearing twice over:blankandscanSourcelive in../js-comment-mask.mjs, and on the merged base the walk picked upunreadableIn, a function that did not exist at the earlier base. A hand-listed closure would have silently measured the wrong thing.git hash-object945ffbea8766641aeefd8fff085a39a087abc24e4fe2ecffffcd296bbd766641aeefd8fff085a39a087abc24e4fe2ecfffSet difference 0 in each direction. The hash is this harness's own row shape (it carries a
filefield), so it is compared only against itself across the two trees and is not offered as matching any figure from #11710.Positive control — run BEFORE either zero was believed
A synthetic ledger carrying the escapees plus the residue. On the merged base the harness reports 5 declarations and on this branch 3, and the diff names exactly the two this card adds over #11710:
$and.are absent from both sides because #11710 already closed them — which is itself the control working. The three survivors are the genuineroute:, its genuineclient:, and the bare-expression residue: non-vacuity in both directions from one fixture. The preceding-character tally carries its own control the same way, and it failed first on a wrong expectation spelling — fixed in the expectation, not in the instrument.Why both are free on today's tree — re-derived at the merged base
Across the seven live ledgers there are 499
route:/client:lead occurrences and every one is preceded by a space: 0 by$, 0 by., 0 by-, 0 by{, 0 by,, 0 by any character outside the allowlist at all. All 499 survive.The pins are FLIPPED and ADDED, never deleted
--prefixed lead still mints a row — deliberately outside the set\bdid not$and.Added: a second sweep leg pinning that the allowlist admits 0 that the lookbehind it replaces did not, the three-way admit counts, and the bare-expression residue in its plainest spelling with its silence pinned alongside.
--self-test: 416 cases, merged base 413 — +3 net, on a diff that flips two assertions and adds three.Reverse verification (ablation) — mutation proven on disk, restore under a trap
The one line the fix moved was reverted to the merged base's spelling — #11630's lookbehind, not the word boundary two cards back — under
trap … EXIT INT TERMarmed before the mutation.affected-docs.mjsis run directly from source bynode: there is nodist/in its path, so no rebuild leg is involved and none is claimed. Mutation confirmed on disk by counting the injected and the removed text separately, anchored on the exact text meant to move, plus hashes:Direction predicted before running: turn red on exactly this card's delta over #11710, and on nothing else. Observed exactly that —
ABLATED_SELFTEST_EXIT=1, 5 checks fail, eachgotvalue the before state:The controls that stayed green are the point, not an omission — a red one would mean the fixture, not the fix, was doing the work:
$route:and.route :fixtures — green both ways. They are fix(docs-audit): widen the ledger key anchor past the word boundary (#11630) #11710's, and the allowlist satisfies them for its own reason. A red one there would mean this merge had reverted fix(docs-audit): widen the ledger key anchor past the word boundary (#11630) #11710.the anchor only ever REMOVESandnothing the LOOKBEHIND it replaces did not— 0 on both trees. Structural: under ablation each sweep leg compares the reverted anchor against itself, so neither can report a difference. The three-way margin is the leg that moves, and it does.a bare? route :in EXPRESSION position still mints a phantom— green both ways, the boundary this deliberately does not cross.no call site restates the anchor(0) andall eight lead scans are built from it(8) — green both ways.Non-vacuity in the other direction is carried by the same fixtures: the genuine
route: 'GET /api/v1/meta'is still read, still carriesmeta.getTypes, and the 269 live rows are unchanged.Verification
Merged base
origin/main945ffbea8; measured and re-measured on final headfcd296bbd, with a clean tree at that commit.Gate union derived, not recalled —
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(no paths; the script takes the change set from the merge base itself, and after the merge it correctly reduces to this branch's one path), 9 families, pluscheck:nul-bytesand the self-test directly. Exit codes captured before any pipe; each verdict below is the line the gate itself printed.pnpm check:agent-test-spelling✓ check-agent-test-spelling: 0 violations — 351 file(s) · 3319 bare -- token(s)…pnpm check:cross-package-test-inputsOK: 16 package(s) read outside themselves, all declared…pnpm check:docs-audit-scope✓ release-owned pages are in scope and read-only: 9 page(s)…pnpm check:entry-guard✓ check:entry-guard: 143 scripts/ file(s) — every entry guard goes through invoked-as.mjspnpm check:parse-guard✓ check:parse-guard: 142 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.pnpm check:pm-governed-merges✓ check-governed-merges --self-test: 129 assertionspnpm check:pnpm-filter-targets✓ check:pnpm-filter-targets: 135/168 --filter occurrence(s)… resolvenode scripts/check-cross-package-test-inputs.mjsOK: 16 package(s) read outside themselves, all declared…node scripts/docs-audit/check-affected-docs.mjs✓ affected-docs self-test: 416 cases pass.pnpm check:nul-bytes✓ check-nul-bytes --self-test: 75 assertions over a temp git repoaffected-docs.mjs --self-test✓ affected-docs self-test: 416 cases pass.(merged base: 413)Lint — the full repo-wide run, not a narrowing.
pnpm lint(eslint . --no-inline-config) was run in full onfcd296bbdunder the shared verify lock: exit 0, no findings,VERDICT command-exit 0 · held the lock 56s · waited 51s. No narrowing is claimed and none is needed.No changeset — checked against the actual rule, not inherited.
changeset-checkin.github/workflows/pr-automation.ymlhas no path exemption; its only two exemptions are theskip-changesetlabel and the Changesets release PR. Verified that this branch's own diff publishes nothing: the root package isprivate: truewith nofilesfield,scripts/matches none of thepnpm-workspace.yamlglobs, and it has nopackage.jsonof its own.skip-changesetapplied and read back. (The.changeset/*.mdfiles visible in the merge commits aremain's, arriving through the merge, not this card's.)Generated by Claude Code
Generated by Claude Code