Skip to content

gate(spec): register packages/spec/src/** doc blocks as the fourth symbol-anchor corpus, censused first - #17241

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-17065-spec-docblock-anchor-corpus
Sep 9, 2026
Merged

baozhoutao merged 2 commits into
mainfrom
claude/issue-17065-spec-docblock-anchor-corpus

Conversation

@claude

@claude claude Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Clause-②: no

Fixes #17065

Registers packages/spec/src/** doc blocks as the fourth corpus of the shared symbol-anchor resolver — a defineCorpus call in the deliberately thin shape check-adr-symbol-anchors.mjs and check-scripts-symbol-anchors.mjs already use. ⛔ No second resolver — the 2026-09-01 ruling on #13556 forbids one in terms, and scripts/symbol-anchors.mjs is untouched by this diff.

⭐ The census came first, and here is the number

The card wrote the order hard and it was followed literally: register, run the reporting arms, take the count, then choose the exit contract. Measured on 08e38c6377 over the 1,317 tracked .ts files under packages/spec/src, with the instrument named beside every number because three instruments give three answers on one tree:

instrument count
raw extractAnchors over the whole file (counts spec's own fixture data) 198
through the commentProse projection — doc prose only 189
…and the cited path names a tracked file — what this gate judges 6, across 5 files

Total hard findings: 7 across 6 of 1,317 files — the 6 line anchors plus 1 unresolved-path. Alongside them: 183 citations naming no tracked file (69 bare-filename, 62 directory-qualified, 52 continuation, across 34 files), and 3 soft cross-repo-skipped rows.

Controls, because a small number needs them more than a big one does. Every zero here is backed by a same-corpus positive control, using short fragments only:

  • counts.symbol is 0. Control: git grep for a backticked path-hash-identifier span across packages/spec/src returns exactly 1 file — api/websocket.zod.ts — and that one span is the unresolved-path finding. So the zero is "the convention has not reached this corpus", not "the extractor matched nothing".
  • The extractor is demonstrably live on this corpus: 2,639 anchors across 1,317 sources, 2,635 of them file-level.
  • The declined count is enumerated, not just counted: --list-unresolvable prints all 183 rows, and the self-test holds declined.length equal to the counter.

The bare-path axis, measured for the checkBarePaths: false call: 2,635 bare path spans; 346 name a tracked file at the repo root, 610 resolve only when prefixed with packages/spec/src/ (spec doc blocks habitually cite package-relative), and 1,679 (758 distinct) bind against neither base. Judging them would produce 2,290 findings — the same call docs/adr/** made at 1,056 and scripts/** at 1,617.

The exit contract that number justified, and why

A pinned day-one residual, with the gate ON. All 7 findings live in packages/spec/** text, which this card ⛔ forbids this lane from editing — repairing a spec doc block is domain:spec work with its own cards. That left two honest options: a gate that can never fail, or a gate that fails on everything except a dated, enumerated, shrink-only residual.

A gate that can never fail is the verifier AGENTS.md names as worse than no verifier, so CENSUS_RESIDUAL pins the 7 sites with their repairs and every finding outside it is a hard red from day one — which is exactly the property the card asked for: the fourth set does not have to be found by hand.

⛔ It is not an exemption list. It is exact in both directions: a row whose citation gets repaired goes stale and reds until it is deleted, so a repair forces the row out in the same PR. Rows are keyed by citation text, never by line — a line-keyed row would be a line anchor inside the line-anchor gate, rotting the first time anyone added a paragraph above it.

⚠️ Re-grade triggers: both measured, neither fires — with one caveat that belongs to the PM

Trigger 1 (rot comparable to 72.1%, or a count that makes this a migration): does NOT fire. 7 findings across 6 of 1,317 files is a gate, not a migration.

⚠️ But that answer is conditional on the scope call, and the conditionality is the reading rather than a footnote to it. With judgeUntrackedLineAnchors flipped to true, the same tree yields 189 findings on day one — which would be a migration. The small number is not a claim that spec doc blocks are clean; it is a statement about the citations a resolver living in this repo can bind. Both numbers are pinned in CENSUS_17065 and the self-test holds them consistent, so the reassuring one can never be read without the other.

Trigger 2 (a fifth hand-repair filed before this lands): does NOT fire. Searched with a positive control that the search reaches this very card (#17065 came back in the results); the open cards in this family are #16960 (the third repair) and #15809 (the scripts/** declined-citation worklist). No fifth.

⭐ An honest limit of this gate, stated because it is the card's own subject

The three objectql/engine.ts:NNNN sites that #16960 owns are written with an abbreviated path, so they name no tracked file and land in the declined set — this gate would not have caught them. Same for the record-validator.ts sites #16441 repaired. The fourth set this registration stops from being found by hand is the set written with a repo-root path.

That is not a defect in the registration — it is the same scope call scripts/** made, whose residual became #15809 — but it would be dishonest to let the header imply otherwise, so the header says it in those words and --list-unresolvable keeps the population a worklist rather than a number. Handed to the PM below rather than carded here.

Verification

Ablation — the gate really can fail, both directions, on the live tree. Each leg: on-disk mutation proven by occurrence counts and a changed blob hash, run, restore proven byte-identical against the HEAD blob. ⛔ No packages/spec/** text was touched by either leg — both mutate the gate's own CENSUS_RESIDUAL, which is what decides the exit contract.

leg mutation gate exit what it printed
1 — an unpinned finding must red delete the http-server.zod.ts residual row (occurrences 1 to 0; blob 955d9879 to ad18e70f) 1 [line-anchor] packages/spec/src/system/http-server.zod.ts:238
2 — a row matching nothing must red add a ghost row (occurrences 0 to 1; blob 955d9879 to 8dabf07e) 1 1 STALE CENSUS_RESIDUAL row(s) — and the self-test reds too
restore — 0 on-disk blob 955d9879 identical to the HEAD blob; green line back

Derived gates. node scripts/pm/dispatch-gates.mjs --commands derived 63 families from the changeset; all 63 were run and reconciled with --ran: "63 derived famil(ies) accounted for — 63 run, 0 NOT-MEASURED."

One derived gate went genuinely red and was repaired: check-scripts-symbol-anchors — scripts/** is itself a corpus, so the new gate's own header quoting a broken anchor verbatim made this file a finding. Both offending spellings are now described in words, with the reason stated inline. Both corpora are green on the final commit.

Five commands exit 3 = PREREQUISITE NOT MET and are NOT MEASURED, not red — they read dist/ and want a full pnpm build: check:dts-closure, check:dual-build-cjs-loads, check:lean-entry-closure, check:sourcemap-no-sources-content, check:type-check-debt. This diff touches no package source, so no package closure is affected; the full build belongs to CI.

Re-run on the final commit 0be9a49284: both symbol-anchor corpora plus their self-tests, the ADR corpus, the shared resolver's self-test, check-step-collectors, check-self-test-workflow-commands, check-self-test-wired, check:nul-bytes, check:parse-guard, check:declared-population-live, check:watch-hint-literal, check:type-check-coverage — all exit 0. check:pm-dispatch-gates (the tool whose ledger prose this diff edits) passes: "dispatch-gates self-test: 1624 cases pass."

Lint, as a declared narrowing rather than a repo-wide run. Targeted eslint --no-inline-config --format json over the 2 changed .mjs files: 2 files linted, 0 errors, 0 warnings. The three pieces of evidence the narrowing needs: ① the population is read from eslint.config.mjs itself, which lints only from the root; ② the file count is read from --format json output, not asserted; ③ the invariance claim is the config's own measured declaration — it "never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file" — so this diff cannot move the verdict on any file it did not touch. The repo-wide pnpm lint remains CI's run.

skip-changeset, measured with a positive control. Nothing published moves: 70 published packages inspected, 0 name scripts/ or lint.yml in files[]; the control is that all 70 name dist, proving the probe reads real entries. The root manifest is private: true. All four paths are fast-track — repo-root config, .github/workflows/, scripts/**, scripts/pm/**.

Acceptance notes — found, not fixed

Not addressed here

⛔ Out of scope: #16962 remains open — that card's @example caption convention is a different convention with no existing mechanism, deliberately not merged with this one. #16960 remains open and lands on its own; this PR ⛔ does not repair its three sites, and does not repair the 7 pinned residual sites either.


Generated by Claude Code

…hor corpus

The fourth corpus of the shared symbol-anchor resolver (scripts/symbol-anchors.mjs),
joined by a `defineCorpus` call in the deliberately thin shape
check-adr-symbol-anchors.mjs and check-scripts-symbol-anchors.mjs already use --
no second resolver.

The census came first and the exit contract second. Measured on 08e38c6 in
--list / --list-unresolvable reporting mode over 1,317 tracked .ts files:

  198  raw extractAnchors over the whole file (counts spec's own fixture data)
  189  through the commentProse projection (doc prose only)
    6  ...and the cited path names a tracked file -- what this gate judges

7 hard findings across 6 files (6 line-anchor + 1 unresolved-path), 183 declined
citations naming no tracked file, 3 soft cross-repo rows. That is a gate, not a
migration -- the opposite of docs/adr/**'s 243-of-337 (72.1%).

Repairing those 7 edits packages/spec/** text, which is domain:spec work with its
own cards, so the exit contract is a pinned day-one residual: CENSUS_RESIDUAL
enumerates the 7 sites with their repairs, every finding outside it is a hard red
from day one, and a row whose citation gets repaired goes STALE and reds until it
is deleted. Rows are keyed by citation TEXT, never by line -- a line-keyed row
would be a line anchor inside the line-anchor gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
…erbatim

`scripts/**` is itself a registered corpus, so the new gate's own header is
swept by check-scripts-symbol-anchors. Quoting the websocket.zod.ts citation in
anchor form -- and spelling the bare-word-colon-path shape literally -- made
this file a finding against that corpus (1 unresolved-path, 1 soft cross-repo).

Both are now described in words, with the reason stated inline so the next
author does not reintroduce them. The citation itself is unchanged: it stays
pinned in CENSUS_RESIDUAL as a string literal, which the commentProse
projection blanks as code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 9, 2026
@github-actions github-actions Bot added size/l ci/cd dependencies Pull requests that update a dependency file labels Sep 9, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 9, 2026 21:25
@baozhoutao
baozhoutao enabled auto-merge September 9, 2026 21:25
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 9, 2026
Merged via the queue into main with commit 6aa1d09 Sep 9, 2026
39 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-17065-spec-docblock-anchor-corpus branch September 9, 2026 22:11
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
… resolver (objectstack-ai#16898) (objectstack-ai#18100)

Clause-②: no

Closes objectstack-ai#16898

`scripts/symbol-anchors.mjs` states the objectstack-ai#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 objectstack-ai#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
(objectstack-ai#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, objectstack-ai#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: objectstack-ai#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: *"objectstack-ai#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.** objectstack-ai#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. ⛔ **objectstack-ai#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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants