Skip to content

Commit 3f273a8

Browse files
refactor(scripts): register platform-checklist as a symbol-anchor corpus instead of forking the grammar (#18588)
Fixes #18107 Clause-②: no The platform checklist reused the shared resolver's **rule** after #16898 (PR #18100) but was still not a registered **corpus**: `defineCorpus`, `sweepCorpus` and the shared `extractAnchors` all read **0** in `scripts/check-platform-checklist.mjs`, while it carried its own `ANCHORABLE_EXTENSIONS` (4), `SYMBOL_ANCHOR` regex (6) and `findSymbolAnchors` (3). The 2026-09-01 ruling this module reproduces says a corpus joins by registration and, where the core falls short, 「**widen the core**」. This does that. ## ⚠️ Two sentences that are both true, and must not be collided #18100's own error text forbids widening `scripts/symbol-anchors.mjs` to make a checklist citation green, and **that instruction still stands and is not touched here**. It governs the **resolution rule** — what counts as a symbol being present in the cited file. This PR widens the **grammar and the population**: how a citation is *written*, and which documents are *swept*. Two different surfaces: | surface | who decides | moved here? | |---|---|---| | what counts as the symbol being PRESENT | `symbolResolutionClass`, shared, one implementation | ⛔ **no** — untouched | | how a citation is WRITTEN, which documents are SWEPT | corpus data on `defineCorpus` | ✅ yes | Not one anchor resolves here that did not resolve before. The residual ledger is not grown — it is byte-identical, all 56 rows still fire. ## ⭐ The card's stated obstacle was not the real one — measured The card located the blocker as `defineCorpus`'s walker recursing into `runs/`. That is *a* blocker, and it is fixed. It is not the one that mattered. Measured on `origin/main` @ `4bd2c60e81`, over the 20 family files, in this worktree (`/home/user/objectstack-issue-18107`): | detector | symbol anchors found | |---|--:| | the forked local `findSymbolAnchors` | **634** | | the shared `extractAnchors` (spanned grammar) | **8** | Every citation in `areas/*.json` lives inside a JSON string value, where a backtick is payload and not a markdown code span. That **626-anchor gap**, not the `runs/` walker, is what had kept this corpus forked. Registering it therefore needed two core widenings, not one. ## What was widened in `scripts/symbol-anchors.mjs` Both are corpus declarations, **default-off**, each with its own default-off control in the battery: - **`unspannedAnchors: true`** — the anchor written without a code span. Unspanned, the path must carry a directory, its first character may not be `@`, and the left boundary refuses an identifier character, `@`, `-` or `/`. Every `#`-shaped neighbour a data ledger is full of is pinned by name: an issue reference, a JSON-pointer fragment, an ADR section reference, a scoped package specifier, a hyphenated word with a tail, a bare filename with no directory. - **`excludeDirs: ['runs']`** — a directory **name** the walker does not descend into, at any depth. `docPattern` is a regex on the basename and cannot see a directory, so this shape had no spelling. `defineCorpus` refuses an entry carrying a separator rather than let a path-shaped one match nothing. Plus one additive grammar change: a dotted `#Outer.member` is now the shared grammar's, and `sweepCorpus` requires **every** segment to resolve. This was the checklist's own capability; it moved into the core rather than being dropped with the fork. Additive because a dotted symbol could not be written inside a code span at all before — the span has to close right after the symbol. `sweepCorpus` also returns `byDoc` (the same counts per swept document), which is what a per-file floor ratchets: a total stays put while one file is emptied and another grows. ### Proof the widening did not move the already-registered corpora Ran each gate on `4bd2c60e81` (comparison worktree) and on this branch, and diffed stdout+stderr: | gate | before | after | output | |---|---|---|---| | `check-adr-symbol-anchors` | exit 0 | exit 0 | **byte-identical** | | `check-scripts-symbol-anchors` | exit 0 | exit 0 | **byte-identical** | | `check-system-context-census` | exit 0 | exit 0 | **byte-identical** | Measured separately before writing the dotted widening: 0 new dotted-span anchors across `docs/adr` (139 files), `scripts/**` (261 files through `commentProse`) and the system-context page. ## What was deleted from `scripts/check-platform-checklist.mjs` `ANCHORABLE_EXTENSIONS` (the 8-member strict subset), `SYMBOL_ANCHOR`, `findSymbolAnchors` and `absentAnchorSegments`. `CORPUS` is a `defineCorpus` call; the sweep is one `sweepCorpus` call. What stays local is what is genuinely this corpus's own — the #16898 residual ledger (a closed one), its grow-never ceiling, and the per-file shrink-never floor — all keyed off what the sweep reports. Three new pins the drift had no watcher for: - **V1** — the anchorable-extension vocabulary this gate names IS the shared **object**, compared by identity: a copied array would satisfy an equality and drift again the next day. - **V2** — a source read saying this file defines no extension set, no anchor regex and no detector. Names match as a **prefix with any suffix allowed**; ablation showed the first spelling (exact name) let a `…_SHADOW` redefinition through. The residual gap — a fork under a name sharing none of these tokens — is stated in place, not implied away. - **V3** — the control for V2: the same read DOES find the registration, so a green above is "no fork" and not "the read returned nothing". ⛔ Every re-judged #16898 case (`P3`, `P3b`, `P5`→`N5`, the `R`/`N` blocks) **survives the transplant unchanged in verdict**, per that card's own rule that deleting one would leave the tree unable to say the narrowing happened. The 25 detector cases moved into `symbol-anchors`' own battery, whose floor rose 93 → 122; this gate's rose 40 → 42. ## ⚠️ The verdict change, enumerated — it is ONE cell The census is identical across 17 of 17 family files but one: ``` "README.md": 2 → 1 ``` Corpus-wide: **578/634 → 577/633 resolved**, 56 on the residual (unchanged), 17 file floors held. The one anchor is `packages/core/src/security/platform-admin.ts#parsePlatformAdminEmails` at `README.md:172` — inside a ```` ```text ```` fence, where the README **documents how to write an anchor**. The forked `matchAll` judged its own worked example; the shared extractor treats fenced content as quoted material. That is a **true positive for the new behaviour** and the same discipline `symbol-anchors`' header states for itself ("a gate header that teaches the grammar must not fail it"). `README.md`'s floor is 1 and now sits exactly at it, with zero headroom. Each anticipated change, measured rather than assumed: | anticipated | measured effect | |---|--:| | extensions 8 → 23 | **0 anchors** — the live population is `ts` 622, `json` 9, `mjs` 3, inside both sets | | fences now skipped | **1 anchor** (the README's own example, above) | | `anchor-exempt` now honoured | **0** — no marker exists anywhere in the family | | `existsSync` → git-`tracked` | **0** — no target is untracked, and none is missing | ⛔ Nothing was narrowed to reach green, no floor was lowered, and no row joined the residual. ## 📎 One check became unreachable — stated, not buried The deleted detector matched any `[A-Za-z]{1,4}` extension and then **errored** on one outside its 8 (`UNRESOLVABLE ANCHOR`). Under the shared grammar a path whose extension is not anchorable is simply not an anchor, so an author writing a `#symbol` on, say, a `.txt` now gets silence where they used to get a red. Measured: that error fires **0** times on today's population and could not have fired, since all 634 anchors are `ts`/`json`/`mjs`. It is a narrow but real loss of author feedback, it is not something this PR should reintroduce as a local matcher (that is the fork), and it is reported for triage rather than fixed here. ## Testing - `pnpm check:platform-checklist` — **exit 0**, `577/633 resolved … across 20 swept documents against 309 cited sources, 56 on the named #16898 residual, 17 file floors held` - `node scripts/check-platform-checklist.mjs --self-test` — **exit 0**, 189 assertions (42 in the symbol-anchor battery) - `node scripts/symbol-anchors.mjs --self-test` — **exit 0**, 122 cases - **31 of 32** derived gate families green; the 32nd (`check:entry-guard`) caught a real defect in this PR — `export const CORPUS` on a file whose top level runs the gate — fixed in `3683a4b9e` and re-run green. ### Ablation — every new check was made to fail before its green was trusted 11 mutations. Each one: proved it landed on disk (anchor-text count before/after **and** injected-text count), ran, restored with `git checkout HEAD --`, and verified the restore byte-identical by `git hash-object` against the HEAD blob plus a clean `git diff HEAD`. | # | mutation | result | |---|---|---| | M1 | the walker ignores `excludeDirs` | core self-test red — `swept [...kept.md, ...runs/record.md]` | | M1b | same, seen by the gate battery | red — X1 and X3 | | M2 | `sweepCorpus` stops forwarding `unspannedAnchors` | gate red — 70 problems | | M3 | the unspanned pass stops de-duplicating against spans | core red — spanned anchor counted twice | | M4 | only the head dotted segment is resolved | gate red — R3, P3 | | M5 | the residual ledger matches nothing | gate red — 123 problems | | M6 | the corpus population drifts from `familyFiles` | gate red | | M7 | a private detector function comes back | red — V2 | | M7b | a `_SHADOW`-suffixed extension set comes back | ⚠️ **passed** at first — the pin was too loose; tightened, then red | | M7c | the shared vocabulary replaced by a copy of itself | red — V1 and V2 | | M8 | `familyFiles` stops excluding `runs/` | red — L2, `20 swept vs 21 walked` | | M10 | the V2 source read made to return nothing | red — V3 | The `runs/` exclusion is proved in **both directions twice over**: on a synthetic fixture in the core, where the *same* unresolvable anchor sits in an excluded and an included subtree (excluded → no finding, included → one finding, and a second sweep with no `excludeDirs` finds both); and on the **live corpus** in this gate, where `runs/` is asserted to hold files at all (X0 — without it the exclusion could be a no-op forever), to be swept by nothing (X1), with the areas beside it still swept by the same call (X2), and dropping the exclusion putting those documents back (X3). ## Changeset `skip-changeset` — measured, not assumed. Of **75** package manifests, **0** name `scripts/symbol-anchors.mjs` or `scripts/check-platform-checklist.mjs` in `files[]`; the root manifest is `private: true` with no `files`. Firing control on the same reading: **70** manifests ship `CHANGELOG.md` in `files[]`, so the reflex that "docs and scripts are always exempt" is not what is being relied on here. ## ⚠️ Reach `pnpm check:platform-checklist` is **not CI-wired**, by a standing maintainer decision recorded in `lint.yml`; the visibility gap is #11730. A red from this gate reaches whoever runs it, not every PR. `check:scripts-symbol-anchors` and `check:entry-guard`, which this diff also moves, *are* wired. ## Acceptance notes - `docs/qa/platform-checklist/README.md` now sits exactly at its anchor floor of 1, with zero headroom: the next citation re-authored out of that file reds the gate. Noted, not filed — the next PR to touch that README is the one that meets it. - The `UNRESOLVABLE ANCHOR` check that became unreachable (above) is reported for triage as a finding. - `#18101` is not addressed here and `#18104` is not addressed here; both remain open. The `accept-set` and `bad-citation` halves of the residual are untouched by this PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk --- _Generated by [Claude Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b103baf commit 3f273a8

2 files changed

Lines changed: 685 additions & 236 deletions

File tree

0 commit comments

Comments
 (0)