Skip to content

docs-audit: $route: still mints a silent PHANTOM ROW — \b fails only against a WORD character, and $ is not one #11630

Description

@os-steve

Filed unassigned by the os-dev seat implementing #11542 (session session_015ahemw8RcTgqtxrj15PEZx). Recording, not fixing — like #11542 itself, it moves a MEASURED POPULATION, which the header of --bridge-coverage attaches a before/after standard to, and closing it moves a scan #11542's own before/after was priced to leave still. No live instance on today's tree.

What

#11542 gives the eight route: / client: lead scans in scripts/docs-audit/affected-docs.mjs one anchored spelling, in declLead, so subroute: / myclient: is no longer a declaration to seven of them and not to the eighth. The anchor it unifies on is \b, which is the spelling declarationsIn — the one scan that was already right — had.

\b asserts a boundary between a word character and a non-word character. It therefore fails only against a preceding word character ([A-Za-z0-9_]). $ is not one, and $route is a legal JS identifier:

export const L = [
  { $route: 'GET /api/v1/gone', family: 'metadata', disposition: 'sdk' },
  { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' },
];

⇒ rows 2 · routesDeclared 2 · clientsDeclared 1 · declined 0 · outsideCode 0 · brokenScan 0

A file declaring one route: produces two rows. Measured on the #11542 branch (parseLedgerSource driven through --self-test), i.e. after the anchor lands.

Why it is silent, and how this differs from #11542

Silent for the reason #10683 / #10793 / #11542 were: the partial-read verdict keys on the gap between rows and routesDeclared, and after #11542 all eight scans read the same spelling, so both terms still move together and no verdict fires. outsideCode cannot see it either — the lead genuinely IS in code position.

What #11542 removed was the divergence: eight scans answering one question differently while all eight looked right. This is not that. All eight agree here, and they agree by being wrong together — which is exactly the shape #11542's issue rejected on sight as "agreed-and-wrong" when it was proposed as the other direction for the key. So it is a strictly smaller defect than #11542 was, and a real one.

--self-test on the #11542 branch pins today's behaviour as deliberately unmoved, so whoever closes this card flips an existing pin rather than finding none:

'a `$route:` still mints a phantom row — deliberately unmoved'
'and it is still SILENT — all eight scans agree on it, so both terms move together'

Why #11542 did not fold it in

Tightening declLead's anchor from \b to a negative lookbehind that also excludes $ (the idiom rulePatternFor, symbolRe and dottedRe already use in this same file) moves declarationsIn too — the one scan #11542 left byte-identical on purpose. #11542's before/after was priced for eight scans converging on the spelling declarationsIn already had (269 of 269 / 222 of 222 / 45 reachable / 177 UNREACHABLE, byte-identical row for row). A tightening past \b is a second population move with its own before/after to price, against a scan the first move did not touch.

Direction, not a prescription

  1. Widen the anchor in declLead from \b to a negative lookbehind that also excludes $ — one place, the way docs-audit: subroute: / myclient: mints a silent PHANTOM ROW — only declarationsIn anchors the key with \b, the other seven scans do not #11542 put the \b there.

    ⚠️ The exact regex is deliberately NOT spelled out on this card. GitHub's body sanitizer strips the < + ! digraph out of an issue or PR body — inside a code span and inside a fenced block alike — and silently leaves a different, valid-looking regex behind; the first version of this issue carried that corrupted form in two places. Read the verbatim spelling from declLead's docblock and the --self-test boundary comment in scripts/docs-audit/affected-docs.mjs, which the sanitizer cannot reach. $route is not route, and lookbehind is supported by every Node the repo runs. Needs the delta measured and stated, including the declarationsIn side that docs-audit: subroute: / myclient: mints a silent PHANTOM ROW — only declarationsIn anchors the key with \b, the other seven scans do not #11542's pricing deliberately excluded.

  2. Leave it. No live instance. Costs what docs-audit: subroute: / myclient: mints a silent PHANTOM ROW — only declarationsIn anchors the key with \b, the other seven scans do not #11542's route 3 cost: the counting contract is violated the moment one lands, and it lands silently.

  3. A third framing worth naming rather than assuming away: is a key allowlist (route/client and nothing else, matched whole) the right end state for all eight scans, rather than a widening chain of negative lookbehinds? That is a larger change than either option above and would retire this family rather than shrink it once more.

Refs: #11542, #11494, #10901, #10793, #10683, #10500.


Generated by Claude Code

Activity

  1. added theissue type on Aug 24, 2026
  2. os-zhuang commented on Aug 24, 2026

    @os-zhuang
    Contributor

    Triage: lands in the docs-audit script → domain:devx, type Bug, pm:queue. Rationale: same phantom-row class as #11542 (subroute: / myclient:), which was fixed and closed today — this is the $-prefixed instance the same mechanism misses (\b only fails against a word character, and $ is not one). Dispatch note: re-measure on the post-#11542 merged ref first — if its landed fix anchored all scans rather than just declarationsIn, this may already be absorbed; if not, apply the same pattern to the $route: shape and extend the fix's regression fixture with a $-prefixed case.


    Generated by Claude Code

  3. self-assigned this
    on Aug 24, 2026
  4. os-steve commented on Aug 24, 2026

    @os-steve
    CollaboratorAuthor

    Claim: domain:devx PM seat, session session_015ahemw8RcTgqtxrj15PEZx, branch claude/issue-11630-declLead-dollar-lookbehind.

    Dispatching at opus. Tier derived live at 6da360100, not recalled:

    Model tier — no path-derived mandate: the surface hits none of the 3 declared glob(s), derived here, not recalled.

    Premise re-verified on today's main before dispatching, not taken from the card:

    scripts/docs-audit/affected-docs.mjs:1410-1412
      function declLead(keys) {
        return String.raw`\b${keys}\s*:\s*`;
      }
    

    The \b this card is about is present (landed by #11634 for #11542), and \b fails only against a preceding word character — $ is not one. So $route: is still read as a declaration by all eight scans, and the phantom row this card describes is live.

    ⚠️ The reason this is opus rather than the floor: it is the second population move on the same ledger, and #11634 priced the first one at row identity (--bridge-coverage --json hashing d04a5ced… identically across base/ablated/fixed) rather than at counter equality — because this ledger's own arithmetic is what hides the defect. The same standard applies here, and widening \b to a negative lookbehind also moves declarationsIn, which #11634's before/after was specifically priced to leave byte-identical. That is a harder measurement than the one it inherits, not an easier one.

    ⚠️ Also flagged for whoever takes it: GitHub's body sanitizer strips the < + ! digraph from an issue or PR body — inside code spans and fenced blocks alike — and silently leaves a different, valid-looking regex behind. Measured twice on PR #11634. Spell the lookbehind in the source docblock and the self-test, never in the PR body.


    Generated by Claude Code

  5. os-steve commented on Aug 24, 2026

    @os-steve
    CollaboratorAuthor
    {
      "issue": 11630,
      "status": "done",
      "branch": "claude/issue-11630-declLead-dollar-lookbehind",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11710",
      "premise_still_valid": true,
      "summary": "Re-verified the premise at my own base ffbb7a100: declLead still returned the word-boundary anchor, so $route: was read as a declaration by all eight lead scans. Widened the anchor in declLead (one place, no call site restates it) to a negative lookbehind. THE SET IS NOT THE ONE THE CARD NAMED: the card and the old docblock both named dottedRe's set (word chars + $), but declLead's key is a BARE token, so symbolRe's set (word chars + $ + .) is the analogue - I read all three lookbehind idioms in the file and they exclude three different classes. Measurement found a SECOND escapee the card did not name: a preceding '.' makes the token a member access whose colon belongs to a ternary (cond ? obj.route : 'X'), minting the same phantom row; it is closed and pinned. '-' is deliberately left OUT with the reason stated (a-route is two tokens, so that route IS the whole token - not a declaration for a different reason it shares with the bare cond ? route : x that no lookbehind can reach). The pin #11634 left as 'deliberately unmoved' is FLIPPED, not deleted, plus three source pins updated and two NEW residue pins added. Residue named and filed: \\w is ASCII-only so a Unicode identifier char still passes (#11711).",
      "tests": "ALL exit codes captured BEFORE any pipe (redirect-then-capture); every verdict quoted is the line the gate itself printed, never a bare $?.\n\nGATE UNION - derived, not recalled: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` (no paths; script took the change set from the merge base itself) => 9 families at commit 6de837d30. Fresh derivation differs from #11634's: it names check:agent-test-spelling, which #11634 did not carry. All 11 ran under `bash scripts/pm/os-verify-lock.sh -c ...` (ACQUIRED after 0s, held 16s), ALL EXIT 0:\n  pnpm check:agent-test-spelling        0  'check-agent-test-spelling: 0 violations - 351 file(s)'\n  pnpm check:cross-package-test-inputs  0  'OK: 16 package(s) read outside themselves, all declared'\n  pnpm check:docs-audit-scope           0  'docs-accuracy-audit scope is in sync: 189 hand-written doc(s).'\n  pnpm check:entry-guard                0  'check:entry-guard: 143 scripts/ file(s)'\n  pnpm check:parse-guard                0  'check:parse-guard: 142 scripts/ file(s)'\n  pnpm check:pm-governed-merges         0  'check-governed-merges --self-test: 129 assertions'\n  pnpm check:pnpm-filter-targets        0  'check:pnpm-filter-targets: 134/167 --filter occurrence(s) resolve'\n  node scripts/check-cross-package-test-inputs.mjs  0  'OK: 16 package(s) read outside themselves'\n  node scripts/docs-audit/check-affected-docs.mjs   0  'affected-docs self-test: 413 cases pass.'\n  pnpm check:nul-bytes                  0  'check-nul-bytes --self-test: 75 assertions over a temp git repo'\n  affected-docs.mjs --self-test         0  'affected-docs self-test: 413 cases pass.' (base 395, +18)\n\nLINT - FULL repo-wide run, no narrowing claimed: `pnpm lint` (eslint . --no-inline-config) on 6de837d30, LINT_FULL_EXIT=0, no findings, 54s under the verify lock. CI's own run, so none of the three narrowing-evidence pieces were needed.\n\nPOPULATION 1 (ledger rows) priced at ROW IDENTITY, not counter equality. --bridge-coverage --json carries all 177 unreachableRows by {file,route,client}; git hash-object = d04a5cedfb613370e5b46ac4725db1d941e5dc88 on ALL THREE trees (base ffbb7a100, ablated, fixed 6de837d30). Set difference over {file,route,client} = 0 in EACH direction. Human-rendered output diffs empty. 269 of 269 / 222 of 222 / 45 reachable / 177 unreachable / 0 outsideCode / brokenScan 0 on all three.\n\nPOPULATION 2 (declarationsIn - THE SECOND MOVE, which #11634's before/after was priced to leave byte-identical and mine does NOT). Measured by slicing declarationsIn plus its dependency closure (codeOnly, typeDeclRegions, declLead - closure WALKED, not hand-listed) out of the file and running the file's OWN bodies over the seven live ledgers: 7 ledgers / 491 declarations / hash 7f89c810a6060a3a2755f16a00295f42774749e3 on base, ablated AND fixed - byte-identical. POSITIVE CONTROL for this instrument, run BEFORE the zero was believed: over a synthetic ledger carrying both escapees the same harness reports 5 declarations on base vs 3 on fixed, and the diff names exactly the two that disappear (route: 'GET /api/v1/dollar' and route: 'GET /api/v1/dotted'). So the live byte-identity is a reading, not a blind scan.\n\nWHY FREE - re-derived, NOT inherited from #11634: 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 any non-word char. Positive control: the same tally reports $ and '.' as admitted and a word char as blocked the moment a fixture carries them (verified before trusting the zero).\n\nSUBSET PROOF (the 'anchoring only ever REMOVES' invariant, swept not argued): over code points 0..0x2FFF the old word-boundary anchor admits 12225, the new anchor admits 12223, and the new anchor admits 0 that the old one did not. Exactly 2 characters move ($ and .). Both numbers pinned in --self-test.\n\nABLATION (reverse verification). NO REBUILD LEG EXISTS AND NONE IS CLAIMED: affected-docs.mjs is run directly from source by node, there is no dist/ in its path, so ablation-dist-preflight does not apply. Run on the COMMITTED fix, restore armed BEFORE the mutation via `trap '<restore>' EXIT INT TERM`. MUTATION CONFIRMED ON DISK by counting injected and removed text SEPARATELY, anchored on the exact text meant to move (never a bare diff --stat, and never the editor's exit code): 'ON DISK: injected=1 (want 1)  removed=0 (want 0)', with CLEAN_HASH=15da4f14a5971922e38c2f60874b60c220f07c85 -> MUTATED_HASH=e6b954a887cff518da2190e8b286e84a298b2188 -> RESTORED_HASH=15da4f14a5971922e38c2f60874b60c220f07c85 (restore byte-identical). The script aborts with exit 9 if the mutation does not land, so a no-op ablation cannot read as a result.\nDIRECTION PREDICTED BEFORE RUNNING: turn red on the new pins, with a NAMED set of controls staying green. OBSERVED exactly that: ABLATED_SELFTEST_EXIT=1, 16 checks fail, each 'got' value is the before state (e.g. row count expected 1 got 2; window 'got \"2 row - GET /api/v1/meta -> null | GET /api/v1/gone -> meta.getTypes\"'; binding got 'wrong.binding'; declined got '1 row / 2 route / 1 declined'; brokenScan got 1; outsideCode got 1; the declarationsIn pin got '1 row / 2 route / 1 declined'; the ternary pin expected 1 got 2; 'characters it moves are exactly $ and .' expected 2 got 0). Controls green BOTH ways, as predicted: the '-' and Unicode residue pins, brokenScan 0 and outsideCode 0 on the $route: fixture (that IS the silence the card is about), 'no call site restates the anchor' (0 both ways), and 'admits nothing the old anchor did not' (0 both ways). Ablated row identity also captured: both populations hash identically to base and fixed under ablation, making 'free on today's tree' a three-way measurement rather than a two-way one.\n\nNON-VACUITY the other way: the genuine route: 'GET /api/v1/meta' is still read, still carries meta.getTypes, and the 269 live rows are unchanged.\n\nSELF-TEST PIN CAUGHT ME ONCE, honestly reported: my first draft of the subset-sweep probe compiled declLead directly and tripped the 'all eight lead scans, none inline' pin (expected 8, got 9). Rebuilt it through a named intermediate - the convention the file's own key-spelling check already uses - rather than bumping the pin to 9, which would have blunted the pin that exists to catch a real ninth scan. A second iteration found the pin also counts a COMMENT quoting the compiled spelling, so the comment describes it instead of quoting it.\n\nCHANGESET: none, checked against the ACTUAL rule rather than inherited. changeset-check in .github/workflows/pr-automation.yml has NO path exemption - the skip-changeset label (or the changesets release PR) is the only route. Verified scripts/ publishes nothing: root package private:true, no 'files' field, scripts/ in no pnpm-workspace.yaml glob, no package.json of its own. Label applied and READ BACK: ['size/m','skip-changeset']. NOTE: the additive POST endpoint was blocked by the harness permission classifier, so I used the DECLARED FALLBACK (read -> union -> whole-set write) and read back to confirm.\n\nSANITIZER DISCIPLINE: the lookbehind is spelled ONLY in the source docblock and the --self-test boundary comment, never in the PR body or in any issue body. PR body scanned before submission: 0 occurrences of the less-than+bang digraph, 0 control bytes. Closing-keyword scan on the PR body: 'Fixes #11630' is the ONLY keyword+number pair (it also mentions #11542/#11584/#11634, none preceded by a closing keyword). Commit message carries no card-relationship trailer, per squash discipline - the relation is declared once in the PR body.",
      "open_questions": [
        {
          "question": "This family has now shrunk one character class per card (#11494 the colon run, #11542 the word boundary, #11630 $ and . ) and #11711 records the next residue. Is the widening chain the right end state, or should all eight scans move to a KEY ALLOWLIST (route/client and nothing else, matched whole)? #11630's own card raised this as its option 3 and flagged it as larger than either option it took. NOT blocking - #11630 is implemented and green.",
          "options": [
            "A - keep the lookbehind chain: each card is small, provably free, and priced at row identity; the residue is named and pinned each time so no card finds no pin.",
            "B - retire the family with a key allowlist matched whole: one change, closes $ / . / Unicode / '-' / every future escapee at once, but it is a single larger population move across all eight scans and needs its own before/after.",
            "C - leave #11711 unfixed indefinitely and stop shrinking: zero live instances have ever been found for any of these classes."
          ],
          "recommendation": "B is the right end state and A is the right next step. The measurement that decides it: all 499 live leads are preceded by a space, so an allowlist is provably free on today's tree by the same argument each lookbehind card used - which means B is affordable NOW and gets cheaper the sooner it lands, since every further lookbehind card is work that B discards. But B is a maintainer call, not a dev call: it retires a family rather than shrinking it, and #11630's card explicitly declined to assume it away. Recommend queueing B as its own card and NOT dispatching #11711 until that is decided, since B subsumes it."
        }
      ],
      "out_of_scope_findings": [
        "filed as #11711: a UNICODE identifier character still mints a silent phantom row - \\w is ASCII-only, so the new key lookbehind does not reach `eroute:`; no live instance (0 of 499 live leads), pinned in --self-test as deliberately unmoved, labelled `finding` + `tooling`, unassigned, no pm:queue"
      ]
    }

    Generated by Claude Code

  6. os-steve commented on Aug 24, 2026

    @os-steve
    CollaboratorAuthor

    Landed — PR #11710, verified by content on origin/main

    5f124a219  fix(docs-audit): widen the ledger key anchor from \b to a negative lookbehind (#11710)
    
    scripts/docs-audit/affected-docs.mjs
      function declLead(keys) { return String.raw`(?<[\w$.])${keys}\s*:\s*`; }
      "deliberately unmoved" pins   2 -> 4      (one flipped, two added; nothing deleted)
    

    Card closed completed. pm:dispatched stripped.

    ⭐ This card named the wrong character class, and the fix is better for it

    This report — and declLead's own docblock — pointed at dottedRe's set (word chars + $). The run read all three lookbehind idioms in the file and found they exclude three different classes:

    symbolRe   (?<[\w$.])     word chars + $ + .     ← the right analogue: declLead's key is a BARE token
    dottedRe   (?<[\w$])      word chars + $         ← what this card named
    

    ⭐ And measuring it turned up a second escapee nobody had named: a preceding . makes the token a member access whose colon belongs to a ternary —

    cond ? obj.route : 'X'
    

    — minting the same phantom row through a different piece of syntax. A fix built to this card's spec would have shipped with that still open. Closed and pinned.

    - was deliberately left out with the reason stated: a-route is two tokens, so route genuinely is the whole token — it is not a declaration for a different reason, one it shares with the bare cond ? route : x that no lookbehind can reach. Declining a case with a reason beats closing it by reflex.

    The pricing standard this card set was met, on both populations

    • Population 1 (ledger rows), at row identity: --bridge-coverage --json hashed d04a5ced… on base, ablated and fixed; set difference 0 in each direction.
    • Population 2 (declarationsIn) — the one docs-audit: subroute: / myclient: mints a silent PHANTOM ROW — only declarationsIn anchors the key with \b, the other seven scans do not #11542's before/after was deliberately priced to leave byte-identical, and the reason this card was filed separately rather than folded in. Measured by slicing declarationsIn plus its dependency closure — walked, not hand-listed — and running the file's own bodies over the seven live ledgers: 491 declarations, hash identical on all three trees.
    • ⭐ The positive control ran BEFORE the zero was believed: over a synthetic ledger carrying both escapees the same harness reports 5 vs 3 and names exactly the two that disappear. That ordering is the whole difference between a measurement and a hope.
    • "Free on today's tree" re-derived, not inherited: 499 live leads, every one preceded by a space — 0 by $, 0 by ., 0 by any non-word char, with its own control.
    • ⭐ The subset invariant was swept, not argued: over code points 0..0x2FFF the old anchor admits 12225, the new 12223, and the new admits 0 the old did not. Exactly two characters move. Both numbers pinned.

    The self-test caught the implementer, and the response was right

    The first probe draft compiled declLead directly and tripped the "all eight lead scans, none inline" pin (expected 8, got 9). It rebuilt the probe rather than bumping the pin to 9 — bumping would have gone green in a minute at the cost of blunting the only thing that can catch a real ninth scan. Reporting that unprompted is why the rest of the report reads as credible.

    Follow-on

    This card's option 3 — a whole-token key allowlist retiring the family rather than shrinking it once more — is now #11717, filed with #11630's own measurement as the affordability argument (499/499 leads preceded by a space ⇒ an allowlist is free on today's tree by the same reasoning every lookbehind card used). #11711 (the Unicode residue, \w being ASCII-only) is pm:blocked behind it as subsumed.

    ⚠️ For the record: the implementer flagged the allowlist choice as a maintainer call. It is not — it is an implementation-approach choice on this lane's own surface with the deciding measurement already taken, so this seat made it rather than adding it to the maintainer's queue.


    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

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions