Skip to content

docs(devx): anchor the isSystem census page by symbol, not by line number - #16614

Merged
baozhoutao merged 7 commits into
mainfrom
claude/issue-15921-system-context-symbol-anchors
Sep 7, 2026
Merged

docs(devx): anchor the isSystem census page by symbol, not by line number#16614
baozhoutao merged 7 commits into
mainfrom
claude/issue-15921-system-context-symbol-anchors

Conversation

@claude

@claude claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #15921

The census page stops living on line numbers. Every anchor it writes is now a path#symbol citation resolved by the repo's shared symbol-anchor resolver, and the gate's population check becomes per file at symbol granularity.

Implements decision batch #77 (comment 5567940532) on this card: batch #56's option A in direction, amended in its population half, which the measured tree cannot satisfy position-free.

What the ruling asked for, and where each item landed

1. Anchors migrate to path#symbol, file-level only where no declaration can be named, resolved through the resolver behind check-adr-symbol-anchors as a thin corpus registration — no widening of scripts/symbol-anchors.mjs.

scripts/symbol-anchors.mjs is UNTOUCHED by this diff (git diff --stat below shows three files, and it is not one of them). check-system-context-census.mjs gains a defineCorpus call — CORPUS, docRoots: ['content/docs/permissions'], docPattern naming this one file, checkBarePaths: true — and calls sweepCorpus once. Resolution is delegated whole: the gate re-decides none of it, and a sweep it could not run is a refusal ([no-sweep]), not a skip.

One file-level anchor exists, and it is the honest case rather than a shortfall: packages/plugins/plugin-auth/src/last-admin-guard.ts, whose cited claim lives in the module docblock, which no declaration encloses. Every other citation names a real symbol.

2. The population check becomes per file. --fix stops rewriting line numbers.

Per file: every file the census finds a read in carries at least one anchor, and the SET of symbols the page cites into that file equals the set the census plus NON_READ_ANCHORS require.

One deliberate strengthening, declared rather than slipped in: the ruling names a COUNT equality, and this ships SET equality, which is the same predicate plus the ability to name which symbol is missing. A count that only says "5 against 6" cannot tell an author what to write; the refusal carries both counts anyway, in the same sentence. The literal count-of-READS reading is not what shipped and could not be: it is unsatisfiable on the nine collision files that are the reason the rule became per-file at all.

--fix writes nothing. It is not silently accepted either — a --fix that writes nothing and exits 0 reads exactly like a repair that worked, so it prints what it did not do and why, then returns the ordinary verdict. gen:system-context-census and scripts/regen-artifacts.mjs both stay wired and stay honest.

3. The precision loss is written where a reader meets the anchors.

On the page, in the Callout at the top (before the table, so nobody reaches a row without it) and again in "What CI holds", where it is priced with the numbers: 9 of the 45 anchored files hold more than one read inside a single symbol. Both numbers are ENFORCED by new DECLARED_COUNTS entries, so the sentence that prices the loss cannot quietly stop being true in either direction. The gate's own header carries the same statement, and the self-test pins the gap as a PASSING case so the next reader finds it declared instead of rediscovering it as a defect.

4. Option A (a span-aware resolver) is not refused and is not folded in. Recorded on the page and in the gate header as its own card, to be taken if the gap is ever measured to have let a deletion through. This diff does not widen the shared resolver.

5. Serial and the #15927 answer. Re-confirmed against the actual diff, below.

