docs(adr): migrate rotted ADR line anchors to resolver-gated symbol anchors (#13556) - #14277

Draft
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-13556-adr-symbol-anchors
Draft

docs(adr): migrate rotted ADR line anchors to resolver-gated symbol anchors (#13556)#14277
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-13556-adr-symbol-anchors

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13556

DRAFT, and it stays a draft.docs/adr/** is a governed surface: a human merges this. Never marked ready, never auto-merge.


The ruling this implements (verbatim)

Maintainer 2026-09-01, 逐字「同意」, 总监批 #27 — quoted from the director comment on the card:

  1. A:ADR 行号锚整体迁为符号锚 + resolver 门禁(缺符号变红)—— 与 platform-checklist step (2): re-author the 1,647 source citations as symbol anchors and add a resolver that reds on an absent symbol #13788 已裁方向同构,共享同一个 resolver,⛔ 不造第二套」
  2. C 不作过渡(不考虑渐进):243 个已证烂行号在 A 的一次迁移中同笔消失,⛔ 不先删再迁两道工;B 排除(片段照样烂);D 排除
  3. 普查即工单:343 锚(+52 CONTINUATION = 395 面)的分类表原样作实施输入 —— 11 个跨仓锚(objectui)迁为跨仓符号引用或降为文件级;172 个裸文件名锚保持文件级(无行号可烂);136 个无可查对象的按类降级或删;4 HISTORICAL / 2 EXTERNAL 豁免注记」
  4. ADR-0113 语义反转独立立卡…实施者查无则立,⛔ 不折进迁移 PR;ADR-0119 的陈旧反驳(turso-driver.ts 已存在)同扫」
  5. 受治面:docs/adr/** draft + 人工合并;72.1% 是单向误差下界的申报原样入册」

Premise re-verified before implementing. The census reproduces exactly on today's main: 376 raw tokens → 343 distinct anchors across 35 records, matching the census's own per-ADR counts. Nothing had been migrated.


The anchor grammar

Documented once in scripts/symbol-anchors.mjs's header (ANCHOR_GRAMMAR is that text, so a failure message cannot drift from the spec):

`<dir>/<file>.ts#<symbol>` symbol anchor — <symbol> must have a declaration site in that file
`<dir>/<file>.ts` file-level anchor — the file must exist
`objectui:<dir>/<file>.ts` cross-repo anchor — verified only when a checkout is available
`…#first`, `#second` continuation — inherits the path from the anchor before it
⛔ `<dir>/<file>.ts:4901` A LINE NUMBER IS NOT AN ANCHOR FORM.

It satisfies the three properties the form had to have: (a) mechanically verifiable — #symbol is checked against declaration sites, so an absent symbol is a loud red; (b) survives line churn — nothing encodes a position, so an unrelated edit above the target cannot rot it; (c) reads naturally — #fragment is the syntax a reader already knows from URLs, and it survives copy-paste into a GitHub link. Markdown links keep their link; only the rotted #L<n> fragment is dropped, so check-adr-links still cross-checks them.


The resolver's acceptance semantics — what "resolves" actually means

The census's caveat is binding and is why this is not includes(). Comments are stripped from the target before anything is matched, and a token that merely repeats the anchor's own path is refused outright (the census states this rule for its own classifier: otherwise every anchor "resolves" on the word registry / permission / metadata). Two acceptance classes, reported separately and never merged:

classwhat it acceptscount
declarationa declaration site in the target's language — function/class/interface/type/enum/namespace, a const/let/var binding, a member or object-literal key at the start of a line (name:, 'name':, name(, get name(), a named re-export; for markdown a heading (text or slug); for JSON/YAML a key173
literalthe symbol as a complete quoted string token ('sys_metadata'). Exists because much of this platform's vocabulary is DATA identifiers — object and field API names — declared as strings with no binding site. Whole-token only, never a substring15

literal is deliberately weaker and the migration prefers declaration wherever one exists.

⭐ This confirms the ruling's point 5 — 72.1% was a floor, and the direction is one-way. The census's mechanical test accepted a symbol appearing anywhere in the cited range, including inside a comment. Worked example found while migrating: the census credited object.zod.ts with stateMachines; on today's tree the only occurrence is a comment saying that map no longer exists. Under this resolver it correctly does not resolve, and the anchor drops to file level.

What the gate does NOT claim. It verifies an anchor points at something real. It cannot verify the sentence is true about that code — no static check can. That is a separate defect class with its own card (see the carve-outs).

One scope boundary, stated rather than hidden. Bare file-level path mentions are not judged in this corpus (checkBarePaths: false). Measured, not assumed: judging every bare path in docs/adr/** yields 1,056 findings across all 134 records, 616 distinct paths — abbreviated spellings, sibling-repo paths, and files that moved years ago. A real defect class, but not the line-anchor rot #13556 measured, and folding it in would bury this gate's signal under a corpus-wide cleanup nobody ruled on. It is a per-corpus flag, so a corpus with uniform citations (#13788) can switch it on.


Per-class counts, migrated vs the census

469 occurrences rewritten across 35 records.

dispositioncountnotes
→ symbol anchor188173 declaration, 15 literal
→ file-level anchor211the ruled downgrade where the sentence names nothing checkable
→ cross-repo file-level15objectui: prefix, target NAMED (see below)
dead path — number dropped10census's 9 DEAD PATH + 1
duplicate / orphaned continuations removed24see below
HISTORICAL / EXTERNAL exempt64 + 2, inline <!-- anchor-exempt: … -->
tilde-form bare numbers15a spelling the census did not count

Reconciliation with the census's 343 + 52 = 395. Non-continuation occurrences came to 376 raw — exactly the census's 376 — deduping to 340 under my key against the census's 343 (dedupe-key nuance: one comma-list occurrence covers what the census counted as separate rows). Continuations came to 78, not 52, and the tilde form added 15 more. The real surface was larger than 395, because three spellings the census's extractor could not see were found:

  • comma and slash listsregistry.ts:13,346-389, errors.zod.ts:29-39,147-152, engine.ts:2956/2991
  • cross-line continuations — the census's rule was same-line-only; in this corpus they run on across a paragraph
  • the tilde form~`326`, a bare backticked number with no path at all; ADR-0056 alone held 13

Nine spellings in total are now recognised, each provoked by name in the resolver's self-test.

Per-class dispositions from ruling point 3:

  • 11 cross-repo (objectui) → the ruled fallback. No objectui checkout is in tree, so they are downgraded to file-level anchors that NAME the cross-repo target (objectui:packages/types/src/layout.ts). The resolver verifies them only when $OBJECTUI_CHECKOUT is set; without it they are reported, never red — a check that reddens on the healthy case is the permanently-red gate this repo retired. (15 occurrences of the 11 distinct anchors.)
  • 172 bare-filename anchors → every one disambiguated to a full repo-root path, then symbol-anchored where a symbol resolves and file-level otherwise. ⚠️Reading declared: I read 「保持文件级」 as a floor for anchors with nothing checkable, not a ban on symbol-anchoring a bare-filename citation — because the adopted four-dimension analysis says under A 「172 个裸文件名锚被迫消歧,正是把模糊消灭在编写期」. Banning it would discard the locating power option A exists to preserve. Flagged for the reviewer.
  • 136 with no checkable object → downgraded to file level, or the number deleted where the path was dead.
  • 4 HISTORICAL / 2 EXTERNAL → annotated inline. ⛔ Adding an exemption is marked MAINTAINER-ONLY in the gate: it is the one remedy that makes the gate quieter instead of satisfying it.

24 continuations removed. A continuation landing on the same (file, symbol) as its parent says nothing new — it was a different line number in the same function, and once the position is gone so is the distinction; keeping them produced #installPackage/#installPackage/#installPackage. It can also be wrong: ADR-0119 reads "implemented by driver-sql (), driver-memory (:595+), and driver-mongodb (:545+)", where inheriting the preceding path would claim driver-memory's method lives in sql-driver.ts. The prose already names the right component; the anchor must not contradict it.


Negative control (evidence)

Run on the migrated tree, mutation reverted before commit — git status clean, verified.

1 — symbol mutated to a nonsense name (RESERVED_NAMESPACESRESERVED_NAMESPACEZZZ in ADR-0029):

BEFORE ✅ 1854 anchors … 0 line anchors survive. exit 0
AFTER ❌ check-adr-symbol-anchors: 1 finding(s) across 134 records.
[unresolved-symbol] docs/adr/0029-…:59 `packages/objectql/src/registry.ts#RESERVED_NAMESPACEZZZ`
`RESERVED_NAMESPACEZZZ` has no declaration site or string-literal token in
`packages/objectql/src/registry.ts`
exit 1

2 — a line anchor re-introduced (the migration's permanence, ruling point 2):

❌ [line-anchor] docs/adr/0029-…:59 packages/objectql/src/registry.ts:45
a line number is not an anchor form — cite the symbol
(`packages/objectql/src/registry.ts#symbol`) or drop to a file-level anchor
exit 1

Both restored; gate green again, git status --porcelain empty.


20-anchor spot audit

Deterministic sample (seed 13556) over the migrated symbol anchors, each read as ADR sentence vs the newly anchored declaration site. 19 of 20 point at the mechanism the sentence names.

#ADRanchordeclaration site
10130stack.zod.ts#validateSingleAppfunction validateSingleApp(
20117sharing-service.ts#OWNER_FIELDconst OWNER_FIELD = 'owner_id'
30055sharing-service.ts#buildReadFilterasync buildReadFilter(
40118ai-service.ts#ToolExecutionContextexport interface ToolExecutionContext {
50055security-plugin.ts#rlsFilterconst rlsFilter = await this.computeRlsFilter(
60053sql-driver.ts#formatInputmethod declaration
70029object.zod.ts#managedBymanagedBy: z.enum([…])
80029protocol.ts#restoreArtifactRegistryViewprivate async restoreArtifactRegistryView(
90029registry.ts#mergeObjectDefinitionsfunction mergeObjectDefinitions(
100057setup-nav.contributions.ts#group_people_orggroup: 'group_people_org', (literal)
110057team-graph.ts#expandRoleUsersasync expandRoleUsers(roleName, organizationId?)
120105execution-context.zod.ts#ExecutionContextexport type ExecutionContext =
130086security-plugin.ts#permissions⚠️the one miss — repaired, see below
140089field.zod.ts#requiredWhenrequiredWhen: ExpressionInputSchema…
150130registry.ts#RESERVED_NAMESPACESexport const RESERVED_NAMESPACES = new Set([…])
160126metadata-plugin.zod.ts#DEFAULT_METADATA_TYPE_REGISTRYexport const DEFAULT_METADATA_TYPE_REGISTRY:
170104field.zod.ts#LocationCoordinatesSchemaexport const LocationCoordinatesSchema = lazySchema(
180096security-plugin.ts#getReadFiltergetReadFilter: (object, context?) =>
190055security-plugin.ts#computeRlsFiltercomputeRlsFilter: (sets, o, engineOp, c) =>
200130registry.ts#unregisterObjectsByPackageunregisterObjectsByPackage(packageId, force)

The audit changed the output, which is the point of running it. An earlier pass had picked #opCtx (a bare function parameter), #automation for domains/automation.ts (a token straight off the anchor's own path — the census's named trap), and #permissions/#description/#properties (too generic to locate anything). The path-derived-token rule and a measured weak-token list were added, and every one of those was re-picked or dropped to file level; rows 5 and 19 above are the repaired #opCtx case.

Row 13 is the residual: security-plugin.ts#permissions resolved only to a local const permissions inside an unrelated function. The sentence is locating bootstrapDeclaredRoles, the "exact sibling" it names — and that symbol is not in that file (the census's DEAD PATH verdict on bootstrap-declared-roles.ts says the same from the other side). Repaired to a file-level anchor; ⛔ the sentence itself is untouched.


The #13788 reuse seam

Ruling: 「共享同一个 resolver,⛔ 不造第二套」. The split is structural, not a convention:

  • scripts/symbol-anchors.mjs — grammar, extractor, resolution rule, defineCorpus, sweepCorpus. Sweeps nothing on its own and knows about no corpus.
  • scripts/check-adr-symbol-anchors.mjs — a defineCorpus({ id, label, docRoots, docPattern, crossRepos, checkBarePaths }) call plus a population declaration and an exit contract. 241 lines, almost all of it header.

#13788 joins by adding one registration for the platform-checklist corpus. The knobs it will need already exist and are exercised: docPattern (its corpus is .mdx), crossRepos, and checkBarePaths (its citations are uniform, so it can turn the bare-path check on and get it for free). Why one resolver, mechanically: the expensive part is the resolution rule, and two copies drift silently — each gate stays green on its own corpus while meaning something different by "resolves". One implementation, one self-test.

⚠️Name note:scripts/check-adr-anchors.mjs already exists and is an unrelated gate (code → ADR citations, the #3723 direction). This is the opposite direction — ADR → code — hence check-adr-symbol-anchors.


Wiring — the gate RUNS

Registered the way sibling gates are, following check-system-context-census:

Checks run

pnpm lint ✅ · symbol-anchors --self-test ✅ · check-adr-symbol-anchors --self-test ✅ (every finding class provoked on a synthetic corpus, healthy anchors silent, population live, wiring pinned) · gate on the migrated tree ✅ 1,854 anchors across 134 records, 0 line anchors survive

Derived from node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, plus the two mandatory for any scripts/** touch:

check-ratchet-remedy-authority ✅ · check:declared-population-live ✅ · check-watch-hint-literal ✅ · check-step-collectors ✅ · check-self-test-wired ✅ · check-adr-links ✅ · check-adr-anchors ✅ · check-doc-anchors ✅ · check-doc-frontmatter ✅ · check-docs-single-h1 ✅ · check-ci-filter-parity ✅ · check-aggregator-roster ✅ · check-required-contexts ✅ · check-adr-0087-registration ✅ · check-empty-changeset ✅ · check-changeset-no-major ✅ · check-self-test-workflow-commands ✅ · check-workflow-status-functions ✅ · check-keyed-text-bounds

Two gates legitimately failed against the new code and were fixed, not worked around: check-declared-population-live (the shared core declared a path-shaped literal in its grammar examples that named no tracked file — the examples are now <dir>/<file>.ts placeholders and the module carries a dispatch-gates: no-path-population marker, since a corpus supplies its own roots) and check-watch-hint-literal (the declaration was spelled WATCH_HINTS, which is not on the roster — now ROOT_DIR_WATCH_HINTS, pinned in the self-test against the roots the corpus actually sweeps).

Changeset route

skip-changeset, by the workflow's own prescription: lint.yml states that "this PR edits a CI-internal script" is the textbook skip-changeset case — such a PR releases nothing. This PR is docs/adr/** + scripts/** + CI wiring and releases nothing user-facing. Label applied.


Carve-outs held OUT of this PR (ruling point 4)

Open questions for the reviewer

  1. The 「172 保持文件级」 reading (above) — floor, or ceiling? I took it as a floor and disambiguated all bare filenames to full paths, symbol-anchoring where a symbol resolves. If the intent was strictly file-level for all 172, that is a one-flag change to the migration.
  2. 61 stale full-path mentions in ADRs outside the census's 35 (e.g. packages/runtime/src/kernel-manager.ts in ADR-0004) — real rot of a different class, found while measuring the bare-path option. Recorded here, ⛔ not fixed, and not carded pending direction on whether it wants its own card.
  3. ADR-0020's object.stateMachines row now describes a map whose only trace in object.zod.ts is a comment saying it no longer exists. Plausibly that ADR's own decision landing rather than a defect, so no card — flagging it rather than deciding it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Mszp61SetNhQfVGvWh9bNn


Generated by Claude Code

…rs (#13556)
The #13556 census enumerated every line anchor in `docs/adr/**` — 343 across
35 of the 134 records, plus 52 continuation anchors — and resolved each against
main. Excluding 4 HISTORICAL and 2 EXTERNAL, 243 of 337 live anchors were
broken: 72.1%, declared as a one-way LOWER bound. Rot tracked target-file CHURN
rather than record age: the four hottest targets were 100% broken across 50
anchors. A rotted line anchor does not fail — it silently points a reader at
unrelated code inside the document whose purpose is to be a durable record.
Maintainer ruling 2026-09-01 (总监批 #27), option A: migrate the corpus to
symbol anchors behind a resolver gate that reds on an absent symbol, in ONE
migration with no transition period, sharing a single resolver with #13788.
- `scripts/symbol-anchors.mjs` — the SHARED core: grammar, extractor and the
resolution rule. A corpus is a `defineCorpus` registration, so #13788 joins
by registering rather than by forking the resolver.
- `scripts/check-adr-symbol-anchors.mjs` — the `docs/adr/**` registration.
- 469 occurrences rewritten across 35 records: 188 became symbol anchors (173
resolved at a declaration site, 15 as a quoted data identifier), 211 dropped
to file level, 15 to cross-repo file level, 10 dead paths lost their number,
24 duplicate or orphaned continuations were removed, and 6 HISTORICAL /
EXTERNAL anchors carry an inline exemption marker.
Resolution is deliberately stricter than the census: comments are stripped
before matching, so a symbol named only in a comment does not resolve, and a
token that merely repeats the anchor's own path is refused. That strictness is
what confirms 72.1% was a floor — `object.zod.ts` was credited with
`stateMachines` by the census, and the only occurrence today is a comment
saying that map no longer exists.
Nine spellings of a line reference are recognised, three of which the census's
own extractor could not see: comma and slash lists, the trailing `+` form, bare
anchors in running prose, fenced comment headers, and the tilde form.
⛔ No ADR's decision content changes; only anchor form does. ADR-0113's Context
row is known to state the inverse of today's mechanism — carded as #14193,
deliberately NOT repaired here, with a note in the record pointing at it.
Refs #13556
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mszp61SetNhQfVGvWh9bNn
`check-step-collectors --self-test` was red on this branch:
• lint.yml "ADR symbol anchors resolve (no line numbers survive)":
all green => every command runs (3/2)
The counts are (executed / collected), so three ran where two were collected —
not the reverse. That gate drives the real `run:` block under `bash -e` with a
STUB planted at each collected command's SCRIPT PATH, then holds "all green =>
every command runs" as an equality. The step ended with a bare
`node scripts/check-adr-symbol-anchors.mjs` after the collector's exit guard,
which re-invokes the very stub planted for that script's `--self-test` entry —
a third execution against two collected commands.
Split into two steps: the collector carries the two self-tests and nothing
else, and the production sweep is its own step. That is also the shape both
existing collectors in this file already have, and it restores the property
the split was for — the sweep now runs on its own footing instead of behind
the self-tests' `exit 1`.
⛔ No scope change: `docs/adr/**`, the resolver and its corpus registration are
untouched. Both invocations the gate's own `--self-test` pins in lint.yml are
still present, so the wiring assertion still holds.
Refs #13556
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mszp61SetNhQfVGvWh9bNn
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationsize/xlskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant

@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

docs(adr): migrate rotted ADR line anchors to resolver-gated symbol anchors (#13556) - #14277

Draft
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-13556-adr-symbol-anchors
Draft

docs(adr): migrate rotted ADR line anchors to resolver-gated symbol anchors (#13556)#14277
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-13556-adr-symbol-anchors

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13556

DRAFT, and it stays a draft.docs/adr/** is a governed surface: a human merges this. Never marked ready, never auto-merge.


The ruling this implements (verbatim)

Maintainer 2026-09-01, 逐字「同意」, 总监批 #27 — quoted from the director comment on the card:

  1. A:ADR 行号锚整体迁为符号锚 + resolver 门禁(缺符号变红)—— 与 platform-checklist step (2): re-author the 1,647 source citations as symbol anchors and add a resolver that reds on an absent symbol #13788 已裁方向同构,共享同一个 resolver,⛔ 不造第二套」
  2. C 不作过渡(不考虑渐进):243 个已证烂行号在 A 的一次迁移中同笔消失,⛔ 不先删再迁两道工;B 排除(片段照样烂);D 排除
  3. 普查即工单:343 锚(+52 CONTINUATION = 395 面)的分类表原样作实施输入 —— 11 个跨仓锚(objectui)迁为跨仓符号引用或降为文件级;172 个裸文件名锚保持文件级(无行号可烂);136 个无可查对象的按类降级或删;4 HISTORICAL / 2 EXTERNAL 豁免注记」
  4. ADR-0113 语义反转独立立卡…实施者查无则立,⛔ 不折进迁移 PR;ADR-0119 的陈旧反驳(turso-driver.ts 已存在)同扫」
  5. 受治面:docs/adr/** draft + 人工合并;72.1% 是单向误差下界的申报原样入册」

Premise re-verified before implementing. The census reproduces exactly on today's main: 376 raw tokens → 343 distinct anchors across 35 records, matching the census's own per-ADR counts. Nothing had been migrated.


The anchor grammar

Documented once in scripts/symbol-anchors.mjs's header (ANCHOR_GRAMMAR is that text, so a failure message cannot drift from the spec):

`<dir>/<file>.ts#<symbol>` symbol anchor — <symbol> must have a declaration site in that file
`<dir>/<file>.ts` file-level anchor — the file must exist
`objectui:<dir>/<file>.ts` cross-repo anchor — verified only when a checkout is available
`…#first`, `#second` continuation — inherits the path from the anchor before it
⛔ `<dir>/<file>.ts:4901` A LINE NUMBER IS NOT AN ANCHOR FORM.

It satisfies the three properties the form had to have: (a) mechanically verifiable — #symbol is checked against declaration sites, so an absent symbol is a loud red; (b) survives line churn — nothing encodes a position, so an unrelated edit above the target cannot rot it; (c) reads naturally — #fragment is the syntax a reader already knows from URLs, and it survives copy-paste into a GitHub link. Markdown links keep their link; only the rotted #L<n> fragment is dropped, so check-adr-links still cross-checks them.


The resolver's acceptance semantics — what "resolves" actually means

The census's caveat is binding and is why this is not includes(). Comments are stripped from the target before anything is matched, and a token that merely repeats the anchor's own path is refused outright (the census states this rule for its own classifier: otherwise every anchor "resolves" on the word registry / permission / metadata). Two acceptance classes, reported separately and never merged:

classwhat it acceptscount
declarationa declaration site in the target's language — function/class/interface/type/enum/namespace, a const/let/var binding, a member or object-literal key at the start of a line (name:, 'name':, name(, get name(), a named re-export; for markdown a heading (text or slug); for JSON/YAML a key173
literalthe symbol as a complete quoted string token ('sys_metadata'). Exists because much of this platform's vocabulary is DATA identifiers — object and field API names — declared as strings with no binding site. Whole-token only, never a substring15

literal is deliberately weaker and the migration prefers declaration wherever one exists.

⭐ This confirms the ruling's point 5 — 72.1% was a floor, and the direction is one-way. The census's mechanical test accepted a symbol appearing anywhere in the cited range, including inside a comment. Worked example found while migrating: the census credited object.zod.ts with stateMachines; on today's tree the only occurrence is a comment saying that map no longer exists. Under this resolver it correctly does not resolve, and the anchor drops to file level.

What the gate does NOT claim. It verifies an anchor points at something real. It cannot verify the sentence is true about that code — no static check can. That is a separate defect class with its own card (see the carve-outs).

One scope boundary, stated rather than hidden. Bare file-level path mentions are not judged in this corpus (checkBarePaths: false). Measured, not assumed: judging every bare path in docs/adr/** yields 1,056 findings across all 134 records, 616 distinct paths — abbreviated spellings, sibling-repo paths, and files that moved years ago. A real defect class, but not the line-anchor rot #13556 measured, and folding it in would bury this gate's signal under a corpus-wide cleanup nobody ruled on. It is a per-corpus flag, so a corpus with uniform citations (#13788) can switch it on.


Per-class counts, migrated vs the census

469 occurrences rewritten across 35 records.

dispositioncountnotes
→ symbol anchor188173 declaration, 15 literal
→ file-level anchor211the ruled downgrade where the sentence names nothing checkable
→ cross-repo file-level15objectui: prefix, target NAMED (see below)
dead path — number dropped10census's 9 DEAD PATH + 1
duplicate / orphaned continuations removed24see below
HISTORICAL / EXTERNAL exempt64 + 2, inline <!-- anchor-exempt: … -->
tilde-form bare numbers15a spelling the census did not count

Reconciliation with the census's 343 + 52 = 395. Non-continuation occurrences came to 376 raw — exactly the census's 376 — deduping to 340 under my key against the census's 343 (dedupe-key nuance: one comma-list occurrence covers what the census counted as separate rows). Continuations came to 78, not 52, and the tilde form added 15 more. The real surface was larger than 395, because three spellings the census's extractor could not see were found:

  • comma and slash listsregistry.ts:13,346-389, errors.zod.ts:29-39,147-152, engine.ts:2956/2991
  • cross-line continuations — the census's rule was same-line-only; in this corpus they run on across a paragraph
  • the tilde form~`326`, a bare backticked number with no path at all; ADR-0056 alone held 13

Nine spellings in total are now recognised, each provoked by name in the resolver's self-test.

Per-class dispositions from ruling point 3:

  • 11 cross-repo (objectui) → the ruled fallback. No objectui checkout is in tree, so they are downgraded to file-level anchors that NAME the cross-repo target (objectui:packages/types/src/layout.ts). The resolver verifies them only when $OBJECTUI_CHECKOUT is set; without it they are reported, never red — a check that reddens on the healthy case is the permanently-red gate this repo retired. (15 occurrences of the 11 distinct anchors.)
  • 172 bare-filename anchors → every one disambiguated to a full repo-root path, then symbol-anchored where a symbol resolves and file-level otherwise. ⚠️Reading declared: I read 「保持文件级」 as a floor for anchors with nothing checkable, not a ban on symbol-anchoring a bare-filename citation — because the adopted four-dimension analysis says under A 「172 个裸文件名锚被迫消歧,正是把模糊消灭在编写期」. Banning it would discard the locating power option A exists to preserve. Flagged for the reviewer.
  • 136 with no checkable object → downgraded to file level, or the number deleted where the path was dead.
  • 4 HISTORICAL / 2 EXTERNAL → annotated inline. ⛔ Adding an exemption is marked MAINTAINER-ONLY in the gate: it is the one remedy that makes the gate quieter instead of satisfying it.

24 continuations removed. A continuation landing on the same (file, symbol) as its parent says nothing new — it was a different line number in the same function, and once the position is gone so is the distinction; keeping them produced #installPackage/#installPackage/#installPackage. It can also be wrong: ADR-0119 reads "implemented by driver-sql (), driver-memory (:595+), and driver-mongodb (:545+)", where inheriting the preceding path would claim driver-memory's method lives in sql-driver.ts. The prose already names the right component; the anchor must not contradict it.


Negative control (evidence)

Run on the migrated tree, mutation reverted before commit — git status clean, verified.

1 — symbol mutated to a nonsense name (RESERVED_NAMESPACESRESERVED_NAMESPACEZZZ in ADR-0029):

BEFORE ✅ 1854 anchors … 0 line anchors survive. exit 0
AFTER ❌ check-adr-symbol-anchors: 1 finding(s) across 134 records.
[unresolved-symbol] docs/adr/0029-…:59 `packages/objectql/src/registry.ts#RESERVED_NAMESPACEZZZ`
`RESERVED_NAMESPACEZZZ` has no declaration site or string-literal token in
`packages/objectql/src/registry.ts`
exit 1

2 — a line anchor re-introduced (the migration's permanence, ruling point 2):

❌ [line-anchor] docs/adr/0029-…:59 packages/objectql/src/registry.ts:45
a line number is not an anchor form — cite the symbol
(`packages/objectql/src/registry.ts#symbol`) or drop to a file-level anchor
exit 1

Both restored; gate green again, git status --porcelain empty.


20-anchor spot audit

Deterministic sample (seed 13556) over the migrated symbol anchors, each read as ADR sentence vs the newly anchored declaration site. 19 of 20 point at the mechanism the sentence names.

#ADRanchordeclaration site
10130stack.zod.ts#validateSingleAppfunction validateSingleApp(
20117sharing-service.ts#OWNER_FIELDconst OWNER_FIELD = 'owner_id'
30055sharing-service.ts#buildReadFilterasync buildReadFilter(
40118ai-service.ts#ToolExecutionContextexport interface ToolExecutionContext {
50055security-plugin.ts#rlsFilterconst rlsFilter = await this.computeRlsFilter(
60053sql-driver.ts#formatInputmethod declaration
70029object.zod.ts#managedBymanagedBy: z.enum([…])
80029protocol.ts#restoreArtifactRegistryViewprivate async restoreArtifactRegistryView(
90029registry.ts#mergeObjectDefinitionsfunction mergeObjectDefinitions(
100057setup-nav.contributions.ts#group_people_orggroup: 'group_people_org', (literal)
110057team-graph.ts#expandRoleUsersasync expandRoleUsers(roleName, organizationId?)
120105execution-context.zod.ts#ExecutionContextexport type ExecutionContext =
130086security-plugin.ts#permissions⚠️the one miss — repaired, see below
140089field.zod.ts#requiredWhenrequiredWhen: ExpressionInputSchema…
150130registry.ts#RESERVED_NAMESPACESexport const RESERVED_NAMESPACES = new Set([…])
160126metadata-plugin.zod.ts#DEFAULT_METADATA_TYPE_REGISTRYexport const DEFAULT_METADATA_TYPE_REGISTRY:
170104field.zod.ts#LocationCoordinatesSchemaexport const LocationCoordinatesSchema = lazySchema(
180096security-plugin.ts#getReadFiltergetReadFilter: (object, context?) =>
190055security-plugin.ts#computeRlsFiltercomputeRlsFilter: (sets, o, engineOp, c) =>
200130registry.ts#unregisterObjectsByPackageunregisterObjectsByPackage(packageId, force)

The audit changed the output, which is the point of running it. An earlier pass had picked #opCtx (a bare function parameter), #automation for domains/automation.ts (a token straight off the anchor's own path — the census's named trap), and #permissions/#description/#properties (too generic to locate anything). The path-derived-token rule and a measured weak-token list were added, and every one of those was re-picked or dropped to file level; rows 5 and 19 above are the repaired #opCtx case.

Row 13 is the residual: security-plugin.ts#permissions resolved only to a local const permissions inside an unrelated function. The sentence is locating bootstrapDeclaredRoles, the "exact sibling" it names — and that symbol is not in that file (the census's DEAD PATH verdict on bootstrap-declared-roles.ts says the same from the other side). Repaired to a file-level anchor; ⛔ the sentence itself is untouched.


The #13788 reuse seam

Ruling: 「共享同一个 resolver,⛔ 不造第二套」. The split is structural, not a convention:

  • scripts/symbol-anchors.mjs — grammar, extractor, resolution rule, defineCorpus, sweepCorpus. Sweeps nothing on its own and knows about no corpus.
  • scripts/check-adr-symbol-anchors.mjs — a defineCorpus({ id, label, docRoots, docPattern, crossRepos, checkBarePaths }) call plus a population declaration and an exit contract. 241 lines, almost all of it header.

#13788 joins by adding one registration for the platform-checklist corpus. The knobs it will need already exist and are exercised: docPattern (its corpus is .mdx), crossRepos, and checkBarePaths (its citations are uniform, so it can turn the bare-path check on and get it for free). Why one resolver, mechanically: the expensive part is the resolution rule, and two copies drift silently — each gate stays green on its own corpus while meaning something different by "resolves". One implementation, one self-test.

⚠️Name note:scripts/check-adr-anchors.mjs already exists and is an unrelated gate (code → ADR citations, the #3723 direction). This is the opposite direction — ADR → code — hence check-adr-symbol-anchors.


Wiring — the gate RUNS

Registered the way sibling gates are, following check-system-context-census:

Checks run

pnpm lint ✅ · symbol-anchors --self-test ✅ · check-adr-symbol-anchors --self-test ✅ (every finding class provoked on a synthetic corpus, healthy anchors silent, population live, wiring pinned) · gate on the migrated tree ✅ 1,854 anchors across 134 records, 0 line anchors survive

Derived from node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, plus the two mandatory for any scripts/** touch:

check-ratchet-remedy-authority ✅ · check:declared-population-live ✅ · check-watch-hint-literal ✅ · check-step-collectors ✅ · check-self-test-wired ✅ · check-adr-links ✅ · check-adr-anchors ✅ · check-doc-anchors ✅ · check-doc-frontmatter ✅ · check-docs-single-h1 ✅ · check-ci-filter-parity ✅ · check-aggregator-roster ✅ · check-required-contexts ✅ · check-adr-0087-registration ✅ · check-empty-changeset ✅ · check-changeset-no-major ✅ · check-self-test-workflow-commands ✅ · check-workflow-status-functions ✅ · check-keyed-text-bounds

Two gates legitimately failed against the new code and were fixed, not worked around: check-declared-population-live (the shared core declared a path-shaped literal in its grammar examples that named no tracked file — the examples are now <dir>/<file>.ts placeholders and the module carries a dispatch-gates: no-path-population marker, since a corpus supplies its own roots) and check-watch-hint-literal (the declaration was spelled WATCH_HINTS, which is not on the roster — now ROOT_DIR_WATCH_HINTS, pinned in the self-test against the roots the corpus actually sweeps).

Changeset route

skip-changeset, by the workflow's own prescription: lint.yml states that "this PR edits a CI-internal script" is the textbook skip-changeset case — such a PR releases nothing. This PR is docs/adr/** + scripts/** + CI wiring and releases nothing user-facing. Label applied.


Carve-outs held OUT of this PR (ruling point 4)

Open questions for the reviewer

  1. The 「172 保持文件级」 reading (above) — floor, or ceiling? I took it as a floor and disambiguated all bare filenames to full paths, symbol-anchoring where a symbol resolves. If the intent was strictly file-level for all 172, that is a one-flag change to the migration.
  2. 61 stale full-path mentions in ADRs outside the census's 35 (e.g. packages/runtime/src/kernel-manager.ts in ADR-0004) — real rot of a different class, found while measuring the bare-path option. Recorded here, ⛔ not fixed, and not carded pending direction on whether it wants its own card.
  3. ADR-0020's object.stateMachines row now describes a map whose only trace in object.zod.ts is a comment saying it no longer exists. Plausibly that ADR's own decision landing rather than a defect, so no card — flagging it rather than deciding it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Mszp61SetNhQfVGvWh9bNn


Generated by Claude Code

…rs (#13556)
The #13556 census enumerated every line anchor in `docs/adr/**` — 343 across
35 of the 134 records, plus 52 continuation anchors — and resolved each against
main. Excluding 4 HISTORICAL and 2 EXTERNAL, 243 of 337 live anchors were
broken: 72.1%, declared as a one-way LOWER bound. Rot tracked target-file CHURN
rather than record age: the four hottest targets were 100% broken across 50
anchors. A rotted line anchor does not fail — it silently points a reader at
unrelated code inside the document whose purpose is to be a durable record.
Maintainer ruling 2026-09-01 (总监批 #27), option A: migrate the corpus to
symbol anchors behind a resolver gate that reds on an absent symbol, in ONE
migration with no transition period, sharing a single resolver with #13788.
- `scripts/symbol-anchors.mjs` — the SHARED core: grammar, extractor and the
resolution rule. A corpus is a `defineCorpus` registration, so #13788 joins
by registering rather than by forking the resolver.
- `scripts/check-adr-symbol-anchors.mjs` — the `docs/adr/**` registration.
- 469 occurrences rewritten across 35 records: 188 became symbol anchors (173
resolved at a declaration site, 15 as a quoted data identifier), 211 dropped
to file level, 15 to cross-repo file level, 10 dead paths lost their number,
24 duplicate or orphaned continuations were removed, and 6 HISTORICAL /
EXTERNAL anchors carry an inline exemption marker.
Resolution is deliberately stricter than the census: comments are stripped
before matching, so a symbol named only in a comment does not resolve, and a
token that merely repeats the anchor's own path is refused. That strictness is
what confirms 72.1% was a floor — `object.zod.ts` was credited with
`stateMachines` by the census, and the only occurrence today is a comment
saying that map no longer exists.
Nine spellings of a line reference are recognised, three of which the census's
own extractor could not see: comma and slash lists, the trailing `+` form, bare
anchors in running prose, fenced comment headers, and the tilde form.
⛔ No ADR's decision content changes; only anchor form does. ADR-0113's Context
row is known to state the inverse of today's mechanism — carded as #14193,
deliberately NOT repaired here, with a note in the record pointing at it.
Refs #13556
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mszp61SetNhQfVGvWh9bNn
`check-step-collectors --self-test` was red on this branch:
• lint.yml "ADR symbol anchors resolve (no line numbers survive)":
all green => every command runs (3/2)
The counts are (executed / collected), so three ran where two were collected —
not the reverse. That gate drives the real `run:` block under `bash -e` with a
STUB planted at each collected command's SCRIPT PATH, then holds "all green =>
every command runs" as an equality. The step ended with a bare
`node scripts/check-adr-symbol-anchors.mjs` after the collector's exit guard,
which re-invokes the very stub planted for that script's `--self-test` entry —
a third execution against two collected commands.
Split into two steps: the collector carries the two self-tests and nothing
else, and the production sweep is its own step. That is also the shape both
existing collectors in this file already have, and it restores the property
the split was for — the sweep now runs on its own footing instead of behind
the self-tests' `exit 1`.
⛔ No scope change: `docs/adr/**`, the resolver and its corpus registration are
untouched. Both invocations the gate's own `--self-test` pins in lint.yml are
still present, so the wiring assertion still holds.
Refs #13556
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mszp61SetNhQfVGvWh9bNn
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationsize/xlskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant

@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

docs(adr): migrate rotted ADR line anchors to resolver-gated symbol anchors (#13556) - #14277

Draft
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-13556-adr-symbol-anchors
Draft

docs(adr): migrate rotted ADR line anchors to resolver-gated symbol anchors (#13556)#14277
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-13556-adr-symbol-anchors

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13556

DRAFT, and it stays a draft.docs/adr/** is a governed surface: a human merges this. Never marked ready, never auto-merge.


The ruling this implements (verbatim)

Maintainer 2026-09-01, 逐字「同意」, 总监批 #27 — quoted from the director comment on the card:

  1. A:ADR 行号锚整体迁为符号锚 + resolver 门禁(缺符号变红)—— 与 platform-checklist step (2): re-author the 1,647 source citations as symbol anchors and add a resolver that reds on an absent symbol #13788 已裁方向同构,共享同一个 resolver,⛔ 不造第二套」
  2. C 不作过渡(不考虑渐进):243 个已证烂行号在 A 的一次迁移中同笔消失,⛔ 不先删再迁两道工;B 排除(片段照样烂);D 排除
  3. 普查即工单:343 锚(+52 CONTINUATION = 395 面)的分类表原样作实施输入 —— 11 个跨仓锚(objectui)迁为跨仓符号引用或降为文件级;172 个裸文件名锚保持文件级(无行号可烂);136 个无可查对象的按类降级或删;4 HISTORICAL / 2 EXTERNAL 豁免注记」
  4. ADR-0113 语义反转独立立卡…实施者查无则立,⛔ 不折进迁移 PR;ADR-0119 的陈旧反驳(turso-driver.ts 已存在)同扫」
  5. 受治面:docs/adr/** draft + 人工合并;72.1% 是单向误差下界的申报原样入册」

Premise re-verified before implementing. The census reproduces exactly on today's main: 376 raw tokens → 343 distinct anchors across 35 records, matching the census's own per-ADR counts. Nothing had been migrated.


The anchor grammar

Documented once in scripts/symbol-anchors.mjs's header (ANCHOR_GRAMMAR is that text, so a failure message cannot drift from the spec):

`<dir>/<file>.ts#<symbol>` symbol anchor — <symbol> must have a declaration site in that file
`<dir>/<file>.ts` file-level anchor — the file must exist
`objectui:<dir>/<file>.ts` cross-repo anchor — verified only when a checkout is available
`…#first`, `#second` continuation — inherits the path from the anchor before it
⛔ `<dir>/<file>.ts:4901` A LINE NUMBER IS NOT AN ANCHOR FORM.

It satisfies the three properties the form had to have: (a) mechanically verifiable — #symbol is checked against declaration sites, so an absent symbol is a loud red; (b) survives line churn — nothing encodes a position, so an unrelated edit above the target cannot rot it; (c) reads naturally — #fragment is the syntax a reader already knows from URLs, and it survives copy-paste into a GitHub link. Markdown links keep their link; only the rotted #L<n> fragment is dropped, so check-adr-links still cross-checks them.


The resolver's acceptance semantics — what "resolves" actually means

The census's caveat is binding and is why this is not includes(). Comments are stripped from the target before anything is matched, and a token that merely repeats the anchor's own path is refused outright (the census states this rule for its own classifier: otherwise every anchor "resolves" on the word registry / permission / metadata). Two acceptance classes, reported separately and never merged:

classwhat it acceptscount
declarationa declaration site in the target's language — function/class/interface/type/enum/namespace, a const/let/var binding, a member or object-literal key at the start of a line (name:, 'name':, name(, get name(), a named re-export; for markdown a heading (text or slug); for JSON/YAML a key173
literalthe symbol as a complete quoted string token ('sys_metadata'). Exists because much of this platform's vocabulary is DATA identifiers — object and field API names — declared as strings with no binding site. Whole-token only, never a substring15

literal is deliberately weaker and the migration prefers declaration wherever one exists.

⭐ This confirms the ruling's point 5 — 72.1% was a floor, and the direction is one-way. The census's mechanical test accepted a symbol appearing anywhere in the cited range, including inside a comment. Worked example found while migrating: the census credited object.zod.ts with stateMachines; on today's tree the only occurrence is a comment saying that map no longer exists. Under this resolver it correctly does not resolve, and the anchor drops to file level.

What the gate does NOT claim. It verifies an anchor points at something real. It cannot verify the sentence is true about that code — no static check can. That is a separate defect class with its own card (see the carve-outs).

One scope boundary, stated rather than hidden. Bare file-level path mentions are not judged in this corpus (checkBarePaths: false). Measured, not assumed: judging every bare path in docs/adr/** yields 1,056 findings across all 134 records, 616 distinct paths — abbreviated spellings, sibling-repo paths, and files that moved years ago. A real defect class, but not the line-anchor rot #13556 measured, and folding it in would bury this gate's signal under a corpus-wide cleanup nobody ruled on. It is a per-corpus flag, so a corpus with uniform citations (#13788) can switch it on.


Per-class counts, migrated vs the census

469 occurrences rewritten across 35 records.

dispositioncountnotes
→ symbol anchor188173 declaration, 15 literal
→ file-level anchor211the ruled downgrade where the sentence names nothing checkable
→ cross-repo file-level15objectui: prefix, target NAMED (see below)
dead path — number dropped10census's 9 DEAD PATH + 1
duplicate / orphaned continuations removed24see below
HISTORICAL / EXTERNAL exempt64 + 2, inline <!-- anchor-exempt: … -->
tilde-form bare numbers15a spelling the census did not count

Reconciliation with the census's 343 + 52 = 395. Non-continuation occurrences came to 376 raw — exactly the census's 376 — deduping to 340 under my key against the census's 343 (dedupe-key nuance: one comma-list occurrence covers what the census counted as separate rows). Continuations came to 78, not 52, and the tilde form added 15 more. The real surface was larger than 395, because three spellings the census's extractor could not see were found:

  • comma and slash listsregistry.ts:13,346-389, errors.zod.ts:29-39,147-152, engine.ts:2956/2991
  • cross-line continuations — the census's rule was same-line-only; in this corpus they run on across a paragraph
  • the tilde form~`326`, a bare backticked number with no path at all; ADR-0056 alone held 13

Nine spellings in total are now recognised, each provoked by name in the resolver's self-test.

Per-class dispositions from ruling point 3:

  • 11 cross-repo (objectui) → the ruled fallback. No objectui checkout is in tree, so they are downgraded to file-level anchors that NAME the cross-repo target (objectui:packages/types/src/layout.ts). The resolver verifies them only when $OBJECTUI_CHECKOUT is set; without it they are reported, never red — a check that reddens on the healthy case is the permanently-red gate this repo retired. (15 occurrences of the 11 distinct anchors.)
  • 172 bare-filename anchors → every one disambiguated to a full repo-root path, then symbol-anchored where a symbol resolves and file-level otherwise. ⚠️Reading declared: I read 「保持文件级」 as a floor for anchors with nothing checkable, not a ban on symbol-anchoring a bare-filename citation — because the adopted four-dimension analysis says under A 「172 个裸文件名锚被迫消歧,正是把模糊消灭在编写期」. Banning it would discard the locating power option A exists to preserve. Flagged for the reviewer.
  • 136 with no checkable object → downgraded to file level, or the number deleted where the path was dead.
  • 4 HISTORICAL / 2 EXTERNAL → annotated inline. ⛔ Adding an exemption is marked MAINTAINER-ONLY in the gate: it is the one remedy that makes the gate quieter instead of satisfying it.

24 continuations removed. A continuation landing on the same (file, symbol) as its parent says nothing new — it was a different line number in the same function, and once the position is gone so is the distinction; keeping them produced #installPackage/#installPackage/#installPackage. It can also be wrong: ADR-0119 reads "implemented by driver-sql (), driver-memory (:595+), and driver-mongodb (:545+)", where inheriting the preceding path would claim driver-memory's method lives in sql-driver.ts. The prose already names the right component; the anchor must not contradict it.


Negative control (evidence)

Run on the migrated tree, mutation reverted before commit — git status clean, verified.

1 — symbol mutated to a nonsense name (RESERVED_NAMESPACESRESERVED_NAMESPACEZZZ in ADR-0029):

BEFORE ✅ 1854 anchors … 0 line anchors survive. exit 0
AFTER ❌ check-adr-symbol-anchors: 1 finding(s) across 134 records.
[unresolved-symbol] docs/adr/0029-…:59 `packages/objectql/src/registry.ts#RESERVED_NAMESPACEZZZ`
`RESERVED_NAMESPACEZZZ` has no declaration site or string-literal token in
`packages/objectql/src/registry.ts`
exit 1

2 — a line anchor re-introduced (the migration's permanence, ruling point 2):

❌ [line-anchor] docs/adr/0029-…:59 packages/objectql/src/registry.ts:45
a line number is not an anchor form — cite the symbol
(`packages/objectql/src/registry.ts#symbol`) or drop to a file-level anchor
exit 1

Both restored; gate green again, git status --porcelain empty.


20-anchor spot audit

Deterministic sample (seed 13556) over the migrated symbol anchors, each read as ADR sentence vs the newly anchored declaration site. 19 of 20 point at the mechanism the sentence names.

#ADRanchordeclaration site
10130stack.zod.ts#validateSingleAppfunction validateSingleApp(
20117sharing-service.ts#OWNER_FIELDconst OWNER_FIELD = 'owner_id'
30055sharing-service.ts#buildReadFilterasync buildReadFilter(
40118ai-service.ts#ToolExecutionContextexport interface ToolExecutionContext {
50055security-plugin.ts#rlsFilterconst rlsFilter = await this.computeRlsFilter(
60053sql-driver.ts#formatInputmethod declaration
70029object.zod.ts#managedBymanagedBy: z.enum([…])
80029protocol.ts#restoreArtifactRegistryViewprivate async restoreArtifactRegistryView(
90029registry.ts#mergeObjectDefinitionsfunction mergeObjectDefinitions(
100057setup-nav.contributions.ts#group_people_orggroup: 'group_people_org', (literal)
110057team-graph.ts#expandRoleUsersasync expandRoleUsers(roleName, organizationId?)
120105execution-context.zod.ts#ExecutionContextexport type ExecutionContext =
130086security-plugin.ts#permissions⚠️the one miss — repaired, see below
140089field.zod.ts#requiredWhenrequiredWhen: ExpressionInputSchema…
150130registry.ts#RESERVED_NAMESPACESexport const RESERVED_NAMESPACES = new Set([…])
160126metadata-plugin.zod.ts#DEFAULT_METADATA_TYPE_REGISTRYexport const DEFAULT_METADATA_TYPE_REGISTRY:
170104field.zod.ts#LocationCoordinatesSchemaexport const LocationCoordinatesSchema = lazySchema(
180096security-plugin.ts#getReadFiltergetReadFilter: (object, context?) =>
190055security-plugin.ts#computeRlsFiltercomputeRlsFilter: (sets, o, engineOp, c) =>
200130registry.ts#unregisterObjectsByPackageunregisterObjectsByPackage(packageId, force)

The audit changed the output, which is the point of running it. An earlier pass had picked #opCtx (a bare function parameter), #automation for domains/automation.ts (a token straight off the anchor's own path — the census's named trap), and #permissions/#description/#properties (too generic to locate anything). The path-derived-token rule and a measured weak-token list were added, and every one of those was re-picked or dropped to file level; rows 5 and 19 above are the repaired #opCtx case.

Row 13 is the residual: security-plugin.ts#permissions resolved only to a local const permissions inside an unrelated function. The sentence is locating bootstrapDeclaredRoles, the "exact sibling" it names — and that symbol is not in that file (the census's DEAD PATH verdict on bootstrap-declared-roles.ts says the same from the other side). Repaired to a file-level anchor; ⛔ the sentence itself is untouched.


The #13788 reuse seam

Ruling: 「共享同一个 resolver,⛔ 不造第二套」. The split is structural, not a convention:

  • scripts/symbol-anchors.mjs — grammar, extractor, resolution rule, defineCorpus, sweepCorpus. Sweeps nothing on its own and knows about no corpus.
  • scripts/check-adr-symbol-anchors.mjs — a defineCorpus({ id, label, docRoots, docPattern, crossRepos, checkBarePaths }) call plus a population declaration and an exit contract. 241 lines, almost all of it header.

#13788 joins by adding one registration for the platform-checklist corpus. The knobs it will need already exist and are exercised: docPattern (its corpus is .mdx), crossRepos, and checkBarePaths (its citations are uniform, so it can turn the bare-path check on and get it for free). Why one resolver, mechanically: the expensive part is the resolution rule, and two copies drift silently — each gate stays green on its own corpus while meaning something different by "resolves". One implementation, one self-test.

⚠️Name note:scripts/check-adr-anchors.mjs already exists and is an unrelated gate (code → ADR citations, the #3723 direction). This is the opposite direction — ADR → code — hence check-adr-symbol-anchors.


Wiring — the gate RUNS

Registered the way sibling gates are, following check-system-context-census:

Checks run

pnpm lint ✅ · symbol-anchors --self-test ✅ · check-adr-symbol-anchors --self-test ✅ (every finding class provoked on a synthetic corpus, healthy anchors silent, population live, wiring pinned) · gate on the migrated tree ✅ 1,854 anchors across 134 records, 0 line anchors survive

Derived from node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, plus the two mandatory for any scripts/** touch:

check-ratchet-remedy-authority ✅ · check:declared-population-live ✅ · check-watch-hint-literal ✅ · check-step-collectors ✅ · check-self-test-wired ✅ · check-adr-links ✅ · check-adr-anchors ✅ · check-doc-anchors ✅ · check-doc-frontmatter ✅ · check-docs-single-h1 ✅ · check-ci-filter-parity ✅ · check-aggregator-roster ✅ · check-required-contexts ✅ · check-adr-0087-registration ✅ · check-empty-changeset ✅ · check-changeset-no-major ✅ · check-self-test-workflow-commands ✅ · check-workflow-status-functions ✅ · check-keyed-text-bounds

Two gates legitimately failed against the new code and were fixed, not worked around: check-declared-population-live (the shared core declared a path-shaped literal in its grammar examples that named no tracked file — the examples are now <dir>/<file>.ts placeholders and the module carries a dispatch-gates: no-path-population marker, since a corpus supplies its own roots) and check-watch-hint-literal (the declaration was spelled WATCH_HINTS, which is not on the roster — now ROOT_DIR_WATCH_HINTS, pinned in the self-test against the roots the corpus actually sweeps).

Changeset route

skip-changeset, by the workflow's own prescription: lint.yml states that "this PR edits a CI-internal script" is the textbook skip-changeset case — such a PR releases nothing. This PR is docs/adr/** + scripts/** + CI wiring and releases nothing user-facing. Label applied.


Carve-outs held OUT of this PR (ruling point 4)

Open questions for the reviewer

  1. The 「172 保持文件级」 reading (above) — floor, or ceiling? I took it as a floor and disambiguated all bare filenames to full paths, symbol-anchoring where a symbol resolves. If the intent was strictly file-level for all 172, that is a one-flag change to the migration.
  2. 61 stale full-path mentions in ADRs outside the census's 35 (e.g. packages/runtime/src/kernel-manager.ts in ADR-0004) — real rot of a different class, found while measuring the bare-path option. Recorded here, ⛔ not fixed, and not carded pending direction on whether it wants its own card.
  3. ADR-0020's object.stateMachines row now describes a map whose only trace in object.zod.ts is a comment saying it no longer exists. Plausibly that ADR's own decision landing rather than a defect, so no card — flagging it rather than deciding it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Mszp61SetNhQfVGvWh9bNn


Generated by Claude Code

…rs (#13556)
The #13556 census enumerated every line anchor in `docs/adr/**` — 343 across
35 of the 134 records, plus 52 continuation anchors — and resolved each against
main. Excluding 4 HISTORICAL and 2 EXTERNAL, 243 of 337 live anchors were
broken: 72.1%, declared as a one-way LOWER bound. Rot tracked target-file CHURN
rather than record age: the four hottest targets were 100% broken across 50
anchors. A rotted line anchor does not fail — it silently points a reader at
unrelated code inside the document whose purpose is to be a durable record.
Maintainer ruling 2026-09-01 (总监批 #27), option A: migrate the corpus to
symbol anchors behind a resolver gate that reds on an absent symbol, in ONE
migration with no transition period, sharing a single resolver with #13788.
- `scripts/symbol-anchors.mjs` — the SHARED core: grammar, extractor and the
resolution rule. A corpus is a `defineCorpus` registration, so #13788 joins
by registering rather than by forking the resolver.
- `scripts/check-adr-symbol-anchors.mjs` — the `docs/adr/**` registration.
- 469 occurrences rewritten across 35 records: 188 became symbol anchors (173
resolved at a declaration site, 15 as a quoted data identifier), 211 dropped
to file level, 15 to cross-repo file level, 10 dead paths lost their number,
24 duplicate or orphaned continuations were removed, and 6 HISTORICAL /
EXTERNAL anchors carry an inline exemption marker.
Resolution is deliberately stricter than the census: comments are stripped
before matching, so a symbol named only in a comment does not resolve, and a
token that merely repeats the anchor's own path is refused. That strictness is
what confirms 72.1% was a floor — `object.zod.ts` was credited with
`stateMachines` by the census, and the only occurrence today is a comment
saying that map no longer exists.
Nine spellings of a line reference are recognised, three of which the census's
own extractor could not see: comma and slash lists, the trailing `+` form, bare
anchors in running prose, fenced comment headers, and the tilde form.
⛔ No ADR's decision content changes; only anchor form does. ADR-0113's Context
row is known to state the inverse of today's mechanism — carded as #14193,
deliberately NOT repaired here, with a note in the record pointing at it.
Refs #13556
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mszp61SetNhQfVGvWh9bNn
`check-step-collectors --self-test` was red on this branch:
• lint.yml "ADR symbol anchors resolve (no line numbers survive)":
all green => every command runs (3/2)
The counts are (executed / collected), so three ran where two were collected —
not the reverse. That gate drives the real `run:` block under `bash -e` with a
STUB planted at each collected command's SCRIPT PATH, then holds "all green =>
every command runs" as an equality. The step ended with a bare
`node scripts/check-adr-symbol-anchors.mjs` after the collector's exit guard,
which re-invokes the very stub planted for that script's `--self-test` entry —
a third execution against two collected commands.
Split into two steps: the collector carries the two self-tests and nothing
else, and the production sweep is its own step. That is also the shape both
existing collectors in this file already have, and it restores the property
the split was for — the sweep now runs on its own footing instead of behind
the self-tests' `exit 1`.
⛔ No scope change: `docs/adr/**`, the resolver and its corpus registration are
untouched. Both invocations the gate's own `--self-test` pins in lint.yml are
still present, so the wiring assertion still holds.
Refs #13556
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mszp61SetNhQfVGvWh9bNn
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationsize/xlskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant

@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

docs(adr): migrate rotted ADR line anchors to resolver-gated symbol anchors (#13556) - #14277

Draft
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-13556-adr-symbol-anchors
Draft

docs(adr): migrate rotted ADR line anchors to resolver-gated symbol anchors (#13556)#14277
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-13556-adr-symbol-anchors

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13556

DRAFT, and it stays a draft.docs/adr/** is a governed surface: a human merges this. Never marked ready, never auto-merge.


The ruling this implements (verbatim)

Maintainer 2026-09-01, 逐字「同意」, 总监批 #27 — quoted from the director comment on the card:

  1. A:ADR 行号锚整体迁为符号锚 + resolver 门禁(缺符号变红)—— 与 platform-checklist step (2): re-author the 1,647 source citations as symbol anchors and add a resolver that reds on an absent symbol #13788 已裁方向同构,共享同一个 resolver,⛔ 不造第二套」
  2. C 不作过渡(不考虑渐进):243 个已证烂行号在 A 的一次迁移中同笔消失,⛔ 不先删再迁两道工;B 排除(片段照样烂);D 排除
  3. 普查即工单:343 锚(+52 CONTINUATION = 395 面)的分类表原样作实施输入 —— 11 个跨仓锚(objectui)迁为跨仓符号引用或降为文件级;172 个裸文件名锚保持文件级(无行号可烂);136 个无可查对象的按类降级或删;4 HISTORICAL / 2 EXTERNAL 豁免注记」
  4. ADR-0113 语义反转独立立卡…实施者查无则立,⛔ 不折进迁移 PR;ADR-0119 的陈旧反驳(turso-driver.ts 已存在)同扫」
  5. 受治面:docs/adr/** draft + 人工合并;72.1% 是单向误差下界的申报原样入册」

Premise re-verified before implementing. The census reproduces exactly on today's main: 376 raw tokens → 343 distinct anchors across 35 records, matching the census's own per-ADR counts. Nothing had been migrated.


The anchor grammar

Documented once in scripts/symbol-anchors.mjs's header (ANCHOR_GRAMMAR is that text, so a failure message cannot drift from the spec):

`<dir>/<file>.ts#<symbol>` symbol anchor — <symbol> must have a declaration site in that file
`<dir>/<file>.ts` file-level anchor — the file must exist
`objectui:<dir>/<file>.ts` cross-repo anchor — verified only when a checkout is available
`…#first`, `#second` continuation — inherits the path from the anchor before it
⛔ `<dir>/<file>.ts:4901` A LINE NUMBER IS NOT AN ANCHOR FORM.

It satisfies the three properties the form had to have: (a) mechanically verifiable — #symbol is checked against declaration sites, so an absent symbol is a loud red; (b) survives line churn — nothing encodes a position, so an unrelated edit above the target cannot rot it; (c) reads naturally — #fragment is the syntax a reader already knows from URLs, and it survives copy-paste into a GitHub link. Markdown links keep their link; only the rotted #L<n> fragment is dropped, so check-adr-links still cross-checks them.


The resolver's acceptance semantics — what "resolves" actually means

The census's caveat is binding and is why this is not includes(). Comments are stripped from the target before anything is matched, and a token that merely repeats the anchor's own path is refused outright (the census states this rule for its own classifier: otherwise every anchor "resolves" on the word registry / permission / metadata). Two acceptance classes, reported separately and never merged:

classwhat it acceptscount
declarationa declaration site in the target's language — function/class/interface/type/enum/namespace, a const/let/var binding, a member or object-literal key at the start of a line (name:, 'name':, name(, get name(), a named re-export; for markdown a heading (text or slug); for JSON/YAML a key173
literalthe symbol as a complete quoted string token ('sys_metadata'). Exists because much of this platform's vocabulary is DATA identifiers — object and field API names — declared as strings with no binding site. Whole-token only, never a substring15

literal is deliberately weaker and the migration prefers declaration wherever one exists.

⭐ This confirms the ruling's point 5 — 72.1% was a floor, and the direction is one-way. The census's mechanical test accepted a symbol appearing anywhere in the cited range, including inside a comment. Worked example found while migrating: the census credited object.zod.ts with stateMachines; on today's tree the only occurrence is a comment saying that map no longer exists. Under this resolver it correctly does not resolve, and the anchor drops to file level.

What the gate does NOT claim. It verifies an anchor points at something real. It cannot verify the sentence is true about that code — no static check can. That is a separate defect class with its own card (see the carve-outs).

One scope boundary, stated rather than hidden. Bare file-level path mentions are not judged in this corpus (checkBarePaths: false). Measured, not assumed: judging every bare path in docs/adr/** yields 1,056 findings across all 134 records, 616 distinct paths — abbreviated spellings, sibling-repo paths, and files that moved years ago. A real defect class, but not the line-anchor rot #13556 measured, and folding it in would bury this gate's signal under a corpus-wide cleanup nobody ruled on. It is a per-corpus flag, so a corpus with uniform citations (#13788) can switch it on.


Per-class counts, migrated vs the census

469 occurrences rewritten across 35 records.

dispositioncountnotes
→ symbol anchor188173 declaration, 15 literal
→ file-level anchor211the ruled downgrade where the sentence names nothing checkable
→ cross-repo file-level15objectui: prefix, target NAMED (see below)
dead path — number dropped10census's 9 DEAD PATH + 1
duplicate / orphaned continuations removed24see below
HISTORICAL / EXTERNAL exempt64 + 2, inline <!-- anchor-exempt: … -->
tilde-form bare numbers15a spelling the census did not count

Reconciliation with the census's 343 + 52 = 395. Non-continuation occurrences came to 376 raw — exactly the census's 376 — deduping to 340 under my key against the census's 343 (dedupe-key nuance: one comma-list occurrence covers what the census counted as separate rows). Continuations came to 78, not 52, and the tilde form added 15 more. The real surface was larger than 395, because three spellings the census's extractor could not see were found:

  • comma and slash listsregistry.ts:13,346-389, errors.zod.ts:29-39,147-152, engine.ts:2956/2991
  • cross-line continuations — the census's rule was same-line-only; in this corpus they run on across a paragraph
  • the tilde form~`326`, a bare backticked number with no path at all; ADR-0056 alone held 13

Nine spellings in total are now recognised, each provoked by name in the resolver's self-test.

Per-class dispositions from ruling point 3:

  • 11 cross-repo (objectui) → the ruled fallback. No objectui checkout is in tree, so they are downgraded to file-level anchors that NAME the cross-repo target (objectui:packages/types/src/layout.ts). The resolver verifies them only when $OBJECTUI_CHECKOUT is set; without it they are reported, never red — a check that reddens on the healthy case is the permanently-red gate this repo retired. (15 occurrences of the 11 distinct anchors.)
  • 172 bare-filename anchors → every one disambiguated to a full repo-root path, then symbol-anchored where a symbol resolves and file-level otherwise. ⚠️Reading declared: I read 「保持文件级」 as a floor for anchors with nothing checkable, not a ban on symbol-anchoring a bare-filename citation — because the adopted four-dimension analysis says under A 「172 个裸文件名锚被迫消歧,正是把模糊消灭在编写期」. Banning it would discard the locating power option A exists to preserve. Flagged for the reviewer.
  • 136 with no checkable object → downgraded to file level, or the number deleted where the path was dead.
  • 4 HISTORICAL / 2 EXTERNAL → annotated inline. ⛔ Adding an exemption is marked MAINTAINER-ONLY in the gate: it is the one remedy that makes the gate quieter instead of satisfying it.

24 continuations removed. A continuation landing on the same (file, symbol) as its parent says nothing new — it was a different line number in the same function, and once the position is gone so is the distinction; keeping them produced #installPackage/#installPackage/#installPackage. It can also be wrong: ADR-0119 reads "implemented by driver-sql (), driver-memory (:595+), and driver-mongodb (:545+)", where inheriting the preceding path would claim driver-memory's method lives in sql-driver.ts. The prose already names the right component; the anchor must not contradict it.


Negative control (evidence)

Run on the migrated tree, mutation reverted before commit — git status clean, verified.

1 — symbol mutated to a nonsense name (RESERVED_NAMESPACESRESERVED_NAMESPACEZZZ in ADR-0029):

BEFORE ✅ 1854 anchors … 0 line anchors survive. exit 0
AFTER ❌ check-adr-symbol-anchors: 1 finding(s) across 134 records.
[unresolved-symbol] docs/adr/0029-…:59 `packages/objectql/src/registry.ts#RESERVED_NAMESPACEZZZ`
`RESERVED_NAMESPACEZZZ` has no declaration site or string-literal token in
`packages/objectql/src/registry.ts`
exit 1

2 — a line anchor re-introduced (the migration's permanence, ruling point 2):

❌ [line-anchor] docs/adr/0029-…:59 packages/objectql/src/registry.ts:45
a line number is not an anchor form — cite the symbol
(`packages/objectql/src/registry.ts#symbol`) or drop to a file-level anchor
exit 1

Both restored; gate green again, git status --porcelain empty.


20-anchor spot audit

Deterministic sample (seed 13556) over the migrated symbol anchors, each read as ADR sentence vs the newly anchored declaration site. 19 of 20 point at the mechanism the sentence names.

#ADRanchordeclaration site
10130stack.zod.ts#validateSingleAppfunction validateSingleApp(
20117sharing-service.ts#OWNER_FIELDconst OWNER_FIELD = 'owner_id'
30055sharing-service.ts#buildReadFilterasync buildReadFilter(
40118ai-service.ts#ToolExecutionContextexport interface ToolExecutionContext {
50055security-plugin.ts#rlsFilterconst rlsFilter = await this.computeRlsFilter(
60053sql-driver.ts#formatInputmethod declaration
70029object.zod.ts#managedBymanagedBy: z.enum([…])
80029protocol.ts#restoreArtifactRegistryViewprivate async restoreArtifactRegistryView(
90029registry.ts#mergeObjectDefinitionsfunction mergeObjectDefinitions(
100057setup-nav.contributions.ts#group_people_orggroup: 'group_people_org', (literal)
110057team-graph.ts#expandRoleUsersasync expandRoleUsers(roleName, organizationId?)
120105execution-context.zod.ts#ExecutionContextexport type ExecutionContext =
130086security-plugin.ts#permissions⚠️the one miss — repaired, see below
140089field.zod.ts#requiredWhenrequiredWhen: ExpressionInputSchema…
150130registry.ts#RESERVED_NAMESPACESexport const RESERVED_NAMESPACES = new Set([…])
160126metadata-plugin.zod.ts#DEFAULT_METADATA_TYPE_REGISTRYexport const DEFAULT_METADATA_TYPE_REGISTRY:
170104field.zod.ts#LocationCoordinatesSchemaexport const LocationCoordinatesSchema = lazySchema(
180096security-plugin.ts#getReadFiltergetReadFilter: (object, context?) =>
190055security-plugin.ts#computeRlsFiltercomputeRlsFilter: (sets, o, engineOp, c) =>
200130registry.ts#unregisterObjectsByPackageunregisterObjectsByPackage(packageId, force)

The audit changed the output, which is the point of running it. An earlier pass had picked #opCtx (a bare function parameter), #automation for domains/automation.ts (a token straight off the anchor's own path — the census's named trap), and #permissions/#description/#properties (too generic to locate anything). The path-derived-token rule and a measured weak-token list were added, and every one of those was re-picked or dropped to file level; rows 5 and 19 above are the repaired #opCtx case.

Row 13 is the residual: security-plugin.ts#permissions resolved only to a local const permissions inside an unrelated function. The sentence is locating bootstrapDeclaredRoles, the "exact sibling" it names — and that symbol is not in that file (the census's DEAD PATH verdict on bootstrap-declared-roles.ts says the same from the other side). Repaired to a file-level anchor; ⛔ the sentence itself is untouched.


The #13788 reuse seam

Ruling: 「共享同一个 resolver,⛔ 不造第二套」. The split is structural, not a convention:

  • scripts/symbol-anchors.mjs — grammar, extractor, resolution rule, defineCorpus, sweepCorpus. Sweeps nothing on its own and knows about no corpus.
  • scripts/check-adr-symbol-anchors.mjs — a defineCorpus({ id, label, docRoots, docPattern, crossRepos, checkBarePaths }) call plus a population declaration and an exit contract. 241 lines, almost all of it header.

#13788 joins by adding one registration for the platform-checklist corpus. The knobs it will need already exist and are exercised: docPattern (its corpus is .mdx), crossRepos, and checkBarePaths (its citations are uniform, so it can turn the bare-path check on and get it for free). Why one resolver, mechanically: the expensive part is the resolution rule, and two copies drift silently — each gate stays green on its own corpus while meaning something different by "resolves". One implementation, one self-test.

⚠️Name note:scripts/check-adr-anchors.mjs already exists and is an unrelated gate (code → ADR citations, the #3723 direction). This is the opposite direction — ADR → code — hence check-adr-symbol-anchors.


Wiring — the gate RUNS

Registered the way sibling gates are, following check-system-context-census:

Checks run

pnpm lint ✅ · symbol-anchors --self-test ✅ · check-adr-symbol-anchors --self-test ✅ (every finding class provoked on a synthetic corpus, healthy anchors silent, population live, wiring pinned) · gate on the migrated tree ✅ 1,854 anchors across 134 records, 0 line anchors survive

Derived from node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, plus the two mandatory for any scripts/** touch:

check-ratchet-remedy-authority ✅ · check:declared-population-live ✅ · check-watch-hint-literal ✅ · check-step-collectors ✅ · check-self-test-wired ✅ · check-adr-links ✅ · check-adr-anchors ✅ · check-doc-anchors ✅ · check-doc-frontmatter ✅ · check-docs-single-h1 ✅ · check-ci-filter-parity ✅ · check-aggregator-roster ✅ · check-required-contexts ✅ · check-adr-0087-registration ✅ · check-empty-changeset ✅ · check-changeset-no-major ✅ · check-self-test-workflow-commands ✅ · check-workflow-status-functions ✅ · check-keyed-text-bounds

Two gates legitimately failed against the new code and were fixed, not worked around: check-declared-population-live (the shared core declared a path-shaped literal in its grammar examples that named no tracked file — the examples are now <dir>/<file>.ts placeholders and the module carries a dispatch-gates: no-path-population marker, since a corpus supplies its own roots) and check-watch-hint-literal (the declaration was spelled WATCH_HINTS, which is not on the roster — now ROOT_DIR_WATCH_HINTS, pinned in the self-test against the roots the corpus actually sweeps).

Changeset route

skip-changeset, by the workflow's own prescription: lint.yml states that "this PR edits a CI-internal script" is the textbook skip-changeset case — such a PR releases nothing. This PR is docs/adr/** + scripts/** + CI wiring and releases nothing user-facing. Label applied.


Carve-outs held OUT of this PR (ruling point 4)

Open questions for the reviewer

  1. The 「172 保持文件级」 reading (above) — floor, or ceiling? I took it as a floor and disambiguated all bare filenames to full paths, symbol-anchoring where a symbol resolves. If the intent was strictly file-level for all 172, that is a one-flag change to the migration.
  2. 61 stale full-path mentions in ADRs outside the census's 35 (e.g. packages/runtime/src/kernel-manager.ts in ADR-0004) — real rot of a different class, found while measuring the bare-path option. Recorded here, ⛔ not fixed, and not carded pending direction on whether it wants its own card.
  3. ADR-0020's object.stateMachines row now describes a map whose only trace in object.zod.ts is a comment saying it no longer exists. Plausibly that ADR's own decision landing rather than a defect, so no card — flagging it rather than deciding it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Mszp61SetNhQfVGvWh9bNn


Generated by Claude Code

…rs (#13556)
The #13556 census enumerated every line anchor in `docs/adr/**` — 343 across
35 of the 134 records, plus 52 continuation anchors — and resolved each against
main. Excluding 4 HISTORICAL and 2 EXTERNAL, 243 of 337 live anchors were
broken: 72.1%, declared as a one-way LOWER bound. Rot tracked target-file CHURN
rather than record age: the four hottest targets were 100% broken across 50
anchors. A rotted line anchor does not fail — it silently points a reader at
unrelated code inside the document whose purpose is to be a durable record.
Maintainer ruling 2026-09-01 (总监批 #27), option A: migrate the corpus to
symbol anchors behind a resolver gate that reds on an absent symbol, in ONE
migration with no transition period, sharing a single resolver with #13788.
- `scripts/symbol-anchors.mjs` — the SHARED core: grammar, extractor and the
resolution rule. A corpus is a `defineCorpus` registration, so #13788 joins
by registering rather than by forking the resolver.
- `scripts/check-adr-symbol-anchors.mjs` — the `docs/adr/**` registration.
- 469 occurrences rewritten across 35 records: 188 became symbol anchors (173
resolved at a declaration site, 15 as a quoted data identifier), 211 dropped
to file level, 15 to cross-repo file level, 10 dead paths lost their number,
24 duplicate or orphaned continuations were removed, and 6 HISTORICAL /
EXTERNAL anchors carry an inline exemption marker.
Resolution is deliberately stricter than the census: comments are stripped
before matching, so a symbol named only in a comment does not resolve, and a
token that merely repeats the anchor's own path is refused. That strictness is
what confirms 72.1% was a floor — `object.zod.ts` was credited with
`stateMachines` by the census, and the only occurrence today is a comment
saying that map no longer exists.
Nine spellings of a line reference are recognised, three of which the census's
own extractor could not see: comma and slash lists, the trailing `+` form, bare
anchors in running prose, fenced comment headers, and the tilde form.
⛔ No ADR's decision content changes; only anchor form does. ADR-0113's Context
row is known to state the inverse of today's mechanism — carded as #14193,
deliberately NOT repaired here, with a note in the record pointing at it.
Refs #13556
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mszp61SetNhQfVGvWh9bNn
`check-step-collectors --self-test` was red on this branch:
• lint.yml "ADR symbol anchors resolve (no line numbers survive)":
all green => every command runs (3/2)
The counts are (executed / collected), so three ran where two were collected —
not the reverse. That gate drives the real `run:` block under `bash -e` with a
STUB planted at each collected command's SCRIPT PATH, then holds "all green =>
every command runs" as an equality. The step ended with a bare
`node scripts/check-adr-symbol-anchors.mjs` after the collector's exit guard,
which re-invokes the very stub planted for that script's `--self-test` entry —
a third execution against two collected commands.
Split into two steps: the collector carries the two self-tests and nothing
else, and the production sweep is its own step. That is also the shape both
existing collectors in this file already have, and it restores the property
the split was for — the sweep now runs on its own footing instead of behind
the self-tests' `exit 1`.
⛔ No scope change: `docs/adr/**`, the resolver and its corpus registration are
untouched. Both invocations the gate's own `--self-test` pins in lint.yml are
still present, so the wiring assertion still holds.
Refs #13556
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mszp61SetNhQfVGvWh9bNn
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationsize/xlskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant

@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

docs(adr): migrate rotted ADR line anchors to resolver-gated symbol anchors (#13556) - #14277

Draft
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-13556-adr-symbol-anchors
Draft

docs(adr): migrate rotted ADR line anchors to resolver-gated symbol anchors (#13556)#14277
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-13556-adr-symbol-anchors

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13556

DRAFT, and it stays a draft.docs/adr/** is a governed surface: a human merges this. Never marked ready, never auto-merge.


The ruling this implements (verbatim)

Maintainer 2026-09-01, 逐字「同意」, 总监批 #27 — quoted from the director comment on the card:

  1. A:ADR 行号锚整体迁为符号锚 + resolver 门禁(缺符号变红)—— 与 platform-checklist step (2): re-author the 1,647 source citations as symbol anchors and add a resolver that reds on an absent symbol #13788 已裁方向同构,共享同一个 resolver,⛔ 不造第二套」
  2. C 不作过渡(不考虑渐进):243 个已证烂行号在 A 的一次迁移中同笔消失,⛔ 不先删再迁两道工;B 排除(片段照样烂);D 排除
  3. 普查即工单:343 锚(+52 CONTINUATION = 395 面)的分类表原样作实施输入 —— 11 个跨仓锚(objectui)迁为跨仓符号引用或降为文件级;172 个裸文件名锚保持文件级(无行号可烂);136 个无可查对象的按类降级或删;4 HISTORICAL / 2 EXTERNAL 豁免注记」
  4. ADR-0113 语义反转独立立卡…实施者查无则立,⛔ 不折进迁移 PR;ADR-0119 的陈旧反驳(turso-driver.ts 已存在)同扫」
  5. 受治面:docs/adr/** draft + 人工合并;72.1% 是单向误差下界的申报原样入册」

Premise re-verified before implementing. The census reproduces exactly on today's main: 376 raw tokens → 343 distinct anchors across 35 records, matching the census's own per-ADR counts. Nothing had been migrated.


The anchor grammar

Documented once in scripts/symbol-anchors.mjs's header (ANCHOR_GRAMMAR is that text, so a failure message cannot drift from the spec):

`<dir>/<file>.ts#<symbol>` symbol anchor — <symbol> must have a declaration site in that file
`<dir>/<file>.ts` file-level anchor — the file must exist
`objectui:<dir>/<file>.ts` cross-repo anchor — verified only when a checkout is available
`…#first`, `#second` continuation — inherits the path from the anchor before it
⛔ `<dir>/<file>.ts:4901` A LINE NUMBER IS NOT AN ANCHOR FORM.

It satisfies the three properties the form had to have: (a) mechanically verifiable — #symbol is checked against declaration sites, so an absent symbol is a loud red; (b) survives line churn — nothing encodes a position, so an unrelated edit above the target cannot rot it; (c) reads naturally — #fragment is the syntax a reader already knows from URLs, and it survives copy-paste into a GitHub link. Markdown links keep their link; only the rotted #L<n> fragment is dropped, so check-adr-links still cross-checks them.


The resolver's acceptance semantics — what "resolves" actually means

The census's caveat is binding and is why this is not includes(). Comments are stripped from the target before anything is matched, and a token that merely repeats the anchor's own path is refused outright (the census states this rule for its own classifier: otherwise every anchor "resolves" on the word registry / permission / metadata). Two acceptance classes, reported separately and never merged:

classwhat it acceptscount
declarationa declaration site in the target's language — function/class/interface/type/enum/namespace, a const/let/var binding, a member or object-literal key at the start of a line (name:, 'name':, name(, get name(), a named re-export; for markdown a heading (text or slug); for JSON/YAML a key173
literalthe symbol as a complete quoted string token ('sys_metadata'). Exists because much of this platform's vocabulary is DATA identifiers — object and field API names — declared as strings with no binding site. Whole-token only, never a substring15

literal is deliberately weaker and the migration prefers declaration wherever one exists.

⭐ This confirms the ruling's point 5 — 72.1% was a floor, and the direction is one-way. The census's mechanical test accepted a symbol appearing anywhere in the cited range, including inside a comment. Worked example found while migrating: the census credited object.zod.ts with stateMachines; on today's tree the only occurrence is a comment saying that map no longer exists. Under this resolver it correctly does not resolve, and the anchor drops to file level.

What the gate does NOT claim. It verifies an anchor points at something real. It cannot verify the sentence is true about that code — no static check can. That is a separate defect class with its own card (see the carve-outs).

One scope boundary, stated rather than hidden. Bare file-level path mentions are not judged in this corpus (checkBarePaths: false). Measured, not assumed: judging every bare path in docs/adr/** yields 1,056 findings across all 134 records, 616 distinct paths — abbreviated spellings, sibling-repo paths, and files that moved years ago. A real defect class, but not the line-anchor rot #13556 measured, and folding it in would bury this gate's signal under a corpus-wide cleanup nobody ruled on. It is a per-corpus flag, so a corpus with uniform citations (#13788) can switch it on.


Per-class counts, migrated vs the census

469 occurrences rewritten across 35 records.

dispositioncountnotes
→ symbol anchor188173 declaration, 15 literal
→ file-level anchor211the ruled downgrade where the sentence names nothing checkable
→ cross-repo file-level15objectui: prefix, target NAMED (see below)
dead path — number dropped10census's 9 DEAD PATH + 1
duplicate / orphaned continuations removed24see below
HISTORICAL / EXTERNAL exempt64 + 2, inline <!-- anchor-exempt: … -->
tilde-form bare numbers15a spelling the census did not count

Reconciliation with the census's 343 + 52 = 395. Non-continuation occurrences came to 376 raw — exactly the census's 376 — deduping to 340 under my key against the census's 343 (dedupe-key nuance: one comma-list occurrence covers what the census counted as separate rows). Continuations came to 78, not 52, and the tilde form added 15 more. The real surface was larger than 395, because three spellings the census's extractor could not see were found:

  • comma and slash listsregistry.ts:13,346-389, errors.zod.ts:29-39,147-152, engine.ts:2956/2991
  • cross-line continuations — the census's rule was same-line-only; in this corpus they run on across a paragraph
  • the tilde form~`326`, a bare backticked number with no path at all; ADR-0056 alone held 13

Nine spellings in total are now recognised, each provoked by name in the resolver's self-test.

Per-class dispositions from ruling point 3:

  • 11 cross-repo (objectui) → the ruled fallback. No objectui checkout is in tree, so they are downgraded to file-level anchors that NAME the cross-repo target (objectui:packages/types/src/layout.ts). The resolver verifies them only when $OBJECTUI_CHECKOUT is set; without it they are reported, never red — a check that reddens on the healthy case is the permanently-red gate this repo retired. (15 occurrences of the 11 distinct anchors.)
  • 172 bare-filename anchors → every one disambiguated to a full repo-root path, then symbol-anchored where a symbol resolves and file-level otherwise. ⚠️Reading declared: I read 「保持文件级」 as a floor for anchors with nothing checkable, not a ban on symbol-anchoring a bare-filename citation — because the adopted four-dimension analysis says under A 「172 个裸文件名锚被迫消歧,正是把模糊消灭在编写期」. Banning it would discard the locating power option A exists to preserve. Flagged for the reviewer.
  • 136 with no checkable object → downgraded to file level, or the number deleted where the path was dead.
  • 4 HISTORICAL / 2 EXTERNAL → annotated inline. ⛔ Adding an exemption is marked MAINTAINER-ONLY in the gate: it is the one remedy that makes the gate quieter instead of satisfying it.

24 continuations removed. A continuation landing on the same (file, symbol) as its parent says nothing new — it was a different line number in the same function, and once the position is gone so is the distinction; keeping them produced #installPackage/#installPackage/#installPackage. It can also be wrong: ADR-0119 reads "implemented by driver-sql (), driver-memory (:595+), and driver-mongodb (:545+)", where inheriting the preceding path would claim driver-memory's method lives in sql-driver.ts. The prose already names the right component; the anchor must not contradict it.


Negative control (evidence)

Run on the migrated tree, mutation reverted before commit — git status clean, verified.

1 — symbol mutated to a nonsense name (RESERVED_NAMESPACESRESERVED_NAMESPACEZZZ in ADR-0029):

BEFORE ✅ 1854 anchors … 0 line anchors survive. exit 0
AFTER ❌ check-adr-symbol-anchors: 1 finding(s) across 134 records.
[unresolved-symbol] docs/adr/0029-…:59 `packages/objectql/src/registry.ts#RESERVED_NAMESPACEZZZ`
`RESERVED_NAMESPACEZZZ` has no declaration site or string-literal token in
`packages/objectql/src/registry.ts`
exit 1

2 — a line anchor re-introduced (the migration's permanence, ruling point 2):

❌ [line-anchor] docs/adr/0029-…:59 packages/objectql/src/registry.ts:45
a line number is not an anchor form — cite the symbol
(`packages/objectql/src/registry.ts#symbol`) or drop to a file-level anchor
exit 1

Both restored; gate green again, git status --porcelain empty.


20-anchor spot audit

Deterministic sample (seed 13556) over the migrated symbol anchors, each read as ADR sentence vs the newly anchored declaration site. 19 of 20 point at the mechanism the sentence names.

#ADRanchordeclaration site
10130stack.zod.ts#validateSingleAppfunction validateSingleApp(
20117sharing-service.ts#OWNER_FIELDconst OWNER_FIELD = 'owner_id'
30055sharing-service.ts#buildReadFilterasync buildReadFilter(
40118ai-service.ts#ToolExecutionContextexport interface ToolExecutionContext {
50055security-plugin.ts#rlsFilterconst rlsFilter = await this.computeRlsFilter(
60053sql-driver.ts#formatInputmethod declaration
70029object.zod.ts#managedBymanagedBy: z.enum([…])
80029protocol.ts#restoreArtifactRegistryViewprivate async restoreArtifactRegistryView(
90029registry.ts#mergeObjectDefinitionsfunction mergeObjectDefinitions(
100057setup-nav.contributions.ts#group_people_orggroup: 'group_people_org', (literal)
110057team-graph.ts#expandRoleUsersasync expandRoleUsers(roleName, organizationId?)
120105execution-context.zod.ts#ExecutionContextexport type ExecutionContext =
130086security-plugin.ts#permissions⚠️the one miss — repaired, see below
140089field.zod.ts#requiredWhenrequiredWhen: ExpressionInputSchema…
150130registry.ts#RESERVED_NAMESPACESexport const RESERVED_NAMESPACES = new Set([…])
160126metadata-plugin.zod.ts#DEFAULT_METADATA_TYPE_REGISTRYexport const DEFAULT_METADATA_TYPE_REGISTRY:
170104field.zod.ts#LocationCoordinatesSchemaexport const LocationCoordinatesSchema = lazySchema(
180096security-plugin.ts#getReadFiltergetReadFilter: (object, context?) =>
190055security-plugin.ts#computeRlsFiltercomputeRlsFilter: (sets, o, engineOp, c) =>
200130registry.ts#unregisterObjectsByPackageunregisterObjectsByPackage(packageId, force)

The audit changed the output, which is the point of running it. An earlier pass had picked #opCtx (a bare function parameter), #automation for domains/automation.ts (a token straight off the anchor's own path — the census's named trap), and #permissions/#description/#properties (too generic to locate anything). The path-derived-token rule and a measured weak-token list were added, and every one of those was re-picked or dropped to file level; rows 5 and 19 above are the repaired #opCtx case.

Row 13 is the residual: security-plugin.ts#permissions resolved only to a local const permissions inside an unrelated function. The sentence is locating bootstrapDeclaredRoles, the "exact sibling" it names — and that symbol is not in that file (the census's DEAD PATH verdict on bootstrap-declared-roles.ts says the same from the other side). Repaired to a file-level anchor; ⛔ the sentence itself is untouched.


The #13788 reuse seam

Ruling: 「共享同一个 resolver,⛔ 不造第二套」. The split is structural, not a convention:

  • scripts/symbol-anchors.mjs — grammar, extractor, resolution rule, defineCorpus, sweepCorpus. Sweeps nothing on its own and knows about no corpus.
  • scripts/check-adr-symbol-anchors.mjs — a defineCorpus({ id, label, docRoots, docPattern, crossRepos, checkBarePaths }) call plus a population declaration and an exit contract. 241 lines, almost all of it header.

#13788 joins by adding one registration for the platform-checklist corpus. The knobs it will need already exist and are exercised: docPattern (its corpus is .mdx), crossRepos, and checkBarePaths (its citations are uniform, so it can turn the bare-path check on and get it for free). Why one resolver, mechanically: the expensive part is the resolution rule, and two copies drift silently — each gate stays green on its own corpus while meaning something different by "resolves". One implementation, one self-test.

⚠️Name note:scripts/check-adr-anchors.mjs already exists and is an unrelated gate (code → ADR citations, the #3723 direction). This is the opposite direction — ADR → code — hence check-adr-symbol-anchors.


Wiring — the gate RUNS

Registered the way sibling gates are, following check-system-context-census:

Checks run

pnpm lint ✅ · symbol-anchors --self-test ✅ · check-adr-symbol-anchors --self-test ✅ (every finding class provoked on a synthetic corpus, healthy anchors silent, population live, wiring pinned) · gate on the migrated tree ✅ 1,854 anchors across 134 records, 0 line anchors survive

Derived from node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, plus the two mandatory for any scripts/** touch:

check-ratchet-remedy-authority ✅ · check:declared-population-live ✅ · check-watch-hint-literal ✅ · check-step-collectors ✅ · check-self-test-wired ✅ · check-adr-links ✅ · check-adr-anchors ✅ · check-doc-anchors ✅ · check-doc-frontmatter ✅ · check-docs-single-h1 ✅ · check-ci-filter-parity ✅ · check-aggregator-roster ✅ · check-required-contexts ✅ · check-adr-0087-registration ✅ · check-empty-changeset ✅ · check-changeset-no-major ✅ · check-self-test-workflow-commands ✅ · check-workflow-status-functions ✅ · check-keyed-text-bounds

Two gates legitimately failed against the new code and were fixed, not worked around: check-declared-population-live (the shared core declared a path-shaped literal in its grammar examples that named no tracked file — the examples are now <dir>/<file>.ts placeholders and the module carries a dispatch-gates: no-path-population marker, since a corpus supplies its own roots) and check-watch-hint-literal (the declaration was spelled WATCH_HINTS, which is not on the roster — now ROOT_DIR_WATCH_HINTS, pinned in the self-test against the roots the corpus actually sweeps).

Changeset route

skip-changeset, by the workflow's own prescription: lint.yml states that "this PR edits a CI-internal script" is the textbook skip-changeset case — such a PR releases nothing. This PR is docs/adr/** + scripts/** + CI wiring and releases nothing user-facing. Label applied.


Carve-outs held OUT of this PR (ruling point 4)

Open questions for the reviewer

  1. The 「172 保持文件级」 reading (above) — floor, or ceiling? I took it as a floor and disambiguated all bare filenames to full paths, symbol-anchoring where a symbol resolves. If the intent was strictly file-level for all 172, that is a one-flag change to the migration.
  2. 61 stale full-path mentions in ADRs outside the census's 35 (e.g. packages/runtime/src/kernel-manager.ts in ADR-0004) — real rot of a different class, found while measuring the bare-path option. Recorded here, ⛔ not fixed, and not carded pending direction on whether it wants its own card.
  3. ADR-0020's object.stateMachines row now describes a map whose only trace in object.zod.ts is a comment saying it no longer exists. Plausibly that ADR's own decision landing rather than a defect, so no card — flagging it rather than deciding it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Mszp61SetNhQfVGvWh9bNn


Generated by Claude Code

…rs (#13556)
The #13556 census enumerated every line anchor in `docs/adr/**` — 343 across
35 of the 134 records, plus 52 continuation anchors — and resolved each against
main. Excluding 4 HISTORICAL and 2 EXTERNAL, 243 of 337 live anchors were
broken: 72.1%, declared as a one-way LOWER bound. Rot tracked target-file CHURN
rather than record age: the four hottest targets were 100% broken across 50
anchors. A rotted line anchor does not fail — it silently points a reader at
unrelated code inside the document whose purpose is to be a durable record.
Maintainer ruling 2026-09-01 (总监批 #27), option A: migrate the corpus to
symbol anchors behind a resolver gate that reds on an absent symbol, in ONE
migration with no transition period, sharing a single resolver with #13788.
- `scripts/symbol-anchors.mjs` — the SHARED core: grammar, extractor and the
resolution rule. A corpus is a `defineCorpus` registration, so #13788 joins
by registering rather than by forking the resolver.
- `scripts/check-adr-symbol-anchors.mjs` — the `docs/adr/**` registration.
- 469 occurrences rewritten across 35 records: 188 became symbol anchors (173
resolved at a declaration site, 15 as a quoted data identifier), 211 dropped
to file level, 15 to cross-repo file level, 10 dead paths lost their number,
24 duplicate or orphaned continuations were removed, and 6 HISTORICAL /
EXTERNAL anchors carry an inline exemption marker.
Resolution is deliberately stricter than the census: comments are stripped
before matching, so a symbol named only in a comment does not resolve, and a
token that merely repeats the anchor's own path is refused. That strictness is
what confirms 72.1% was a floor — `object.zod.ts` was credited with
`stateMachines` by the census, and the only occurrence today is a comment
saying that map no longer exists.
Nine spellings of a line reference are recognised, three of which the census's
own extractor could not see: comma and slash lists, the trailing `+` form, bare
anchors in running prose, fenced comment headers, and the tilde form.
⛔ No ADR's decision content changes; only anchor form does. ADR-0113's Context
row is known to state the inverse of today's mechanism — carded as #14193,
deliberately NOT repaired here, with a note in the record pointing at it.
Refs #13556
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mszp61SetNhQfVGvWh9bNn
`check-step-collectors --self-test` was red on this branch:
• lint.yml "ADR symbol anchors resolve (no line numbers survive)":
all green => every command runs (3/2)
The counts are (executed / collected), so three ran where two were collected —
not the reverse. That gate drives the real `run:` block under `bash -e` with a
STUB planted at each collected command's SCRIPT PATH, then holds "all green =>
every command runs" as an equality. The step ended with a bare
`node scripts/check-adr-symbol-anchors.mjs` after the collector's exit guard,
which re-invokes the very stub planted for that script's `--self-test` entry —
a third execution against two collected commands.
Split into two steps: the collector carries the two self-tests and nothing
else, and the production sweep is its own step. That is also the shape both
existing collectors in this file already have, and it restores the property
the split was for — the sweep now runs on its own footing instead of behind
the self-tests' `exit 1`.
⛔ No scope change: `docs/adr/**`, the resolver and its corpus registration are
untouched. Both invocations the gate's own `--self-test` pins in lint.yml are
still present, so the wiring assertion still holds.
Refs #13556
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mszp61SetNhQfVGvWh9bNn
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationsize/xlskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant

@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

docs(adr): migrate rotted ADR line anchors to resolver-gated symbol anchors (#13556) - #14277

Draft
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-13556-adr-symbol-anchors
Draft

docs(adr): migrate rotted ADR line anchors to resolver-gated symbol anchors (#13556)#14277
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-13556-adr-symbol-anchors

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13556

DRAFT, and it stays a draft.docs/adr/** is a governed surface: a human merges this. Never marked ready, never auto-merge.


The ruling this implements (verbatim)

Maintainer 2026-09-01, 逐字「同意」, 总监批 #27 — quoted from the director comment on the card:

  1. A:ADR 行号锚整体迁为符号锚 + resolver 门禁(缺符号变红)—— 与 platform-checklist step (2): re-author the 1,647 source citations as symbol anchors and add a resolver that reds on an absent symbol #13788 已裁方向同构,共享同一个 resolver,⛔ 不造第二套」
  2. C 不作过渡(不考虑渐进):243 个已证烂行号在 A 的一次迁移中同笔消失,⛔ 不先删再迁两道工;B 排除(片段照样烂);D 排除
  3. 普查即工单:343 锚(+52 CONTINUATION = 395 面)的分类表原样作实施输入 —— 11 个跨仓锚(objectui)迁为跨仓符号引用或降为文件级;172 个裸文件名锚保持文件级(无行号可烂);136 个无可查对象的按类降级或删;4 HISTORICAL / 2 EXTERNAL 豁免注记」
  4. ADR-0113 语义反转独立立卡…实施者查无则立,⛔ 不折进迁移 PR;ADR-0119 的陈旧反驳(turso-driver.ts 已存在)同扫」
  5. 受治面:docs/adr/** draft + 人工合并;72.1% 是单向误差下界的申报原样入册」

Premise re-verified before implementing. The census reproduces exactly on today's main: 376 raw tokens → 343 distinct anchors across 35 records, matching the census's own per-ADR counts. Nothing had been migrated.


The anchor grammar

Documented once in scripts/symbol-anchors.mjs's header (ANCHOR_GRAMMAR is that text, so a failure message cannot drift from the spec):

`<dir>/<file>.ts#<symbol>` symbol anchor — <symbol> must have a declaration site in that file
`<dir>/<file>.ts` file-level anchor — the file must exist
`objectui:<dir>/<file>.ts` cross-repo anchor — verified only when a checkout is available
`…#first`, `#second` continuation — inherits the path from the anchor before it
⛔ `<dir>/<file>.ts:4901` A LINE NUMBER IS NOT AN ANCHOR FORM.

It satisfies the three properties the form had to have: (a) mechanically verifiable — #symbol is checked against declaration sites, so an absent symbol is a loud red; (b) survives line churn — nothing encodes a position, so an unrelated edit above the target cannot rot it; (c) reads naturally — #fragment is the syntax a reader already knows from URLs, and it survives copy-paste into a GitHub link. Markdown links keep their link; only the rotted #L<n> fragment is dropped, so check-adr-links still cross-checks them.


The resolver's acceptance semantics — what "resolves" actually means

The census's caveat is binding and is why this is not includes(). Comments are stripped from the target before anything is matched, and a token that merely repeats the anchor's own path is refused outright (the census states this rule for its own classifier: otherwise every anchor "resolves" on the word registry / permission / metadata). Two acceptance classes, reported separately and never merged:

classwhat it acceptscount
declarationa declaration site in the target's language — function/class/interface/type/enum/namespace, a const/let/var binding, a member or object-literal key at the start of a line (name:, 'name':, name(, get name(), a named re-export; for markdown a heading (text or slug); for JSON/YAML a key173
literalthe symbol as a complete quoted string token ('sys_metadata'). Exists because much of this platform's vocabulary is DATA identifiers — object and field API names — declared as strings with no binding site. Whole-token only, never a substring15

literal is deliberately weaker and the migration prefers declaration wherever one exists.

⭐ This confirms the ruling's point 5 — 72.1% was a floor, and the direction is one-way. The census's mechanical test accepted a symbol appearing anywhere in the cited range, including inside a comment. Worked example found while migrating: the census credited object.zod.ts with stateMachines; on today's tree the only occurrence is a comment saying that map no longer exists. Under this resolver it correctly does not resolve, and the anchor drops to file level.

What the gate does NOT claim. It verifies an anchor points at something real. It cannot verify the sentence is true about that code — no static check can. That is a separate defect class with its own card (see the carve-outs).

One scope boundary, stated rather than hidden. Bare file-level path mentions are not judged in this corpus (checkBarePaths: false). Measured, not assumed: judging every bare path in docs/adr/** yields 1,056 findings across all 134 records, 616 distinct paths — abbreviated spellings, sibling-repo paths, and files that moved years ago. A real defect class, but not the line-anchor rot #13556 measured, and folding it in would bury this gate's signal under a corpus-wide cleanup nobody ruled on. It is a per-corpus flag, so a corpus with uniform citations (#13788) can switch it on.


Per-class counts, migrated vs the census

469 occurrences rewritten across 35 records.

dispositioncountnotes
→ symbol anchor188173 declaration, 15 literal
→ file-level anchor211the ruled downgrade where the sentence names nothing checkable
→ cross-repo file-level15objectui: prefix, target NAMED (see below)
dead path — number dropped10census's 9 DEAD PATH + 1
duplicate / orphaned continuations removed24see below
HISTORICAL / EXTERNAL exempt64 + 2, inline <!-- anchor-exempt: … -->
tilde-form bare numbers15a spelling the census did not count

Reconciliation with the census's 343 + 52 = 395. Non-continuation occurrences came to 376 raw — exactly the census's 376 — deduping to 340 under my key against the census's 343 (dedupe-key nuance: one comma-list occurrence covers what the census counted as separate rows). Continuations came to 78, not 52, and the tilde form added 15 more. The real surface was larger than 395, because three spellings the census's extractor could not see were found:

  • comma and slash listsregistry.ts:13,346-389, errors.zod.ts:29-39,147-152, engine.ts:2956/2991
  • cross-line continuations — the census's rule was same-line-only; in this corpus they run on across a paragraph
  • the tilde form~`326`, a bare backticked number with no path at all; ADR-0056 alone held 13

Nine spellings in total are now recognised, each provoked by name in the resolver's self-test.

Per-class dispositions from ruling point 3:

  • 11 cross-repo (objectui) → the ruled fallback. No objectui checkout is in tree, so they are downgraded to file-level anchors that NAME the cross-repo target (objectui:packages/types/src/layout.ts). The resolver verifies them only when $OBJECTUI_CHECKOUT is set; without it they are reported, never red — a check that reddens on the healthy case is the permanently-red gate this repo retired. (15 occurrences of the 11 distinct anchors.)
  • 172 bare-filename anchors → every one disambiguated to a full repo-root path, then symbol-anchored where a symbol resolves and file-level otherwise. ⚠️Reading declared: I read 「保持文件级」 as a floor for anchors with nothing checkable, not a ban on symbol-anchoring a bare-filename citation — because the adopted four-dimension analysis says under A 「172 个裸文件名锚被迫消歧,正是把模糊消灭在编写期」. Banning it would discard the locating power option A exists to preserve. Flagged for the reviewer.
  • 136 with no checkable object → downgraded to file level, or the number deleted where the path was dead.
  • 4 HISTORICAL / 2 EXTERNAL → annotated inline. ⛔ Adding an exemption is marked MAINTAINER-ONLY in the gate: it is the one remedy that makes the gate quieter instead of satisfying it.

24 continuations removed. A continuation landing on the same (file, symbol) as its parent says nothing new — it was a different line number in the same function, and once the position is gone so is the distinction; keeping them produced #installPackage/#installPackage/#installPackage. It can also be wrong: ADR-0119 reads "implemented by driver-sql (), driver-memory (:595+), and driver-mongodb (:545+)", where inheriting the preceding path would claim driver-memory's method lives in sql-driver.ts. The prose already names the right component; the anchor must not contradict it.


Negative control (evidence)

Run on the migrated tree, mutation reverted before commit — git status clean, verified.

1 — symbol mutated to a nonsense name (RESERVED_NAMESPACESRESERVED_NAMESPACEZZZ in ADR-0029):

BEFORE ✅ 1854 anchors … 0 line anchors survive. exit 0
AFTER ❌ check-adr-symbol-anchors: 1 finding(s) across 134 records.
[unresolved-symbol] docs/adr/0029-…:59 `packages/objectql/src/registry.ts#RESERVED_NAMESPACEZZZ`
`RESERVED_NAMESPACEZZZ` has no declaration site or string-literal token in
`packages/objectql/src/registry.ts`
exit 1

2 — a line anchor re-introduced (the migration's permanence, ruling point 2):

❌ [line-anchor] docs/adr/0029-…:59 packages/objectql/src/registry.ts:45
a line number is not an anchor form — cite the symbol
(`packages/objectql/src/registry.ts#symbol`) or drop to a file-level anchor
exit 1

Both restored; gate green again, git status --porcelain empty.


20-anchor spot audit

Deterministic sample (seed 13556) over the migrated symbol anchors, each read as ADR sentence vs the newly anchored declaration site. 19 of 20 point at the mechanism the sentence names.

#ADRanchordeclaration site
10130stack.zod.ts#validateSingleAppfunction validateSingleApp(
20117sharing-service.ts#OWNER_FIELDconst OWNER_FIELD = 'owner_id'
30055sharing-service.ts#buildReadFilterasync buildReadFilter(
40118ai-service.ts#ToolExecutionContextexport interface ToolExecutionContext {
50055security-plugin.ts#rlsFilterconst rlsFilter = await this.computeRlsFilter(
60053sql-driver.ts#formatInputmethod declaration
70029object.zod.ts#managedBymanagedBy: z.enum([…])
80029protocol.ts#restoreArtifactRegistryViewprivate async restoreArtifactRegistryView(
90029registry.ts#mergeObjectDefinitionsfunction mergeObjectDefinitions(
100057setup-nav.contributions.ts#group_people_orggroup: 'group_people_org', (literal)
110057team-graph.ts#expandRoleUsersasync expandRoleUsers(roleName, organizationId?)
120105execution-context.zod.ts#ExecutionContextexport type ExecutionContext =
130086security-plugin.ts#permissions⚠️the one miss — repaired, see below
140089field.zod.ts#requiredWhenrequiredWhen: ExpressionInputSchema…
150130registry.ts#RESERVED_NAMESPACESexport const RESERVED_NAMESPACES = new Set([…])
160126metadata-plugin.zod.ts#DEFAULT_METADATA_TYPE_REGISTRYexport const DEFAULT_METADATA_TYPE_REGISTRY:
170104field.zod.ts#LocationCoordinatesSchemaexport const LocationCoordinatesSchema = lazySchema(
180096security-plugin.ts#getReadFiltergetReadFilter: (object, context?) =>
190055security-plugin.ts#computeRlsFiltercomputeRlsFilter: (sets, o, engineOp, c) =>
200130registry.ts#unregisterObjectsByPackageunregisterObjectsByPackage(packageId, force)

The audit changed the output, which is the point of running it. An earlier pass had picked #opCtx (a bare function parameter), #automation for domains/automation.ts (a token straight off the anchor's own path — the census's named trap), and #permissions/#description/#properties (too generic to locate anything). The path-derived-token rule and a measured weak-token list were added, and every one of those was re-picked or dropped to file level; rows 5 and 19 above are the repaired #opCtx case.

Row 13 is the residual: security-plugin.ts#permissions resolved only to a local const permissions inside an unrelated function. The sentence is locating bootstrapDeclaredRoles, the "exact sibling" it names — and that symbol is not in that file (the census's DEAD PATH verdict on bootstrap-declared-roles.ts says the same from the other side). Repaired to a file-level anchor; ⛔ the sentence itself is untouched.


The #13788 reuse seam

Ruling: 「共享同一个 resolver,⛔ 不造第二套」. The split is structural, not a convention:

  • scripts/symbol-anchors.mjs — grammar, extractor, resolution rule, defineCorpus, sweepCorpus. Sweeps nothing on its own and knows about no corpus.
  • scripts/check-adr-symbol-anchors.mjs — a defineCorpus({ id, label, docRoots, docPattern, crossRepos, checkBarePaths }) call plus a population declaration and an exit contract. 241 lines, almost all of it header.

#13788 joins by adding one registration for the platform-checklist corpus. The knobs it will need already exist and are exercised: docPattern (its corpus is .mdx), crossRepos, and checkBarePaths (its citations are uniform, so it can turn the bare-path check on and get it for free). Why one resolver, mechanically: the expensive part is the resolution rule, and two copies drift silently — each gate stays green on its own corpus while meaning something different by "resolves". One implementation, one self-test.

⚠️Name note:scripts/check-adr-anchors.mjs already exists and is an unrelated gate (code → ADR citations, the #3723 direction). This is the opposite direction — ADR → code — hence check-adr-symbol-anchors.


Wiring — the gate RUNS

Registered the way sibling gates are, following check-system-context-census:

Checks run

pnpm lint ✅ · symbol-anchors --self-test ✅ · check-adr-symbol-anchors --self-test ✅ (every finding class provoked on a synthetic corpus, healthy anchors silent, population live, wiring pinned) · gate on the migrated tree ✅ 1,854 anchors across 134 records, 0 line anchors survive

Derived from node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, plus the two mandatory for any scripts/** touch:

check-ratchet-remedy-authority ✅ · check:declared-population-live ✅ · check-watch-hint-literal ✅ · check-step-collectors ✅ · check-self-test-wired ✅ · check-adr-links ✅ · check-adr-anchors ✅ · check-doc-anchors ✅ · check-doc-frontmatter ✅ · check-docs-single-h1 ✅ · check-ci-filter-parity ✅ · check-aggregator-roster ✅ · check-required-contexts ✅ · check-adr-0087-registration ✅ · check-empty-changeset ✅ · check-changeset-no-major ✅ · check-self-test-workflow-commands ✅ · check-workflow-status-functions ✅ · check-keyed-text-bounds

Two gates legitimately failed against the new code and were fixed, not worked around: check-declared-population-live (the shared core declared a path-shaped literal in its grammar examples that named no tracked file — the examples are now <dir>/<file>.ts placeholders and the module carries a dispatch-gates: no-path-population marker, since a corpus supplies its own roots) and check-watch-hint-literal (the declaration was spelled WATCH_HINTS, which is not on the roster — now ROOT_DIR_WATCH_HINTS, pinned in the self-test against the roots the corpus actually sweeps).

Changeset route

skip-changeset, by the workflow's own prescription: lint.yml states that "this PR edits a CI-internal script" is the textbook skip-changeset case — such a PR releases nothing. This PR is docs/adr/** + scripts/** + CI wiring and releases nothing user-facing. Label applied.


Carve-outs held OUT of this PR (ruling point 4)

Open questions for the reviewer

  1. The 「172 保持文件级」 reading (above) — floor, or ceiling? I took it as a floor and disambiguated all bare filenames to full paths, symbol-anchoring where a symbol resolves. If the intent was strictly file-level for all 172, that is a one-flag change to the migration.
  2. 61 stale full-path mentions in ADRs outside the census's 35 (e.g. packages/runtime/src/kernel-manager.ts in ADR-0004) — real rot of a different class, found while measuring the bare-path option. Recorded here, ⛔ not fixed, and not carded pending direction on whether it wants its own card.
  3. ADR-0020's object.stateMachines row now describes a map whose only trace in object.zod.ts is a comment saying it no longer exists. Plausibly that ADR's own decision landing rather than a defect, so no card — flagging it rather than deciding it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Mszp61SetNhQfVGvWh9bNn


Generated by Claude Code

…rs (#13556)
The #13556 census enumerated every line anchor in `docs/adr/**` — 343 across
35 of the 134 records, plus 52 continuation anchors — and resolved each against
main. Excluding 4 HISTORICAL and 2 EXTERNAL, 243 of 337 live anchors were
broken: 72.1%, declared as a one-way LOWER bound. Rot tracked target-file CHURN
rather than record age: the four hottest targets were 100% broken across 50
anchors. A rotted line anchor does not fail — it silently points a reader at
unrelated code inside the document whose purpose is to be a durable record.
Maintainer ruling 2026-09-01 (总监批 #27), option A: migrate the corpus to
symbol anchors behind a resolver gate that reds on an absent symbol, in ONE
migration with no transition period, sharing a single resolver with #13788.
- `scripts/symbol-anchors.mjs` — the SHARED core: grammar, extractor and the
resolution rule. A corpus is a `defineCorpus` registration, so #13788 joins
by registering rather than by forking the resolver.
- `scripts/check-adr-symbol-anchors.mjs` — the `docs/adr/**` registration.
- 469 occurrences rewritten across 35 records: 188 became symbol anchors (173
resolved at a declaration site, 15 as a quoted data identifier), 211 dropped
to file level, 15 to cross-repo file level, 10 dead paths lost their number,
24 duplicate or orphaned continuations were removed, and 6 HISTORICAL /
EXTERNAL anchors carry an inline exemption marker.
Resolution is deliberately stricter than the census: comments are stripped
before matching, so a symbol named only in a comment does not resolve, and a
token that merely repeats the anchor's own path is refused. That strictness is
what confirms 72.1% was a floor — `object.zod.ts` was credited with
`stateMachines` by the census, and the only occurrence today is a comment
saying that map no longer exists.
Nine spellings of a line reference are recognised, three of which the census's
own extractor could not see: comma and slash lists, the trailing `+` form, bare
anchors in running prose, fenced comment headers, and the tilde form.
⛔ No ADR's decision content changes; only anchor form does. ADR-0113's Context
row is known to state the inverse of today's mechanism — carded as #14193,
deliberately NOT repaired here, with a note in the record pointing at it.
Refs #13556
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mszp61SetNhQfVGvWh9bNn
`check-step-collectors --self-test` was red on this branch:
• lint.yml "ADR symbol anchors resolve (no line numbers survive)":
all green => every command runs (3/2)
The counts are (executed / collected), so three ran where two were collected —
not the reverse. That gate drives the real `run:` block under `bash -e` with a
STUB planted at each collected command's SCRIPT PATH, then holds "all green =>
every command runs" as an equality. The step ended with a bare
`node scripts/check-adr-symbol-anchors.mjs` after the collector's exit guard,
which re-invokes the very stub planted for that script's `--self-test` entry —
a third execution against two collected commands.
Split into two steps: the collector carries the two self-tests and nothing
else, and the production sweep is its own step. That is also the shape both
existing collectors in this file already have, and it restores the property
the split was for — the sweep now runs on its own footing instead of behind
the self-tests' `exit 1`.
⛔ No scope change: `docs/adr/**`, the resolver and its corpus registration are
untouched. Both invocations the gate's own `--self-test` pins in lint.yml are
still present, so the wiring assertion still holds.
Refs #13556
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mszp61SetNhQfVGvWh9bNn
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationsize/xlskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant

@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

docs(adr): migrate rotted ADR line anchors to resolver-gated symbol anchors (#13556) - #14277

Draft
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-13556-adr-symbol-anchors
Draft

docs(adr): migrate rotted ADR line anchors to resolver-gated symbol anchors (#13556)#14277
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-13556-adr-symbol-anchors

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13556

DRAFT, and it stays a draft.docs/adr/** is a governed surface: a human merges this. Never marked ready, never auto-merge.


The ruling this implements (verbatim)

Maintainer 2026-09-01, 逐字「同意」, 总监批 #27 — quoted from the director comment on the card:

  1. A:ADR 行号锚整体迁为符号锚 + resolver 门禁(缺符号变红)—— 与 platform-checklist step (2): re-author the 1,647 source citations as symbol anchors and add a resolver that reds on an absent symbol #13788 已裁方向同构,共享同一个 resolver,⛔ 不造第二套」
  2. C 不作过渡(不考虑渐进):243 个已证烂行号在 A 的一次迁移中同笔消失,⛔ 不先删再迁两道工;B 排除(片段照样烂);D 排除
  3. 普查即工单:343 锚(+52 CONTINUATION = 395 面)的分类表原样作实施输入 —— 11 个跨仓锚(objectui)迁为跨仓符号引用或降为文件级;172 个裸文件名锚保持文件级(无行号可烂);136 个无可查对象的按类降级或删;4 HISTORICAL / 2 EXTERNAL 豁免注记」
  4. ADR-0113 语义反转独立立卡…实施者查无则立,⛔ 不折进迁移 PR;ADR-0119 的陈旧反驳(turso-driver.ts 已存在)同扫」
  5. 受治面:docs/adr/** draft + 人工合并;72.1% 是单向误差下界的申报原样入册」

Premise re-verified before implementing. The census reproduces exactly on today's main: 376 raw tokens → 343 distinct anchors across 35 records, matching the census's own per-ADR counts. Nothing had been migrated.


The anchor grammar

Documented once in scripts/symbol-anchors.mjs's header (ANCHOR_GRAMMAR is that text, so a failure message cannot drift from the spec):

`<dir>/<file>.ts#<symbol>` symbol anchor — <symbol> must have a declaration site in that file
`<dir>/<file>.ts` file-level anchor — the file must exist
`objectui:<dir>/<file>.ts` cross-repo anchor — verified only when a checkout is available
`…#first`, `#second` continuation — inherits the path from the anchor before it
⛔ `<dir>/<file>.ts:4901` A LINE NUMBER IS NOT AN ANCHOR FORM.

It satisfies the three properties the form had to have: (a) mechanically verifiable — #symbol is checked against declaration sites, so an absent symbol is a loud red; (b) survives line churn — nothing encodes a position, so an unrelated edit above the target cannot rot it; (c) reads naturally — #fragment is the syntax a reader already knows from URLs, and it survives copy-paste into a GitHub link. Markdown links keep their link; only the rotted #L<n> fragment is dropped, so check-adr-links still cross-checks them.


The resolver's acceptance semantics — what "resolves" actually means

The census's caveat is binding and is why this is not includes(). Comments are stripped from the target before anything is matched, and a token that merely repeats the anchor's own path is refused outright (the census states this rule for its own classifier: otherwise every anchor "resolves" on the word registry / permission / metadata). Two acceptance classes, reported separately and never merged:

classwhat it acceptscount
declarationa declaration site in the target's language — function/class/interface/type/enum/namespace, a const/let/var binding, a member or object-literal key at the start of a line (name:, 'name':, name(, get name(), a named re-export; for markdown a heading (text or slug); for JSON/YAML a key173
literalthe symbol as a complete quoted string token ('sys_metadata'). Exists because much of this platform's vocabulary is DATA identifiers — object and field API names — declared as strings with no binding site. Whole-token only, never a substring15

literal is deliberately weaker and the migration prefers declaration wherever one exists.

⭐ This confirms the ruling's point 5 — 72.1% was a floor, and the direction is one-way. The census's mechanical test accepted a symbol appearing anywhere in the cited range, including inside a comment. Worked example found while migrating: the census credited object.zod.ts with stateMachines; on today's tree the only occurrence is a comment saying that map no longer exists. Under this resolver it correctly does not resolve, and the anchor drops to file level.

What the gate does NOT claim. It verifies an anchor points at something real. It cannot verify the sentence is true about that code — no static check can. That is a separate defect class with its own card (see the carve-outs).

One scope boundary, stated rather than hidden. Bare file-level path mentions are not judged in this corpus (checkBarePaths: false). Measured, not assumed: judging every bare path in docs/adr/** yields 1,056 findings across all 134 records, 616 distinct paths — abbreviated spellings, sibling-repo paths, and files that moved years ago. A real defect class, but not the line-anchor rot #13556 measured, and folding it in would bury this gate's signal under a corpus-wide cleanup nobody ruled on. It is a per-corpus flag, so a corpus with uniform citations (#13788) can switch it on.


Per-class counts, migrated vs the census

469 occurrences rewritten across 35 records.

dispositioncountnotes
→ symbol anchor188173 declaration, 15 literal
→ file-level anchor211the ruled downgrade where the sentence names nothing checkable
→ cross-repo file-level15objectui: prefix, target NAMED (see below)
dead path — number dropped10census's 9 DEAD PATH + 1
duplicate / orphaned continuations removed24see below
HISTORICAL / EXTERNAL exempt64 + 2, inline <!-- anchor-exempt: … -->
tilde-form bare numbers15a spelling the census did not count

Reconciliation with the census's 343 + 52 = 395. Non-continuation occurrences came to 376 raw — exactly the census's 376 — deduping to 340 under my key against the census's 343 (dedupe-key nuance: one comma-list occurrence covers what the census counted as separate rows). Continuations came to 78, not 52, and the tilde form added 15 more. The real surface was larger than 395, because three spellings the census's extractor could not see were found:

  • comma and slash listsregistry.ts:13,346-389, errors.zod.ts:29-39,147-152, engine.ts:2956/2991
  • cross-line continuations — the census's rule was same-line-only; in this corpus they run on across a paragraph
  • the tilde form~`326`, a bare backticked number with no path at all; ADR-0056 alone held 13

Nine spellings in total are now recognised, each provoked by name in the resolver's self-test.

Per-class dispositions from ruling point 3:

  • 11 cross-repo (objectui) → the ruled fallback. No objectui checkout is in tree, so they are downgraded to file-level anchors that NAME the cross-repo target (objectui:packages/types/src/layout.ts). The resolver verifies them only when $OBJECTUI_CHECKOUT is set; without it they are reported, never red — a check that reddens on the healthy case is the permanently-red gate this repo retired. (15 occurrences of the 11 distinct anchors.)
  • 172 bare-filename anchors → every one disambiguated to a full repo-root path, then symbol-anchored where a symbol resolves and file-level otherwise. ⚠️Reading declared: I read 「保持文件级」 as a floor for anchors with nothing checkable, not a ban on symbol-anchoring a bare-filename citation — because the adopted four-dimension analysis says under A 「172 个裸文件名锚被迫消歧,正是把模糊消灭在编写期」. Banning it would discard the locating power option A exists to preserve. Flagged for the reviewer.
  • 136 with no checkable object → downgraded to file level, or the number deleted where the path was dead.
  • 4 HISTORICAL / 2 EXTERNAL → annotated inline. ⛔ Adding an exemption is marked MAINTAINER-ONLY in the gate: it is the one remedy that makes the gate quieter instead of satisfying it.

24 continuations removed. A continuation landing on the same (file, symbol) as its parent says nothing new — it was a different line number in the same function, and once the position is gone so is the distinction; keeping them produced #installPackage/#installPackage/#installPackage. It can also be wrong: ADR-0119 reads "implemented by driver-sql (), driver-memory (:595+), and driver-mongodb (:545+)", where inheriting the preceding path would claim driver-memory's method lives in sql-driver.ts. The prose already names the right component; the anchor must not contradict it.


Negative control (evidence)

Run on the migrated tree, mutation reverted before commit — git status clean, verified.

1 — symbol mutated to a nonsense name (RESERVED_NAMESPACESRESERVED_NAMESPACEZZZ in ADR-0029):

BEFORE ✅ 1854 anchors … 0 line anchors survive. exit 0
AFTER ❌ check-adr-symbol-anchors: 1 finding(s) across 134 records.
[unresolved-symbol] docs/adr/0029-…:59 `packages/objectql/src/registry.ts#RESERVED_NAMESPACEZZZ`
`RESERVED_NAMESPACEZZZ` has no declaration site or string-literal token in
`packages/objectql/src/registry.ts`
exit 1

2 — a line anchor re-introduced (the migration's permanence, ruling point 2):

❌ [line-anchor] docs/adr/0029-…:59 packages/objectql/src/registry.ts:45
a line number is not an anchor form — cite the symbol
(`packages/objectql/src/registry.ts#symbol`) or drop to a file-level anchor
exit 1

Both restored; gate green again, git status --porcelain empty.


20-anchor spot audit

Deterministic sample (seed 13556) over the migrated symbol anchors, each read as ADR sentence vs the newly anchored declaration site. 19 of 20 point at the mechanism the sentence names.

#ADRanchordeclaration site
10130stack.zod.ts#validateSingleAppfunction validateSingleApp(
20117sharing-service.ts#OWNER_FIELDconst OWNER_FIELD = 'owner_id'
30055sharing-service.ts#buildReadFilterasync buildReadFilter(
40118ai-service.ts#ToolExecutionContextexport interface ToolExecutionContext {
50055security-plugin.ts#rlsFilterconst rlsFilter = await this.computeRlsFilter(
60053sql-driver.ts#formatInputmethod declaration
70029object.zod.ts#managedBymanagedBy: z.enum([…])
80029protocol.ts#restoreArtifactRegistryViewprivate async restoreArtifactRegistryView(
90029registry.ts#mergeObjectDefinitionsfunction mergeObjectDefinitions(
100057setup-nav.contributions.ts#group_people_orggroup: 'group_people_org', (literal)
110057team-graph.ts#expandRoleUsersasync expandRoleUsers(roleName, organizationId?)
120105execution-context.zod.ts#ExecutionContextexport type ExecutionContext =
130086security-plugin.ts#permissions⚠️the one miss — repaired, see below
140089field.zod.ts#requiredWhenrequiredWhen: ExpressionInputSchema…
150130registry.ts#RESERVED_NAMESPACESexport const RESERVED_NAMESPACES = new Set([…])
160126metadata-plugin.zod.ts#DEFAULT_METADATA_TYPE_REGISTRYexport const DEFAULT_METADATA_TYPE_REGISTRY:
170104field.zod.ts#LocationCoordinatesSchemaexport const LocationCoordinatesSchema = lazySchema(
180096security-plugin.ts#getReadFiltergetReadFilter: (object, context?) =>
190055security-plugin.ts#computeRlsFiltercomputeRlsFilter: (sets, o, engineOp, c) =>
200130registry.ts#unregisterObjectsByPackageunregisterObjectsByPackage(packageId, force)

The audit changed the output, which is the point of running it. An earlier pass had picked #opCtx (a bare function parameter), #automation for domains/automation.ts (a token straight off the anchor's own path — the census's named trap), and #permissions/#description/#properties (too generic to locate anything). The path-derived-token rule and a measured weak-token list were added, and every one of those was re-picked or dropped to file level; rows 5 and 19 above are the repaired #opCtx case.

Row 13 is the residual: security-plugin.ts#permissions resolved only to a local const permissions inside an unrelated function. The sentence is locating bootstrapDeclaredRoles, the "exact sibling" it names — and that symbol is not in that file (the census's DEAD PATH verdict on bootstrap-declared-roles.ts says the same from the other side). Repaired to a file-level anchor; ⛔ the sentence itself is untouched.


The #13788 reuse seam

Ruling: 「共享同一个 resolver,⛔ 不造第二套」. The split is structural, not a convention:

  • scripts/symbol-anchors.mjs — grammar, extractor, resolution rule, defineCorpus, sweepCorpus. Sweeps nothing on its own and knows about no corpus.
  • scripts/check-adr-symbol-anchors.mjs — a defineCorpus({ id, label, docRoots, docPattern, crossRepos, checkBarePaths }) call plus a population declaration and an exit contract. 241 lines, almost all of it header.

#13788 joins by adding one registration for the platform-checklist corpus. The knobs it will need already exist and are exercised: docPattern (its corpus is .mdx), crossRepos, and checkBarePaths (its citations are uniform, so it can turn the bare-path check on and get it for free). Why one resolver, mechanically: the expensive part is the resolution rule, and two copies drift silently — each gate stays green on its own corpus while meaning something different by "resolves". One implementation, one self-test.

⚠️Name note:scripts/check-adr-anchors.mjs already exists and is an unrelated gate (code → ADR citations, the #3723 direction). This is the opposite direction — ADR → code — hence check-adr-symbol-anchors.


Wiring — the gate RUNS

Registered the way sibling gates are, following check-system-context-census:

Checks run

pnpm lint ✅ · symbol-anchors --self-test ✅ · check-adr-symbol-anchors --self-test ✅ (every finding class provoked on a synthetic corpus, healthy anchors silent, population live, wiring pinned) · gate on the migrated tree ✅ 1,854 anchors across 134 records, 0 line anchors survive

Derived from node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, plus the two mandatory for any scripts/** touch:

check-ratchet-remedy-authority ✅ · check:declared-population-live ✅ · check-watch-hint-literal ✅ · check-step-collectors ✅ · check-self-test-wired ✅ · check-adr-links ✅ · check-adr-anchors ✅ · check-doc-anchors ✅ · check-doc-frontmatter ✅ · check-docs-single-h1 ✅ · check-ci-filter-parity ✅ · check-aggregator-roster ✅ · check-required-contexts ✅ · check-adr-0087-registration ✅ · check-empty-changeset ✅ · check-changeset-no-major ✅ · check-self-test-workflow-commands ✅ · check-workflow-status-functions ✅ · check-keyed-text-bounds

Two gates legitimately failed against the new code and were fixed, not worked around: check-declared-population-live (the shared core declared a path-shaped literal in its grammar examples that named no tracked file — the examples are now <dir>/<file>.ts placeholders and the module carries a dispatch-gates: no-path-population marker, since a corpus supplies its own roots) and check-watch-hint-literal (the declaration was spelled WATCH_HINTS, which is not on the roster — now ROOT_DIR_WATCH_HINTS, pinned in the self-test against the roots the corpus actually sweeps).

Changeset route

skip-changeset, by the workflow's own prescription: lint.yml states that "this PR edits a CI-internal script" is the textbook skip-changeset case — such a PR releases nothing. This PR is docs/adr/** + scripts/** + CI wiring and releases nothing user-facing. Label applied.


Carve-outs held OUT of this PR (ruling point 4)

Open questions for the reviewer

  1. The 「172 保持文件级」 reading (above) — floor, or ceiling? I took it as a floor and disambiguated all bare filenames to full paths, symbol-anchoring where a symbol resolves. If the intent was strictly file-level for all 172, that is a one-flag change to the migration.
  2. 61 stale full-path mentions in ADRs outside the census's 35 (e.g. packages/runtime/src/kernel-manager.ts in ADR-0004) — real rot of a different class, found while measuring the bare-path option. Recorded here, ⛔ not fixed, and not carded pending direction on whether it wants its own card.
  3. ADR-0020's object.stateMachines row now describes a map whose only trace in object.zod.ts is a comment saying it no longer exists. Plausibly that ADR's own decision landing rather than a defect, so no card — flagging it rather than deciding it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Mszp61SetNhQfVGvWh9bNn


Generated by Claude Code

…rs (#13556)
The #13556 census enumerated every line anchor in `docs/adr/**` — 343 across
35 of the 134 records, plus 52 continuation anchors — and resolved each against
main. Excluding 4 HISTORICAL and 2 EXTERNAL, 243 of 337 live anchors were
broken: 72.1%, declared as a one-way LOWER bound. Rot tracked target-file CHURN
rather than record age: the four hottest targets were 100% broken across 50
anchors. A rotted line anchor does not fail — it silently points a reader at
unrelated code inside the document whose purpose is to be a durable record.
Maintainer ruling 2026-09-01 (总监批 #27), option A: migrate the corpus to
symbol anchors behind a resolver gate that reds on an absent symbol, in ONE
migration with no transition period, sharing a single resolver with #13788.
- `scripts/symbol-anchors.mjs` — the SHARED core: grammar, extractor and the
resolution rule. A corpus is a `defineCorpus` registration, so #13788 joins
by registering rather than by forking the resolver.
- `scripts/check-adr-symbol-anchors.mjs` — the `docs/adr/**` registration.
- 469 occurrences rewritten across 35 records: 188 became symbol anchors (173
resolved at a declaration site, 15 as a quoted data identifier), 211 dropped
to file level, 15 to cross-repo file level, 10 dead paths lost their number,
24 duplicate or orphaned continuations were removed, and 6 HISTORICAL /
EXTERNAL anchors carry an inline exemption marker.
Resolution is deliberately stricter than the census: comments are stripped
before matching, so a symbol named only in a comment does not resolve, and a
token that merely repeats the anchor's own path is refused. That strictness is
what confirms 72.1% was a floor — `object.zod.ts` was credited with
`stateMachines` by the census, and the only occurrence today is a comment
saying that map no longer exists.
Nine spellings of a line reference are recognised, three of which the census's
own extractor could not see: comma and slash lists, the trailing `+` form, bare
anchors in running prose, fenced comment headers, and the tilde form.
⛔ No ADR's decision content changes; only anchor form does. ADR-0113's Context
row is known to state the inverse of today's mechanism — carded as #14193,
deliberately NOT repaired here, with a note in the record pointing at it.
Refs #13556
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mszp61SetNhQfVGvWh9bNn
`check-step-collectors --self-test` was red on this branch:
• lint.yml "ADR symbol anchors resolve (no line numbers survive)":
all green => every command runs (3/2)
The counts are (executed / collected), so three ran where two were collected —
not the reverse. That gate drives the real `run:` block under `bash -e` with a
STUB planted at each collected command's SCRIPT PATH, then holds "all green =>
every command runs" as an equality. The step ended with a bare
`node scripts/check-adr-symbol-anchors.mjs` after the collector's exit guard,
which re-invokes the very stub planted for that script's `--self-test` entry —
a third execution against two collected commands.
Split into two steps: the collector carries the two self-tests and nothing
else, and the production sweep is its own step. That is also the shape both
existing collectors in this file already have, and it restores the property
the split was for — the sweep now runs on its own footing instead of behind
the self-tests' `exit 1`.
⛔ No scope change: `docs/adr/**`, the resolver and its corpus registration are
untouched. Both invocations the gate's own `--self-test` pins in lint.yml are
still present, so the wiring assertion still holds.
Refs #13556
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mszp61SetNhQfVGvWh9bNn
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationsize/xlskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant

@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

docs(adr): migrate rotted ADR line anchors to resolver-gated symbol anchors (#13556) - #14277

Draft
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-13556-adr-symbol-anchors
Draft

docs(adr): migrate rotted ADR line anchors to resolver-gated symbol anchors (#13556)#14277
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-13556-adr-symbol-anchors

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13556

DRAFT, and it stays a draft.docs/adr/** is a governed surface: a human merges this. Never marked ready, never auto-merge.


The ruling this implements (verbatim)

Maintainer 2026-09-01, 逐字「同意」, 总监批 #27 — quoted from the director comment on the card:

  1. A:ADR 行号锚整体迁为符号锚 + resolver 门禁(缺符号变红)—— 与 platform-checklist step (2): re-author the 1,647 source citations as symbol anchors and add a resolver that reds on an absent symbol #13788 已裁方向同构,共享同一个 resolver,⛔ 不造第二套」
  2. C 不作过渡(不考虑渐进):243 个已证烂行号在 A 的一次迁移中同笔消失,⛔ 不先删再迁两道工;B 排除(片段照样烂);D 排除
  3. 普查即工单:343 锚(+52 CONTINUATION = 395 面)的分类表原样作实施输入 —— 11 个跨仓锚(objectui)迁为跨仓符号引用或降为文件级;172 个裸文件名锚保持文件级(无行号可烂);136 个无可查对象的按类降级或删;4 HISTORICAL / 2 EXTERNAL 豁免注记」
  4. ADR-0113 语义反转独立立卡…实施者查无则立,⛔ 不折进迁移 PR;ADR-0119 的陈旧反驳(turso-driver.ts 已存在)同扫」
  5. 受治面:docs/adr/** draft + 人工合并;72.1% 是单向误差下界的申报原样入册」

Premise re-verified before implementing. The census reproduces exactly on today's main: 376 raw tokens → 343 distinct anchors across 35 records, matching the census's own per-ADR counts. Nothing had been migrated.


The anchor grammar

Documented once in scripts/symbol-anchors.mjs's header (ANCHOR_GRAMMAR is that text, so a failure message cannot drift from the spec):

`<dir>/<file>.ts#<symbol>` symbol anchor — <symbol> must have a declaration site in that file
`<dir>/<file>.ts` file-level anchor — the file must exist
`objectui:<dir>/<file>.ts` cross-repo anchor — verified only when a checkout is available
`…#first`, `#second` continuation — inherits the path from the anchor before it
⛔ `<dir>/<file>.ts:4901` A LINE NUMBER IS NOT AN ANCHOR FORM.

It satisfies the three properties the form had to have: (a) mechanically verifiable — #symbol is checked against declaration sites, so an absent symbol is a loud red; (b) survives line churn — nothing encodes a position, so an unrelated edit above the target cannot rot it; (c) reads naturally — #fragment is the syntax a reader already knows from URLs, and it survives copy-paste into a GitHub link. Markdown links keep their link; only the rotted #L<n> fragment is dropped, so check-adr-links still cross-checks them.


The resolver's acceptance semantics — what "resolves" actually means

The census's caveat is binding and is why this is not includes(). Comments are stripped from the target before anything is matched, and a token that merely repeats the anchor's own path is refused outright (the census states this rule for its own classifier: otherwise every anchor "resolves" on the word registry / permission / metadata). Two acceptance classes, reported separately and never merged:

classwhat it acceptscount
declarationa declaration site in the target's language — function/class/interface/type/enum/namespace, a const/let/var binding, a member or object-literal key at the start of a line (name:, 'name':, name(, get name(), a named re-export; for markdown a heading (text or slug); for JSON/YAML a key173
literalthe symbol as a complete quoted string token ('sys_metadata'). Exists because much of this platform's vocabulary is DATA identifiers — object and field API names — declared as strings with no binding site. Whole-token only, never a substring15

literal is deliberately weaker and the migration prefers declaration wherever one exists.

⭐ This confirms the ruling's point 5 — 72.1% was a floor, and the direction is one-way. The census's mechanical test accepted a symbol appearing anywhere in the cited range, including inside a comment. Worked example found while migrating: the census credited object.zod.ts with stateMachines; on today's tree the only occurrence is a comment saying that map no longer exists. Under this resolver it correctly does not resolve, and the anchor drops to file level.

What the gate does NOT claim. It verifies an anchor points at something real. It cannot verify the sentence is true about that code — no static check can. That is a separate defect class with its own card (see the carve-outs).

One scope boundary, stated rather than hidden. Bare file-level path mentions are not judged in this corpus (checkBarePaths: false). Measured, not assumed: judging every bare path in docs/adr/** yields 1,056 findings across all 134 records, 616 distinct paths — abbreviated spellings, sibling-repo paths, and files that moved years ago. A real defect class, but not the line-anchor rot #13556 measured, and folding it in would bury this gate's signal under a corpus-wide cleanup nobody ruled on. It is a per-corpus flag, so a corpus with uniform citations (#13788) can switch it on.


Per-class counts, migrated vs the census

469 occurrences rewritten across 35 records.

dispositioncountnotes
→ symbol anchor188173 declaration, 15 literal
→ file-level anchor211the ruled downgrade where the sentence names nothing checkable
→ cross-repo file-level15objectui: prefix, target NAMED (see below)
dead path — number dropped10census's 9 DEAD PATH + 1
duplicate / orphaned continuations removed24see below
HISTORICAL / EXTERNAL exempt64 + 2, inline <!-- anchor-exempt: … -->
tilde-form bare numbers15a spelling the census did not count

Reconciliation with the census's 343 + 52 = 395. Non-continuation occurrences came to 376 raw — exactly the census's 376 — deduping to 340 under my key against the census's 343 (dedupe-key nuance: one comma-list occurrence covers what the census counted as separate rows). Continuations came to 78, not 52, and the tilde form added 15 more. The real surface was larger than 395, because three spellings the census's extractor could not see were found:

  • comma and slash listsregistry.ts:13,346-389, errors.zod.ts:29-39,147-152, engine.ts:2956/2991
  • cross-line continuations — the census's rule was same-line-only; in this corpus they run on across a paragraph
  • the tilde form~`326`, a bare backticked number with no path at all; ADR-0056 alone held 13

Nine spellings in total are now recognised, each provoked by name in the resolver's self-test.

Per-class dispositions from ruling point 3:

  • 11 cross-repo (objectui) → the ruled fallback. No objectui checkout is in tree, so they are downgraded to file-level anchors that NAME the cross-repo target (objectui:packages/types/src/layout.ts). The resolver verifies them only when $OBJECTUI_CHECKOUT is set; without it they are reported, never red — a check that reddens on the healthy case is the permanently-red gate this repo retired. (15 occurrences of the 11 distinct anchors.)
  • 172 bare-filename anchors → every one disambiguated to a full repo-root path, then symbol-anchored where a symbol resolves and file-level otherwise. ⚠️Reading declared: I read 「保持文件级」 as a floor for anchors with nothing checkable, not a ban on symbol-anchoring a bare-filename citation — because the adopted four-dimension analysis says under A 「172 个裸文件名锚被迫消歧,正是把模糊消灭在编写期」. Banning it would discard the locating power option A exists to preserve. Flagged for the reviewer.
  • 136 with no checkable object → downgraded to file level, or the number deleted where the path was dead.
  • 4 HISTORICAL / 2 EXTERNAL → annotated inline. ⛔ Adding an exemption is marked MAINTAINER-ONLY in the gate: it is the one remedy that makes the gate quieter instead of satisfying it.

24 continuations removed. A continuation landing on the same (file, symbol) as its parent says nothing new — it was a different line number in the same function, and once the position is gone so is the distinction; keeping them produced #installPackage/#installPackage/#installPackage. It can also be wrong: ADR-0119 reads "implemented by driver-sql (), driver-memory (:595+), and driver-mongodb (:545+)", where inheriting the preceding path would claim driver-memory's method lives in sql-driver.ts. The prose already names the right component; the anchor must not contradict it.


Negative control (evidence)

Run on the migrated tree, mutation reverted before commit — git status clean, verified.

1 — symbol mutated to a nonsense name (RESERVED_NAMESPACESRESERVED_NAMESPACEZZZ in ADR-0029):

BEFORE ✅ 1854 anchors … 0 line anchors survive. exit 0
AFTER ❌ check-adr-symbol-anchors: 1 finding(s) across 134 records.
[unresolved-symbol] docs/adr/0029-…:59 `packages/objectql/src/registry.ts#RESERVED_NAMESPACEZZZ`
`RESERVED_NAMESPACEZZZ` has no declaration site or string-literal token in
`packages/objectql/src/registry.ts`
exit 1

2 — a line anchor re-introduced (the migration's permanence, ruling point 2):

❌ [line-anchor] docs/adr/0029-…:59 packages/objectql/src/registry.ts:45
a line number is not an anchor form — cite the symbol
(`packages/objectql/src/registry.ts#symbol`) or drop to a file-level anchor
exit 1

Both restored; gate green again, git status --porcelain empty.


20-anchor spot audit

Deterministic sample (seed 13556) over the migrated symbol anchors, each read as ADR sentence vs the newly anchored declaration site. 19 of 20 point at the mechanism the sentence names.

#ADRanchordeclaration site
10130stack.zod.ts#validateSingleAppfunction validateSingleApp(
20117sharing-service.ts#OWNER_FIELDconst OWNER_FIELD = 'owner_id'
30055sharing-service.ts#buildReadFilterasync buildReadFilter(
40118ai-service.ts#ToolExecutionContextexport interface ToolExecutionContext {
50055security-plugin.ts#rlsFilterconst rlsFilter = await this.computeRlsFilter(
60053sql-driver.ts#formatInputmethod declaration
70029object.zod.ts#managedBymanagedBy: z.enum([…])
80029protocol.ts#restoreArtifactRegistryViewprivate async restoreArtifactRegistryView(
90029registry.ts#mergeObjectDefinitionsfunction mergeObjectDefinitions(
100057setup-nav.contributions.ts#group_people_orggroup: 'group_people_org', (literal)
110057team-graph.ts#expandRoleUsersasync expandRoleUsers(roleName, organizationId?)
120105execution-context.zod.ts#ExecutionContextexport type ExecutionContext =
130086security-plugin.ts#permissions⚠️the one miss — repaired, see below
140089field.zod.ts#requiredWhenrequiredWhen: ExpressionInputSchema…
150130registry.ts#RESERVED_NAMESPACESexport const RESERVED_NAMESPACES = new Set([…])
160126metadata-plugin.zod.ts#DEFAULT_METADATA_TYPE_REGISTRYexport const DEFAULT_METADATA_TYPE_REGISTRY:
170104field.zod.ts#LocationCoordinatesSchemaexport const LocationCoordinatesSchema = lazySchema(
180096security-plugin.ts#getReadFiltergetReadFilter: (object, context?) =>
190055security-plugin.ts#computeRlsFiltercomputeRlsFilter: (sets, o, engineOp, c) =>
200130registry.ts#unregisterObjectsByPackageunregisterObjectsByPackage(packageId, force)

The audit changed the output, which is the point of running it. An earlier pass had picked #opCtx (a bare function parameter), #automation for domains/automation.ts (a token straight off the anchor's own path — the census's named trap), and #permissions/#description/#properties (too generic to locate anything). The path-derived-token rule and a measured weak-token list were added, and every one of those was re-picked or dropped to file level; rows 5 and 19 above are the repaired #opCtx case.

Row 13 is the residual: security-plugin.ts#permissions resolved only to a local const permissions inside an unrelated function. The sentence is locating bootstrapDeclaredRoles, the "exact sibling" it names — and that symbol is not in that file (the census's DEAD PATH verdict on bootstrap-declared-roles.ts says the same from the other side). Repaired to a file-level anchor; ⛔ the sentence itself is untouched.


The #13788 reuse seam

Ruling: 「共享同一个 resolver,⛔ 不造第二套」. The split is structural, not a convention:

  • scripts/symbol-anchors.mjs — grammar, extractor, resolution rule, defineCorpus, sweepCorpus. Sweeps nothing on its own and knows about no corpus.
  • scripts/check-adr-symbol-anchors.mjs — a defineCorpus({ id, label, docRoots, docPattern, crossRepos, checkBarePaths }) call plus a population declaration and an exit contract. 241 lines, almost all of it header.

#13788 joins by adding one registration for the platform-checklist corpus. The knobs it will need already exist and are exercised: docPattern (its corpus is .mdx), crossRepos, and checkBarePaths (its citations are uniform, so it can turn the bare-path check on and get it for free). Why one resolver, mechanically: the expensive part is the resolution rule, and two copies drift silently — each gate stays green on its own corpus while meaning something different by "resolves". One implementation, one self-test.

⚠️Name note:scripts/check-adr-anchors.mjs already exists and is an unrelated gate (code → ADR citations, the #3723 direction). This is the opposite direction — ADR → code — hence check-adr-symbol-anchors.


Wiring — the gate RUNS

Registered the way sibling gates are, following check-system-context-census:

Checks run

pnpm lint ✅ · symbol-anchors --self-test ✅ · check-adr-symbol-anchors --self-test ✅ (every finding class provoked on a synthetic corpus, healthy anchors silent, population live, wiring pinned) · gate on the migrated tree ✅ 1,854 anchors across 134 records, 0 line anchors survive

Derived from node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, plus the two mandatory for any scripts/** touch:

check-ratchet-remedy-authority ✅ · check:declared-population-live ✅ · check-watch-hint-literal ✅ · check-step-collectors ✅ · check-self-test-wired ✅ · check-adr-links ✅ · check-adr-anchors ✅ · check-doc-anchors ✅ · check-doc-frontmatter ✅ · check-docs-single-h1 ✅ · check-ci-filter-parity ✅ · check-aggregator-roster ✅ · check-required-contexts ✅ · check-adr-0087-registration ✅ · check-empty-changeset ✅ · check-changeset-no-major ✅ · check-self-test-workflow-commands ✅ · check-workflow-status-functions ✅ · check-keyed-text-bounds

Two gates legitimately failed against the new code and were fixed, not worked around: check-declared-population-live (the shared core declared a path-shaped literal in its grammar examples that named no tracked file — the examples are now <dir>/<file>.ts placeholders and the module carries a dispatch-gates: no-path-population marker, since a corpus supplies its own roots) and check-watch-hint-literal (the declaration was spelled WATCH_HINTS, which is not on the roster — now ROOT_DIR_WATCH_HINTS, pinned in the self-test against the roots the corpus actually sweeps).

Changeset route

skip-changeset, by the workflow's own prescription: lint.yml states that "this PR edits a CI-internal script" is the textbook skip-changeset case — such a PR releases nothing. This PR is docs/adr/** + scripts/** + CI wiring and releases nothing user-facing. Label applied.


Carve-outs held OUT of this PR (ruling point 4)

Open questions for the reviewer

  1. The 「172 保持文件级」 reading (above) — floor, or ceiling? I took it as a floor and disambiguated all bare filenames to full paths, symbol-anchoring where a symbol resolves. If the intent was strictly file-level for all 172, that is a one-flag change to the migration.
  2. 61 stale full-path mentions in ADRs outside the census's 35 (e.g. packages/runtime/src/kernel-manager.ts in ADR-0004) — real rot of a different class, found while measuring the bare-path option. Recorded here, ⛔ not fixed, and not carded pending direction on whether it wants its own card.
  3. ADR-0020's object.stateMachines row now describes a map whose only trace in object.zod.ts is a comment saying it no longer exists. Plausibly that ADR's own decision landing rather than a defect, so no card — flagging it rather than deciding it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Mszp61SetNhQfVGvWh9bNn


Generated by Claude Code

…rs (#13556)
The #13556 census enumerated every line anchor in `docs/adr/**` — 343 across
35 of the 134 records, plus 52 continuation anchors — and resolved each against
main. Excluding 4 HISTORICAL and 2 EXTERNAL, 243 of 337 live anchors were
broken: 72.1%, declared as a one-way LOWER bound. Rot tracked target-file CHURN
rather than record age: the four hottest targets were 100% broken across 50
anchors. A rotted line anchor does not fail — it silently points a reader at
unrelated code inside the document whose purpose is to be a durable record.
Maintainer ruling 2026-09-01 (总监批 #27), option A: migrate the corpus to
symbol anchors behind a resolver gate that reds on an absent symbol, in ONE
migration with no transition period, sharing a single resolver with #13788.
- `scripts/symbol-anchors.mjs` — the SHARED core: grammar, extractor and the
resolution rule. A corpus is a `defineCorpus` registration, so #13788 joins
by registering rather than by forking the resolver.
- `scripts/check-adr-symbol-anchors.mjs` — the `docs/adr/**` registration.
- 469 occurrences rewritten across 35 records: 188 became symbol anchors (173
resolved at a declaration site, 15 as a quoted data identifier), 211 dropped
to file level, 15 to cross-repo file level, 10 dead paths lost their number,
24 duplicate or orphaned continuations were removed, and 6 HISTORICAL /
EXTERNAL anchors carry an inline exemption marker.
Resolution is deliberately stricter than the census: comments are stripped
before matching, so a symbol named only in a comment does not resolve, and a
token that merely repeats the anchor's own path is refused. That strictness is
what confirms 72.1% was a floor — `object.zod.ts` was credited with
`stateMachines` by the census, and the only occurrence today is a comment
saying that map no longer exists.
Nine spellings of a line reference are recognised, three of which the census's
own extractor could not see: comma and slash lists, the trailing `+` form, bare
anchors in running prose, fenced comment headers, and the tilde form.
⛔ No ADR's decision content changes; only anchor form does. ADR-0113's Context
row is known to state the inverse of today's mechanism — carded as #14193,
deliberately NOT repaired here, with a note in the record pointing at it.
Refs #13556
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mszp61SetNhQfVGvWh9bNn
`check-step-collectors --self-test` was red on this branch:
• lint.yml "ADR symbol anchors resolve (no line numbers survive)":
all green => every command runs (3/2)
The counts are (executed / collected), so three ran where two were collected —
not the reverse. That gate drives the real `run:` block under `bash -e` with a
STUB planted at each collected command's SCRIPT PATH, then holds "all green =>
every command runs" as an equality. The step ended with a bare
`node scripts/check-adr-symbol-anchors.mjs` after the collector's exit guard,
which re-invokes the very stub planted for that script's `--self-test` entry —
a third execution against two collected commands.
Split into two steps: the collector carries the two self-tests and nothing
else, and the production sweep is its own step. That is also the shape both
existing collectors in this file already have, and it restores the property
the split was for — the sweep now runs on its own footing instead of behind
the self-tests' `exit 1`.
⛔ No scope change: `docs/adr/**`, the resolver and its corpus registration are
untouched. Both invocations the gate's own `--self-test` pins in lint.yml are
still present, so the wiring assertion still holds.
Refs #13556
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mszp61SetNhQfVGvWh9bNn
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationsize/xlskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant

@claude