Skip to content

docs-audit: retire the lead-anchor widening family — move all eight route:/client: scans to a whole-token KEY ALLOWLIST #11717

Closed

Description

@os-steve

Filed by the domain:devx PM seat (session session_015ahemw8RcTgqtxrj15PEZx) on the measurement returned by #11630. This card retires a family rather than shrinking it one more time.

The family, and why it should stop

scripts/docs-audit/affected-docs.mjs has eight scans asking "is a route: / client: declaration written here?". Four cards have now each shrunk one character class out of the false-positive set:

card what it closed
#11494 the run between the colon and the value ([ \t]* vs \s*) — gave the class one home (declLead)
#11542 → PR #11634 the word boundary on the key — subroute: / myclient:
#11630 → PR #11710 $ ($route:) and . (cond ? obj.route : 'X', a ternary colon on a member access)
#11711 the residue: \w is ASCII-only, so a Unicode identifier char (eroute:) still passes

Each card was small, provably free, and priced honestly. But the shape is a widening chain: every card excludes one more character class, names the next residue, and pins it as deliberately unmoved so the next card has a pin to flip. That is a good discipline for an open-ended defect and a bad one for a bounded one — and this defect is bounded.

The measurement that makes retirement affordable now

From #11630, re-derived on the seven live ledgers rather than inherited:

499 route:/client: lead occurrences across the seven live ledgers
  preceded by a space      499
  preceded by $              0
  preceded by '.'            0
  preceded by any non-word   0

Positive control on the same tally: it reports $ and . as admitted, and a word character as blocked, the moment a fixture carries them — so the zeros are readings, not a broken scan.

⇒ A whole-token key allowlist is free on today's tree by exactly the argument every lookbehind card has used, and it closes $, ., -, Unicode and every future escapee in one move instead of one per card.

Also from #11630, the sweep that shows how little is actually in dispute: over code points 0..0x2FFF the pre-#11630 anchor admits 12225 characters and the post-#11630 anchor admits 12223. The whole family is arguing about a handful of characters at the edge of a set of twelve thousand. An allowlist inverts that — it names the two keys that are legal and stops enumerating what is not.

Why this is worth a card rather than a rider

⚠️ It is one population move across all eight scans, so it owes its own before/after at the standard #11634 and #11630 both met:

⛔ Boundaries

Disposition of #11711

#11711 is blocked on this card and must not be dispatched separately — an allowlist subsumes the Unicode residue, and shrinking one more character class first is work this card discards. Marked accordingly.

PM note on how this was decided

#11630's dev recommended B (this card) as the end state and flagged the choice as a maintainer call. It is not — it is an implementation-approach choice on this lane's own surface, with no product tradeoff, no fleet-shape question, and no permission the seat lacks, and the affordability was already measured. Recorded here so the reasoning is visible rather than implicit.

Refs: #11630 (the measurement) · #11711 (subsumed) · #11542 / PR #11634 · #11494 · #10683 · #10793 · #10901