The numbers, re-derived on this tree (⛔ every figure in the card's thread was stale)

Derived with the repo's own instruments, never a regex. scripts/doc-line-anchors.mjs#extractLineAnchors over the page as it stood on the merge base f2f6684cd5:

141 anchor occurrences   96 full · 42 continuation · 3 range-end
 61 distinct spellings
 57 resolved files
133 distinct file:line targets
  7 path citations with no line (5 distinct)

scripts/isystem-census.mjs, same tree:

106 elevation read sites · 20 packages · 45 files
 89 distinct enclosing symbols          <- what a page CAN anchor
  9 files hold more than one read inside one symbol
  0 sites with no nameable symbol

The nine collision files, re-verified (mechanism assumption 2 in the dispatch — it holds, and it is the reason the population rule is per file):

file reads symbols
packages/objectql/src/engine.ts 10 9
packages/plugins/plugin-approvals/src/approval-service.ts 8 8
packages/plugins/plugin-reports/src/report-service.ts 7 6
packages/plugins/plugin-security/src/security-plugin.ts 7 5
packages/rest/src/rest-server.ts 6 2
packages/plugins/plugin-sharing/src/share-link-service.ts 5 3
packages/plugins/plugin-audit/src/comment-access-hooks.ts 4 2
packages/services/service-storage/src/attachment-access-hooks.ts 4 2
packages/runtime/src/domains/meta.ts 3 1
packages/rest/src/package-routes.ts 2 1

(Ten rows: approval-service.ts is listed for completeness and is NOT one of the nine — its 8 reads live in 8 distinct symbols.)

After the migration, on the merged head:

106 sites · 20 packages · 45 files · 89 symbols
127 symbol anchors + 8 file-level citations on the page
103 distinct symbols cited == 103 required (89 census + 14 ledger-only)
 22 NON_READ_ANCHORS rows (was 27), 7 of them declared `collapsesOntoRead`
  1 file-level ledger row

The ledger is re-keyed, and the overlap is DECLARED

NON_READ_ANCHORS rows were keyed by needle, a literal of source text that every reformatting moved. They are now { file, symbol } — the same pair the page writes — and the gate asks the SHARED resolver whether the file still declares the symbol, so the ledger and the page can never mean different things by "the symbol is there".

Symbol granularity created a case the needle version did not have: a citation can share its symbol with a census read site. The owner_id guard block and the short-circuit that skips it are both inside packages/plugins/plugin-security/src/security-plugin.ts#start. Such a row stops EXCUSING anything while its why and its rowSeams are still worth keeping, so it declares collapsesOntoRead: true, and the gate refuses when the declaration and the census disagree in EITHER direction. An undeclared overlap reads as a row that excuses an anchor when it does not; a declared overlap that has ended is a row nobody re-examined.

Red-first evidence — the pair the ruling names, on the real tree

Both legs ran on the actual repository against the committed implementation, each mutation proved on disk before the gate was read, each restore proved by git diff HEAD and a blob-hash comparison against the HEAD blob. Full transcript in the report comment on #15921.

A pure line move must NOT red. 40 lines inserted above packages/objectql/src/engine.ts, the file carrying the most anchors:

on-disk proof: '// ablation padding' lines 0 -> 40
on-disk proof: engine.ts blob 1cead9fce99e -> b918910be42c
LINE-MOVE gate exit=0
check-system-context-census: OK — 106 elevation read sites ... 
restored: git diff HEAD empty? [] hash=1cead9fce99e head=1cead9fce99e

A symbol rename must RED. getReadFilter renamed in security-plugin.ts, page untouched:

on-disk proof: old spelling 20 -> 0; new spelling 0 -> 20
RENAME gate exit=1
[unresolved-symbol]        ...security-plugin.ts#getReadFilter -- no declaration site
[site-without-a-row]       ...security-plugin.ts#getReadFilterRenamedForAblation holds 1 elevation read(s)
[anchor-is-not-a-read-site] the page anchors ...security-plugin.ts#getReadFilter
restored: git diff HEAD empty? [] hash=84ae26d2d588 head=84ae26d2d588

Three findings from three different checks — resolution, population and classification each see it independently.

And the BEFORE control, so the property is shown to be new rather than asserted. The base version of the page and both scripts checked out into the same tree, the SAME 40-line insertion applied:

BEFORE, unmutated tree:      exit=0
BEFORE, SAME pure line move: exit=1, 28 findings
  [site-without-a-row] packages/objectql/src/engine.ts:10455 ...
  [site-without-a-row] packages/objectql/src/engine.ts:11807 ...
  [site-without-a-row] packages/objectql/src/engine.ts:11990 ...

Same tree, same mutation, opposite verdicts. That is the whole of what the migration buys.

Ruling item 5, re-confirmed against THIS diff

#15927 is about the two PAGE_ROW_REFERENCES entries declared unheld — two prose sentences whose wording derives no seam key. Re-checked after the change, not before it:

#15927 is UNAFFECTED, confirmed after the change rather than before it. It also remains blocked on its own hold condition, and this PR is one more open PR touching the page.

Verification

node scripts/check-system-context-census.mjs              exit 0
node scripts/check-system-context-census.mjs --self-test   exit 0, 73 cases, 18 batteries
node scripts/check-system-context-census.mjs --fix          exit 0, wrote nothing (git status clean)
node scripts/isystem-census.mjs                             106 sites / 20 packages / 45 files

Derived gate families, reconciled: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 62 families; all 62 run; --ran reports 62 derived famil(ies) accounted for — 62 run, 0 NOT-MEASURED. Four of them first answered PREREQUISITE NOT MET (unbuilt @objectstack/lint / formula / client-react), were re-run after building those packages, and passed.

Repo-wide lint, NOT narrowed — the full set ran and there was nothing to narrow: pnpm exec eslint . --no-inline-config --format json at 6db5435952 linted 6291 files, 0 errors, 0 warnings.

turbo ls --affected against the merge base reports 0 packages — this diff is scripts/** and content/docs/** only, so no package test or typecheck is owed.

Changeset

No .changeset/*.md: this diff publishes nothing from any released package (one docs page, two repo scripts). Batch #56's ruling said skip-changeset applies and batch #77 does not move that. ⛔ The label is deliberately NOT applied by this seat — the dispatch reserved it for the PM. That instruction conflicts with the repo's standing clause for os-dev (which makes labelling the executor's step); the conflict is raised in the report's open_questions rather than settled here.

Out of scope, filed

#16612 — once this page carries no line numbers, the os-regen mixed: 'line-anchors' comparator (blankAnchorLineNumbers) becomes the IDENTITY, so the routing's stated safety argument and three prose blocks go stale together. Measured: base 141 line anchors, comparator not the identity; this branch 0 line anchors, comparator IS the identity. ⛔ Not repaired here — the dispatch fenced the merge driver off by name, and the choice among "retire mixed", "move to NOT_DRIVER_MANAGED" and "fix only the prose" is a maintainer's.

⚠️ Expect at least one os-regen lap

This branch touches a merge=os-regen routed path, so it meets the driver on every main move. The merge already in this branch went through scripts/pm/os-regen-merge.sh (step 2 correctly KEPT the branch's bytes — the branch changed the page and main did not). That script and the driver are unchanged by this diff.


os-regen lap 2 (merged 68437d4d95, head a3b057a9c8) — and one real defect it exposed

main moved and the branch went dirty. The cause was PR #16587, whose only touch to the files this card owns was two rows of this page, both of them pure line-number re-anchorings of domains/automation.ts produced by the OLD --fix (989 to 1079, and 254 / 545 / 635 to 255 / 546 / 636).

Main's side carried nothing this branch needs, and that is measured, not assumed. The repo's own comparator for exactly this question says so:

merge-base page: line anchors 141
origin/main page: line anchors 141
branch page:      line anchors 0
main vs merge-base differ ONLY in anchor line numbers: true   (blankAnchorLineNumbers)
raw bytes differ: true

So main's edit is entirely inside the generated half, and that half no longer exists here. scripts/pm/os-regen-merge.sh step 2 correctly reported "both sides changed it" and took main's side into the worktree; its own runbook sentence covers this case — "if the branch's edit here was a HAND edit that no regeneration reproduces, restore the branch bytes before regenerating" — and this branch's page is exactly that, because --fix no longer regenerates anything. The merge commit keeps the branch's page bytes; nothing was lost in either direction.

#16587 changed nothing the census measures. Re-derived on the merged tree:

106 sites · 20 packages · 45 files · 89 symbols · 9 collapsing files      (identical)
packages/runtime/src/domains/automation.ts
  sites=4  symbols=mayReadRunState, refuseUngrantedFlowWrite, refuseUnrelatedScreenRead, handleAutomationRequest
  site lines 255 / 546 / 636 / 1079   (were 254 / 545 / 635 / 989)

Four reads moved down the file and none moved between symbols, arrived or vanished. That is precisely the class of change this card exists to stop caring about: the page needed no edit, and the gate is green with zero findings.

The defect the lap exposed, and its fix

The pre-commit hook runs this gate, and this branch's new red-first battery builds a throwaway git corpus. It ran git init and git add -A with the AMBIENT environment — and under a hook git has exported GIT_DIR, GIT_WORK_TREE and GIT_INDEX_FILE. So the throwaway repository was never created and the real repository's index was written instead: 8,190 paths staged as deleted, with every self-test case still printing ok. Recovered with git reset --hard onto the merge commit, which already held the correct tree.

Fixed in a3b057a9c8: the self-test strips every GIT_* key before its first case, the corpus builder passes the stripped environment to each child explicitly, and a regression pin injects a bogus GIT_DIR, builds the corpus under it, and requires the temp tree to come back with its own two files staged. The measured incident is written into the helper's header so the next reader does not rediscover it.

The fix is proved by the same path that broke: the commit carrying it went through pre-commit, the hook ran the gate and its self-test, the marker cleared (system-context.mdx — current), and git status --porcelain afterwards is empty.

⚠️ Stated rather than papered over: sweepCorpus resolves through git ls-files and passes no environment of its own, so a sweep of a SYNTHETIC root inherits the caller's GIT_DIR. Every in-repo caller is unaffected — the inherited values name this repository, which is the right answer — so this is not widened here (the ruling forbids touching the shared resolver); the caveat is recorded beside the CORPUS registration for whoever registers the next corpus.

Verification at a3b057a9c8

node scripts/check-system-context-census.mjs             exit 0, same OK line, zero findings
node scripts/check-system-context-census.mjs --self-test  exit 0, 74 cases, 18 batteries
dispatch-gates --commands                                 62 families, byte-identical to lap 1
dispatch-gates --ran                                      62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN
pnpm exec eslint . --no-inline-config                     6293 files, 0 errors, 0 warnings
driver-free mergeability probe (bare shared clone)        exit 0 — merges clean with no custom driver

🤖 Generated with Claude Code

https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8


Generated by Claude Code

@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation labels Sep 7, 2026
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 7, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 7, 2026 12:57
@baozhoutao
baozhoutao enabled auto-merge September 7, 2026 12:58
…nvironment

Its throwaway red-first corpus ran `git init` / `git add -A` with the ambient
environment. Under `pre-commit` — where `check-regen-pending` invokes this gate,
and where an os-regen merge lap lands — git has exported GIT_DIR, GIT_WORK_TREE
and GIT_INDEX_FILE, so the corpus was never created and the repository's own
index was written instead: 8,190 paths staged as deleted, with every self-test
case still printing ok.

The self-test now strips every GIT_* key before its first case and the corpus
builder passes the stripped environment explicitly. A regression pin injects a
bogus GIT_DIR and requires the throwaway tree to come back with its own two
files staged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 7, 2026
Merged via the queue into main with commit 55bbd92 Sep 7, 2026
35 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-15921-system-context-symbol-anchors branch September 7, 2026 13:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants