Skip to content

Commit 9b00f9f

Browse files
claude[bot]claude
andauthored
gate(checklist): bind the platform-checklist corpus to the ONE symbol resolver (#16898) (#18100)
Clause-②: no Closes #16898 `scripts/symbol-anchors.mjs` states the #13556 ruling it implements in its own header, verbatim: **"One resolver."** The platform-checklist corpus resolved symbol anchors with a rule of its own — mask comments, then ask whether the bare token appears anywhere in the file — in exactly the place that ruling says there is to be one implementation. It was the **looser** of the two, which is the dangerous direction: a second resolver that is greener than the shared one is never the resolver anybody points at, so the drift is only ever found by census. ## What changed `check-platform-checklist.mjs#absentAnchorSegments` now hands every segment to `scripts/symbol-anchors.mjs#symbolResolutionClass` and holds **no matcher at all** — no regex, no `maskComments`, nothing. The per-segment split of a dotted anchor stays, because that is this corpus's anchor *grammar* (the shared `#symbol` carries no dot), not a rule about what "present" means. A self-test assertion (N5) reads this file's own source to pin the body against re-forking. ⛔ `scripts/symbol-anchors.mjs` is **untouched**. Widening the shared core to keep this ledger green would export the looser reading to the ADR, `scripts/**`, spec-docblock and system-context corpora — the exact failure the ruling exists to prevent. ⛔ No floor is lowered, and the maintainer-only `scripts/checklist-symbol-anchor-baseline.json` is **not edited at all**. ## The measurement — anchors ceasing to resolve IS the deliverable | | anchor occurrences | | --- | --- | | scanned | 633 | | resolved under the **withdrawn permissive rule** | 633 | | resolved under the **shared resolver** | **577** — 516 `declaration`, 61 `literal` | | **stopped resolving** | **56** (55 distinct `(family file, anchor)` pairs, 54 distinct anchors) | That 56 is coverage this checklist was reporting and did not have. It is carried in `SHARED_RESOLVER_RESIDUAL`, one row per `(family file, anchor)`, each with the shape that used to resolve it and a verdict — **not** absorbed by a rule and **not** absorbed by a lowered floor. The residual is a **closed ledger, not an allow-list with a shape**: - every row must **fire**; a row whose anchor resolves again or has gone is a `STALE RESIDUAL ROW` red that names it and asks for its deletion, so the ledger cannot outlive the defect it records; - an anchor that fails to resolve and is **not** on the list is an ordinary `ABSENT SYMBOL` red — new bad citations cannot join silently; - a grow-never ceiling sits beside it, so rows leave by **repair**, never by raising a number. The floor population is now `resolved + residual` and the gate's own output prints both halves, so the coverage claim is legible rather than folded: ```text symbol anchors: 578/634 resolved by `symbol-anchors.mjs` (the ONE resolver) against 309 cited sources, 56 on the named #16898 residual, 17 file floors held ``` (578/634 rather than 577/633 because the README rewrite below adds one anchor, which resolves.) ## Every broken anchor, classified Acceptance 2. `bad-citation` = the cited file does not declare the symbol; the repair is in the ledger (re-point the anchor, or drop to a bare citation). `accept-set` = the anchor names something real the **shared** accept set does not reach ⇒ **a different card against `scripts/symbol-anchors.mjs`**, and ⛔ nothing here widens it. ### verdict: `bad-citation` — 47 rows **`string-substring`** (29) — symbol survives only INSIDE a longer string token | anchor | family file | | --- | --- | | `examples/app-showcase/src/data/seed/index.ts#Specimen` | `areas/records-forms.json` | | `packages/mcp/src/plugin.ts#OS_MCP_SERVER_ENABLED` | `areas/ai.json` | | `packages/objectql/src/overlay-precedence.test.ts#not_overridable` | `areas/studio-authoring.json` | | `packages/platform-objects/src/identity/sys-oauth-application.object.ts#OAuth` | `areas/identity-auth.json` | | `packages/plugins/plugin-auth/src/auth-plugin.ts#Providers` | `areas/platform-core.json` | | `packages/plugins/plugin-auth/src/auth-route-ledger.ts#bootstrapStatus` | `areas/identity-auth.json` | | `packages/plugins/plugin-auth/src/auth-route-ledger.ts#linkSocial` | `areas/identity-auth.json` | | `packages/plugins/plugin-auth/src/auth-route-ledger.ts#revokeOthers` | `areas/identity-auth.json` | | `packages/plugins/plugin-auth/src/auth-route-ledger.ts#sendVerificationEmail` | `areas/identity-auth.json` | | `packages/plugins/plugin-auth/src/auth-route-ledger.ts#setActive` | `areas/identity-auth.json` | | `packages/plugins/plugin-auth/src/auth-route-ledger.ts#updateUser` | `areas/identity-auth.json` | | `packages/qa/dogfood/test/membership-role-vocabulary.dogfood.test.ts#PermissionSet` | `areas/identity-auth.json` | | `packages/qa/dogfood/test/package-first-authoring.dogfood.test.ts#writable_package_required` | `areas/platform-core.json` | | `packages/rest/src/meta-write-actor-identity.test.ts#Actor` | `areas/studio-authoring.json` | | `packages/rest/src/rest-route-ledger.ts#describeDelegableScope` | `areas/identity-auth.json` | | `packages/rest/src/rest-route-ledger.ts#getHistory` | `areas/studio-authoring.json` | | `packages/rest/src/rest-route-ledger.ts#jobId` | `areas/records-forms.json` | | `packages/rest/src/rest-route-ledger.ts#REST` | `areas/api-backend.json` | | `packages/rest/src/rest-route-ledger.ts#REST` | `areas/studio-authoring.json` | | `packages/rest/src/rest-route-ledger.ts#saveItem` | `areas/access-security.json` | | `packages/rest/src/rest-route-ledger.ts#shareId` | `areas/access-security.json` | | `packages/runtime/src/route-ledger.ts#commitId` | `areas/platform-core.json` | | `packages/runtime/src/route-ledger.ts#getLegalNextStates` | `areas/api-backend.json` | | `packages/runtime/src/route-ledger.ts#getRuntimeStatus` | `areas/automation.json` | | `packages/runtime/src/route-ledger.ts#getScreen` | `areas/automation.json` | | `packages/runtime/src/route-ledger.ts#runId` | `areas/automation.json` | | `packages/spec/src/data/object.zod.ts#FEEDS_DISABLED` | `areas/records-forms.json` | | `packages/spec/src/data/object.zod.ts#query` | `areas/records-forms.json` | | `packages/triggers/trigger-api/src/trigger-api-route-ledger.ts#flowName` | `areas/api-backend.json` | **`import-only`** (9) — cited file IMPORTS the symbol; the declaration is elsewhere | anchor | family file | | --- | --- | | `examples/app-showcase/objectstack.config.ts#ConnectorRestPlugin` | `areas/automation.json` | | `examples/app-showcase/src/security/seed-approval-demo.ts#AUDITOR_DEMO_USER` | `areas/approvals.json` | | `examples/app-showcase/src/security/seed-approval-demo.ts#PHONE_DEMO_USER` | `areas/identity-auth.json` | | `packages/cli/src/commands/compile.ts#emitJson` | `areas/cli.json` | | `packages/cli/src/commands/doctor-deprecation-hint-commands.test.ts#Doctor` | `areas/cli.json` | | `packages/cli/src/utils/format.exit-code.test.ts#emitJson` | `areas/cli.json` | | `packages/platform-objects/src/identity/sys-member.object.ts#BUILTIN_MEMBERSHIP_ROLE_OPTIONS` | `areas/identity-auth.json` | | `packages/runtime/src/domains/ai.ts#capabilityUnavailable` | `areas/ai.json` | | `packages/services/service-messaging/src/messaging-service.ts#PreferenceResolver` | `areas/integration-system.json` | **`json-value-not-key`** (3) — JSON target carries it as a VALUE; the shared rule reads KEYS | anchor | family file | | --- | --- | | `packages/create-objectstack/src/templates/blank/package.json#objectstack` | `areas/cli.json` | | `packages/spec/liveness/email_template.json#requireVars` | `areas/integration-system.json` | | `packages/spec/liveness/field.json#live` | `areas/attachments-storage.json` | **`regex-literal`** (1) — symbol survives only inside a regex literal | anchor | family file | | --- | --- | | `packages/verify/src/verify.ts#VALIDATION_FAILED` | `areas/cli.json` | **`detector-artifact`** (1) — not a citation at all: THIS gate's detector truncated an item-id at its first hyphen | anchor | family file | | --- | --- | | `docs/qa/platform-checklist/areas/access-security.json#access` | `areas/identity-auth.json` | **`member-access`** (3) — symbol survives only as `x.symbol` on another object | anchor | family file | | --- | --- | | `packages/objectql/src/engine.ts#objectExtensions` | `areas/platform-core.json` | | `packages/plugins/plugin-security/src/security-plugin.ts#__referentialFieldClear` | `areas/identity-auth.json` | | `packages/runtime/src/domains/notifications.ts#markRead` | `areas/platform-core.json` | **`local-binding`** (1) — a parameter name (plus a hyphenated string sharing the token) | anchor | family file | | --- | --- | | `packages/lint/src/validate-action-locations.ts#action` | `areas/records-forms.json` | ### verdict: `accept-set` — 8 rows **`inline-key`** (6) — a real object-literal key, written INLINE rather than at the start of a line | anchor | family file | | --- | --- | | `examples/app-showcase/objectstack.config.ts#declarativeStdio` | `areas/integration-system.json` | | `examples/app-showcase/src/data/objects/business-unit.object.ts#allowCreate` | `areas/records-forms.json` | | `examples/app-showcase/src/data/seed/index.ts#sales_region` | `areas/dashboards.json` | | `examples/app-showcase/src/data/seed/index.ts#signed_on` | `areas/dashboards.json` | | `examples/app-showcase/src/system/datasources/showcase-external.datasource.ts#onMismatch` | `areas/integration-system.json` | | `examples/app-showcase/src/ui/actions/index.ts#maxSize` | `areas/records-forms.json` | **`dotted-string-head`** (2) — a DATA identifier that is the HEAD SEGMENT of a dotted string token | anchor | family file | | --- | --- | | `packages/spec/src/kernel/public-auth-features.ts#sys_invitation` | `areas/identity-auth.json` | | `packages/spec/src/kernel/public-auth-features.ts#sys_user` | `areas/identity-auth.json` | ⭐ The single sharpest row is the `detector-artifact`: `docs/qa/platform-checklist/areas/access-security.json#access`. There is no such anchor — **this gate's own detector** truncated an item-id reference (`…json#access-security.scope-depth-asymmetry`) at its first hyphen, and the permissive rule then "resolved" the phantom `#access` against the spelling `access-security` sitting in the file. A looser second resolver does not merely miss rot; it manufactures green for citations that were never written. ## Does the third corpus diverge? — No. (Acceptance 3, with the reading) The card counted **three** registered corpora when it was filed. On `main` today there are **four**, and the fourth arrived after the card (#17241, `6aa1d09043`). All four were read: | gate | registration | its own resolution rule? | | --- | --- | --- | | `scripts/check-adr-symbol-anchors.mjs` | `defineCorpus` + `sweepCorpus` | none | | `scripts/check-scripts-symbol-anchors.mjs` | `defineCorpus` + `sweepCorpus` | none | | `scripts/check-spec-docblock-symbol-anchors.mjs` | `defineCorpus` + `sweepCorpus` | none | | `scripts/check-system-context-census.mjs` | `defineCorpus` + `sweepCorpus` | none — and it additionally calls `symbolResolutionClass` **directly** for its own `NON_READ_ANCHORS` ledger rows, i.e. delegation, not a fork | Mechanical readings behind that table: - `grep -ln "defineCorpus({" scripts/*.mjs` ⇒ the four gates above plus `scripts/symbol-anchors.mjs` (the definition). - `grep -ln "symbolResolutionClass" scripts/*.mjs` ⇒ `check-system-context-census.mjs`, `isystem-census.mjs`, `symbol-anchors.mjs` — all consumers, no re-implementation. - No local symbol-presence predicate exists in any of the four (no `function …Symbol…` / `…Resolve…` / `absent…` / `declares…` definitions beyond their `assert` helpers). ⇒ **The shape of the fix is unchanged.** This is "one corpus went its own way", not "the shared resolver was never really adopted" — the only divergence in the tree was `check-platform-checklist.mjs`, and this PR closes it. ⚠️ One honest residual on that question: this corpus is now bound to the shared **resolution rule**, but it is still **not** a `defineCorpus` registration — its anchors live in JSON string values rather than markdown code spans, and its `#symbol` may be dotted where the shared grammar's may not. Full registration would need the shared core widened on both counts, which is precisely what this card forbids doing here. Recorded below as an unsettled question for the PM. ## Positive control (Acceptance 4) ⛔ Without it, "bound correctly" and "everything broke" are indistinguishable. Three independent readings, all on this branch: 1. **At scale, live.** 577 of 633 anchors still resolve — 516 as `declaration`, 61 as `literal`. A binding that simply refused everything would have reported 0. 2. **In the self-test, on one fixture.** `N4` asserts a declaration (`export function declaredHere`) **and** a complete quoted token (`'whole_token'`) both still resolve, in the same fixture where `N1`/`N2`/`N3` assert that a call site, a local parameter and a substring of a longer string token do not. 3. **The control is load-bearing, proved by ablation.** Re-forking the resolver (below) fires `P3`, `P5`, `N1`, `N2`, `N3`, `N5` — and leaves `N4` **silent**, because the permissive rule also accepts a declaration and a whole token. That is exactly why `N4` alone cannot detect a fork and why it has to sit beside `N1`–`N3` rather than instead of them. ## Ablation — four legs, each with an on-disk mutation proof and a hash-settled restore ⭐ ⛔ No restore relies on `trap` (measured to silently not fire in this container, #17875). Every restore is `git checkout HEAD -- PATH` — never bare — settled by `git hash-object` equality against the `HEAD` blob **and** an empty `git diff HEAD`, never by an exit code. Every mutation is proved to have reached disk by a before/after `grep -c` on the exact anchored text **and** a blob-hash change, before any verdict is read. `HEAD` blob for `scripts/check-platform-checklist.mjs`: `7be4b5ddef7f08f3fc02cc1ffe86626f80ace9c4`. | leg | mutation | on-disk proof | result | | --- | --- | --- | --- | | A | put the permissive token match back into the resolver body | anchor `1 → 0`, injected text `×1`, blob `7be4b5d… → b1a9d29…` | gate **exit 1**, resolver positive control FAILED: `P3 P5 N1 N2 N3 N5` — `N4` silent | | B | delete one residual row (`…/plugin.ts#OS_MCP_SERVER_ENABLED`) | row `1 → 0`, blob changed | gate **exit 1**, 1 × `ABSENT SYMBOL` naming that anchor — an unlisted failure still reds | | C | append a 56th residual row | bogus row `×1`, blob changed | gate **exit 1**, grow-never ceiling refuses (`D3`) — the ledger cannot grow | | D | re-point one residual row at an anchor that resolves | old `1 → 0`, new `×1`, blob changed | gate **exit 1**, 1 × `STALE RESIDUAL ROW` + 1 × `ABSENT SYMBOL` — a row that stops firing is a red | All four restores printed `RESTORE: SETTLED — blob == 7be4b5d AND git diff HEAD empty`. Tree verified clean afterwards; ⛔ no ablation artefact is committed. ## Tests pinning the permissive behaviour — re-judged in place, ⛔ never deleted Two self-test assertions asserted a resolution only the permissive rule could produce. Both are kept, with the verdict inverted and the reason recorded in a comment block above them, because deleting them would leave the tree unable to say the narrowing ever happened. Each keeps the direction it really covered, on a shape the shared rule does accept: | assertion | was | is now | direction preserved as | | --- | --- | --- | --- | | `P3` | `RESOLVED.mode` resolves — `{ mode: 'strict' }` written INLINE | that member is **ABSENT**; the shared rule takes a member key at the **start of a line** | `P3b`, on a member declared at the start of a line | | `P5` | a `.json` **value** resolves, because JSON was scanned unmasked | a `.json` value is **ABSENT**; the shared rule reads JSON **keys** | `P5b`, which resolves a key while a `//`-carrying string value sits beside it | ⚠️ `P3`'s shape is not incidental — six of the eight `accept-set` residual rows are that same inline-object-literal-key shape, so `P3` now pins the very narrowness the follow-up card is about. New assertions: `P3b`, `P5b`, `N1`–`N5` (the binding and its positive control), `D1`–`D4` (the residual ledger, both directions). The `BATTERY_SYMBOL_ANCHORS` floor is ratcheted `29 → 40`; the file's self-test total is `176 → 187`. ## Reverse-read: which existing sentence does this make false? Sentences carrying a rev or a date are history and were left alone; what rots is a bare present-tense claim. Both directions were checked. **Made false — fixed in this PR (1):** 1. `docs/qa/platform-checklist/README.md`, the author-facing statement of the rule. Three claims went false at once: *"A symbol inside a string literal counts as present"* (now: a **complete quoted token** only), *"This is presence-in-code, not a declaration analysis"* (it now **is** one, plus the literal class), and the framing that `check:platform-checklist` decides the question at all. Rewritten to state that the rule belongs to the shared module, to name what an author actually gets (comments stripped, declaration, complete quoted token — and **nothing else**), and to say that an anchor the core refuses is either a bad citation or a card against that module. **Made false — reported, ⛔ NOT edited here (1):** 2. `scripts/checklist-symbol-anchor-baseline.json`, its `$comment`: *"Each entry is the count of anchors that RESOLVED in that family file."* An entry is now `resolved + residual`. ⛔ Not edited: #16898 states that file is maintainer-only by its own `$authority` line, and the safe reading of "maintainer-only" is the whole file, not just the numbers. **No floor is changed either way.** The gate's own `--anchor-census` output now says the per-file counts are the FLOOR population and not a coverage figure, so the honest reading is available from the gate even while the stale sentence stands. ⇒ **a one-line maintainer edit, not this PR's to make.** **Reverse direction — a sentence that was false and is now closer to true, ⛔ NOT edited (1):** 3. `scripts/symbol-anchors.mjs`, its ruling block: *"#13788 … reuses THIS module by registering a corpus."* That was simply **false** before this PR — the checklist reused nothing. It is now **half true**: the module's resolution rule is reused; a `defineCorpus` registration still does not exist, for the grammar reasons above. Left for the maintainer along with the follow-up card, since qualifying a quoted ruling is their call and the remaining gap is exactly that card's subject. **Zeros, reported as zeros:** no ADR sentence, no `content/docs/**` page and no other gate header states the checklist's resolution rule — `grep` over `docs/qa/platform-checklist`, `scripts/checklist-symbol-anchor-baseline.json` and `.github/workflows` returned the three items above and nothing else. No test outside `check-platform-checklist.mjs` asserts on this rule. ## Verification - `node scripts/check-platform-checklist.mjs` ⇒ **exit 0**, line quoted above. - `node scripts/check-platform-checklist.mjs --self-test` ⇒ **exit 0**, 187 assertions. - Derived gate family (`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, change set taken from git, not hand-listed): **34 families derived, 34 run, 0 NOT-MEASURED, 0 UNRUN**, reconciled with recorded exit codes via `--ran`. All 34 exit 0 — including `check:scripts-symbol-anchors` (this file's own comment prose is in that corpus), `check:comment-mask-corpus`, `check:nul-bytes`, `check:self-test-wired` and `check:declaration-mirrors`. One family needed `turbo run build --filter=@objectstack/formula --filter=@objectstack/lint` first and reported exit 3 (PREREQUISITE NOT MET, nothing measured) until it had it; it is green on the built tree. - Control-character sweep beyond the gate: `grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'` over both changed files ⇒ no hits. ## Changeset — measured, then judged: `skip-changeset` Nothing published moves. Measured rather than asserted: 82 `package.json` files enumerated, 70 publishable (non-private), and **no** publishable package's `files[]` reaches `scripts/` or `docs/qa/`. Positive control on the same enumeration: 70 of those 70 `files[]` blocks do mention `dist`, so the scan was reading real arrays and not an empty set. ⇒ the `skip-changeset` **label** is applied to this PR (a label, not a body sentence). ## Follow-ups this PR deliberately does not do 1. **Repair the 47 `bad-citation` rows.** #16898 says in as many words that how the findings land "is a sizing question this card does not answer", and floors have almost no headroom, so dropping a `#symbol` is not available and each row needs a re-point with judgement. Rows leave the ledger one repair at a time. 2. **Widen the shared accept set for the 8 `accept-set` rows** — an inline object-literal key (6) and the head segment of a dotted string token such as `sys_user` in `'sys_user.actions.invite_user'` (2). That is a card against `scripts/symbol-anchors.mjs`, weighed against all five corpora, ⛔ never a local widening. 3. **Register this corpus through `defineCorpus`**, which the ruling actually asks for and which needs the shared grammar to reach dotted symbols and non-code-span anchors. 4. ⛔ **#16821's accept set** is out of scope per triage ("correctly fenced") and was not touched. --- _Generated by [Claude Code](https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 82d8942 commit 9b00f9f

2 files changed

Lines changed: 344 additions & 51 deletions

File tree

‎docs/qa/platform-checklist/README.md‎

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -173,13 +173,31 @@ Why this shape:
173173
```
174174

175175
`check:platform-checklist` resolves every anchor: the file must be in this repo and the
176-
symbol must still be in it, **comments masked** — a symbol surviving only in a docblock
177-
is ABSENT, because prose about a symbol is not a symbol. A symbol inside a string
178-
literal counts as present: much of what this ledger cites *is* a string (capability
179-
names, error codes, `sys_*` machine names). Dotted anchors resolve segment by segment.
180-
This is presence-in-code, not a declaration analysis — what it catches is the rename or
181-
deletion that leaves the pointer naming nothing, which is exactly the rot the line
182-
numbers used to carry.
176+
symbol must still be **declared** in it. ⭐ The rule is **not this gate's** and not this
177+
paragraph's — it is the one shared resolver every anchor corpus in this tree uses
178+
(`scripts/symbol-anchors.mjs#symbolResolutionClass`), and that module's own
179+
`THE RESOLUTION RULE` block is the authority. What an author needs from it:
180+
181+
- **comments are stripped first** — a symbol surviving only in a docblock is ABSENT,
182+
because prose about a symbol is not a symbol;
183+
- a **declaration site** resolves (a `function` / `class` / `interface` / `type` /
184+
`const` / `let` / `var`, a member or object-literal key written **at the start of a
185+
line**, a named re-export). For a `.json` target that means a **key**, never a value;
186+
- a **complete quoted string token** resolves — `'sys_metadata'` — which is what keeps
187+
the machine names this ledger is full of anchorable (capability names, error codes,
188+
`sys_*` object and field names);
189+
- ⚠️ and nothing else does. A **call site**, an **import**, a **member access on some
190+
other object**, a **local parameter**, or the symbol appearing only as a **substring
191+
of a longer string** (`saveItem` inside `'meta.saveItem'`, `:shareId` inside a route
192+
pattern) is **NOT** resolution. If that is all the cited file has, the citation is
193+
naming the file that *uses* the symbol, not the file that *declares* it — re-point
194+
it, or cite the file bare.
195+
196+
Dotted anchors resolve segment by segment. ⛔ An anchor the shared resolver refuses is
197+
either a bad citation or a case for widening that shared core, and the second is its
198+
own card against that module — ⛔ never a local rule here: the core is shared with the
199+
ADR, `scripts/**`, spec-docblock and system-context corpora, and widening it to suit
200+
this ledger would export the looser reading to all four (#16898).
183201
- **A citation the gate cannot resolve stays BARE — that is honest, not a gap.** Anchors
184202
are only for extensions the gate reads symbols out of (`.ts .tsx .mts .cts .js .mjs
185203
.cjs .json`); an anchor on a `.md` or a `.yml` is an ERROR rather than a silent skip,

0 commit comments

Comments
 (0)