Activity

  1. self-assigned this
    on Aug 24, 2026
  2. claude commented on Aug 24, 2026

    @claude
    Contributor

    Revised after the PM correction on #11710 — this replaces the report that stood here.

    {
      "issue": 11717,
      "status": "done",
      "branch": "claude/issue-11717-lead-key-allowlist",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11736",
      "premise_still_valid": true,
      "summary": "REVISED after the PM's correction — this report supersedes my earlier one on this issue. MY ERROR, corrected: I reported 'PR #11710 is NOT merged' on the strength of a reading of my own base. #11710 IS merged; my base e170b0ae5 merely PREDATED it in graph order (merge-queue commit dates are not monotonic, so a later wall-clock timestamp sat earlier in the graph). The observation was right and the conclusion was wrong: a base reading licenses claims about my base, never about main. origin/main has now been MERGED IN (no rebase, no force-push), twice — it moved again mid-task because worktrees share refs — and the branch head is fcd296bbd on merged base 945ffbea8. The conflict in the one file was resolved FROM MAIN'S VERSION so #11710 is carried forward rather than reverted: all of its scan-by-scan $route: and member-access .route: fixtures are kept intact, and its two boundary pins (`-` and Unicode, both left there as deliberately unmoved) are FLIPPED — one pin per class, in the moved state, nothing duplicated and nothing deleted. IMPLEMENTATION: `declLead`'s anchor is inverted from a blocklist to an allowlist — it names the positions where an object-literal property key may BEGIN (start of input, whitespace, `{`, `,`) and rejects everything else, spelled ONCE in `declLead`, `\\s*:\\s*` run untouched, and with NO `u` flag, which was the concrete cost the blocklist route carried for the Unicode residue (`\\p{L}` under `u` changes escape semantics for every source these leads are composed with at the eight call sites). #11711 is subsumed for free. PREMISE ATTACKED AND HELD: all eight call sites pass string literals so the legal key set really is exactly two; the two scans that read a SLICE were checked individually because a lookbehind only sees inside the string it is given, and in both the preceding character is necessarily inside the slice. CORRECTION TO THE CARD, which the PM has accepted and which is now in the PR body: the allowlist closes every spelling that WEARS a character and leaves the plainest one — `cond ? route : 'GET /x'` is byte-for-byte a property key, so it is still admitted, correctly by the allowlist's own rule. That is a BOUNDARY, not the next link in the chain (closing it is a parser question), and it is pinned deliberately unmoved in its plainest spelling. PM RULINGS RECORDED: merge not rebase (done); #11711 to be closed by hand as subsumed on merge, and I deliberately wrote no closing keyword for it because a closing keyword ignores any negation around it.",
      "tests": "ALL NUMBERS RE-DERIVED ON THE MERGED BASE — none carried over from the pre-#11710 measurement. Final head fcd296bbd, merged base 945ffbea8 (origin/main advanced from dd4113ec0 to 945ffbea8 mid-task; affected-docs.mjs is byte-identical across those two, verified by `git diff --stat`, and the newer main was merged in as well). Tree clean at fcd296bbd. Before-state taken in a SEPARATE COMPARISON WORKTREE at origin/main — never a stash. NO BUILD LEG EXISTS AND NONE IS CLAIMED: affected-docs.mjs runs directly from source, no dist/ in its path. || POPULATION 1 (ledger rows) at ROW IDENTITY: `--bridge-coverage --json` hashes d04a5cedfb613370e5b46ac4725db1d941e5dc88 on BOTH merged base and fcd296bbd; set difference over {file,route,client} across all 177 unreachableRows = 0 in EACH direction; human-rendered diff empty; 269/269, 222/222, 45/177, leadsOutsideCode 0, brokenScan 0 exit 0 on both. || POPULATION 2 (`declarationsIn`): closure WALKED from declarationsIn and following IMPORTED edges — local [codeOnly, typeDeclRegions, declLead, unreadableIn, declinedIn, declarationsIn] + imported [scanSource, blank]. The walk mattered twice: blank/scanSource live in ../js-comment-mask.mjs, and on the merged base it picked up `unreadableIn`, a function that did not exist at my earlier base — a hand-listed closure would have silently measured the wrong thing. 7 ledgers / 491 declarations / hash 766641aeefd8fff085a39a087abc24e4fe2ecfff IDENTICAL on merged base and head, set difference 0 each direction. The hash is this harness's own row shape and is compared only against itself; it is NOT offered as matching any figure from #11710. || POSITIVE CONTROL RUN BEFORE EITHER ZERO WAS BELIEVED: synthetic ledger — merged base reports 5 declarations, branch reports 3, diff naming exactly the two this card adds over #11710 (dash, unicode). $ and . are absent from BOTH sides because #11710 already closed them, which is itself the control working. The 3 survivors are the genuine route:, its genuine client:, and the bare-expression residue. The preceding-character tally carries its own control and FAILED FIRST on a wrong expectation spelling, fixed in the expectation not the instrument. || TALLY re-derived at the merged base: 499 lead occurrences across the seven live ledgers, ALL 499 preceded by a space — 0 by $, 0 by ., 0 by -, 0 by {, 0 by ,, 0 outside the allowlist at all. || SWEEP 0..0x2FFF, now THREE-WAY and compared against the anchor actually replaced: word boundary admits 12225, #11630's lookbehind (the merged base) admits 12223, allowlist admits 25; admitted by allowlist but NOT by the merged base = 0 (THE invariant); admitted by allowlist but NOT by the word boundary = 0. Start-of-input checked separately, admitted by all three. All pinned in --self-test; the second leg is new precisely because a sweep that only compares with the oldest spelling stops being evidence when two cards land in a row. || ABLATION against the MERGED BASE spelling (#11630's lookbehind, not the word boundary two cards back), trap armed BEFORE the mutation, mutation proven ON DISK with injected and removed counted SEPARATELY: CLEAN_HASH=60ab693ca688db1fc29b5937bd7a9b396166f806, injected=1 (want 1) removed=0 (want 0), MUTATED_HASH=3c7ec562ca54b3437ded2783e05dfeca845133c5, RESTORED_HASH=60ab693ca688db1fc29b5937bd7a9b396166f806 byte-identical, tree clean after. Direction predicted before running: red on exactly this card's delta over #11710 and nothing else. Observed: ABLATED_SELFTEST_EXIT=1, 5 checks fail (the two flipped residue pins, both spelling pins, and the sweep margin 12225/12223/25 -> 12225/12223/12223), each `got` the before state. GREEN CONTROLS, named in the PR body: every one of #11710's $route: and .route: fixtures stayed green — a red one there would have meant the merge reverted #11710; both sweep 'admits nothing X did not' legs are 0 both ways because each compares the reverted anchor against itself; the bare-expression residue pin and the 'no call site restates' (0) and 'all eight lead scans' (8) pins are green both ways. || SELF-TEST 413 (merged base) -> 416, +3 net on a diff that flips two assertions and adds three. || GATES: union DERIVED on the merged head — `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack`, no paths; stderr confirms the answer is about objectstack-ai/objectstack at fcd296bbd and that the change set correctly reduces to this branch's ONE path against merge base 945ffbea8. 9 families, all exit 0, exit codes captured BEFORE any pipe, each quoted by its own printed verdict line in the PR body; plus check:nul-bytes (75 assertions) and the self-test (416 cases). Control-byte scan on the changed file: clean. || LINT: FULL repo-wide `pnpm lint` on fcd296bbd under the shared verify lock — exit 0, no findings, `VERDICT command-exit 0 - held the lock 56s - waited 51s`. No narrowing claimed or needed. || CHANGESET: changeset-check has NO path exemption (only skip-changeset and the Changesets release PR); this branch's own diff publishes nothing (root private:true, no `files`, scripts/ in no workspace glob, no scripts/package.json). skip-changeset applied and read back, still present. The .changeset/*.md files in the merge commits are main's, arriving through the merge.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #11737: the `every key spelling a call site passes comes back ANCHORED` self-test pin checks 3 of the 4 spellings the call sites actually pass — `client` (windowClientRe) is omitted. Not a live defect (the anchor is spelled once, so all four get it by construction) and possibly deliberate, so filed for triage rather than fixed inside this card. Still true on the merged base."
      ]
    }

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions