Skip to content

fix(devx): compare the census+ledger UNION in --fix, so a shifted citation is not a population change - #13575

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-13490-census-fix-false-population
Aug 31, 2026
Merged

fix(devx): compare the census+ledger UNION in --fix, so a shifted citation is not a population change#13575
os-project-manager merged 1 commit into
mainfrom
claude/issue-13490-census-fix-false-population

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes #13490

The defect, in one sentence

--fix decided whether a page anchor was a read anchor or a ledger-excused citation by asking whether its line is one NON_READ_ANCHORS currently locates — and those two numbers live in different coordinate systems. The page's anchors are pre-shift by construction (rot is the only reason --fix is running); a ledger row locates itself by NEEDLE in the current tree. So a file whose ledger-excused citation also moved had that citation counted as a read anchor, and the gate announced a POPULATION change over a population that never moved.

Reproduced exactly, then fixed

The domain:services displacement table from PR #13514, replayed on this tree by inserting 20 lines above security-plugin.ts:1392 and deleting one line between 2491 and 3808 — mutation proved on disk by marker count and git hash-object against the HEAD blob, restored the same way.

Census lines after the displacement, byte-identical to the reported table:

1392 → 1412  1414 → 1434  1565 → 1585  2491 → 2511   (+20)
3808 → 3827  4295 → 4314  4446 → 4465  4524 → 4543   (+19)
before this PR after
gate 16 problem(s) over 145 anchors and 109 census sites same
--fix page anchors 8 distinct read line(s), census finds 7 · 0 rewritten 9 re-anchored, 0 refused
gate after --fix still red OK — 109 elevation read sites in 20 packages across 45 files, all anchored; 145 anchors resolve, 27 declared non-read

The nine rewrites are exactly the renumbers the other lane had to hand-apply.

⚠️ Correction to the card's evidence. The domain:services comment annotates 4524 → 4543 as "the NON_READ_ANCHORS row-2 needle". It is not: 4524 is a real census read (if (context?.isSystem) return true;), and the row-2 needle (3.5. [#3004]) is at 2491 → 2511. Both are in the table, so the reproduction and the diagnosis are unaffected — only the annotation was wrong.

The safety property is measured, not assumed

Three genuine population changes, each mutation proved on disk, each still REFUSED with 0 anchors rewritten and the gate left red:

mutation refusal says the set difference names
a read site ARRIVES 8 anchors vs 9 anchorable (8 reads + 1 citation) already anchored 8 of 9 · target, NO anchor: 4525
ARRIVES and everything displaces 8 vs 9 already anchored 0 of 9 — uniform displacement and a count mismatch
a read site is DELETED 8 vs 7 already anchored 7 of 7 · anchor, NO target: 4524

That last column is the card's central complaint answered: an author can now tell a displacement from an arrival from the refusal itself, instead of running isystem-census.mjs --json in two trees by hand.

⭐ A second failure direction, previously unreported

Ablating the fix (old counting arm restored, new cases kept) turned up the half nobody had measured: the old arm did not only refuse falsely, it also silently ACCEPTED a real population change. When a read site arrives while the ledger citation is stale, the miscounted citation makes the two counts agree by cancellation — refused comes back empty and --fix rewrites the ledger citation onto the newly arrived elevation read site, reporting it as a successful re-anchor on a security-documentation page. The refusal H3 calls the safety property was defeated by the same defect, in the direction that leaves no error message.

Why the union, and not "count the reads correctly"

Two candidate shapes, and they differ:

  • compare page read anchors against census reads alone — accepts when the reads match even if a ledger citation was added or dropped without the page following;
  • compare the page's distinct anchor lines against the UNION of both classes.

The union is the safer one and is what landed. It also buys a postcondition the per-class comparison cannot state: the rewrite is a bijection from the page's distinct anchor lines onto the file's anchorable lines, so for every file --fix touches, evaluate is clean by construction — no missing site, no unexplained anchor, no unused ledger row. That is pinned behaviourally, not argued in a comment.

It also removes the miscount in the dangerous direction, which exclusion-by-line got wrong too: a stale READ anchor that lands on a line the ledger now occupies was subtracted, the counts could agree by cancellation, and the rewrite mapped the survivors onto each other's rows — a page that is wrong and GREEN, because both classes stay covered. That crossing is real: on the second occurrence rest-server.ts:1267 was simultaneously the second inbound seam's new home and a read row's stale anchor.

⛔ Stated rather than papered over: alignment is by ORDER, so a pure displacement is reconstructed exactly, but a REORDERING that moves a cited construct past another one inside the same file is indistinguishable from a shift on line numbers alone. No line-only tool can tell those apart, and evaluate cannot either. That limit is written into the code.

Self-test

Seven new cases (35 total, was 28). Ablation: with the old counting arm restored and the new cases kept — mutation proved on disk by injected-marker count 1, live describeRefusal call count 0, and a blob hash differing from HEAD; restored and proved by git diff HEAD empty plus a matching blob hash — six of the seven go red:

FAIL FIX 13490: a shift that moves the LEDGER citation too is a shift, not a population change
FAIL FIX 13490: what --fix rewrote evaluates clean
FAIL FIX 13490: a citation crossing a read anchor keeps each ROW on its own line, not merely covered
FAIL FIX 13490: a site that ARRIVES while the citation shifts is still REFUSED
FAIL FIX 13490: the refusal states BOTH counts it compared and the ledger it set aside
FAIL FIX 13490: the refusal names the set difference, not just a count

The seventh (an unanchored site in the crossing file is REFUSED too) stays green under ablation by design — it is the safety control that must hold in both arms, so it is not expected to discriminate.

Scope

Only the counting arm of --fix, plus its refusal message and self-test. NON_READ_ANCHORS is untouched — it behaved correctly throughout both occurrences, locating by needle and re-resolving on its own. scripts/isystem-census.mjs is untouched (#13548's fence).

Verification, at afd593b48

  • node scripts/check-system-context-census.mjs --self-testall cases passed (35 cases)
  • node scripts/check-system-context-census.mjsOK — 109 elevation read sites in 20 packages across 45 files, all anchored; 145 anchors resolve, 27 declared non-read
  • The 13 families scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack derives from the real change set, plus both convention-triggered obligations for editing a gate script (bare-root-worklist.mjs --self-test, check:pm-dispatch-gates) — all exit 0
  • pnpm lint (full repo, 5542-file ESLint population read from its own config) under the shared verify lock → VERDICT command-exit 0
  • check:nul-bytes exit 0, plus a direct control-byte scan of the diff: no hits
  • check-test-completeness.mjs exit 3 = NOT MEASURED, by the gate's own words — neither red nor green

No changeset: this publishes from no package. Repo precedent for scripts/-only gate changes is skip-changeset, which is applied to this PR.


Generated by Claude Code

…ation is not a population change

`check-system-context-census --fix` partitioned a file's page anchors into read
anchors and ledger-excused citations by LINE: an anchor was a read anchor unless
it sat on a line `NON_READ_ANCHORS` currently locates. That compares two
different coordinate systems. The page's anchors are pre-shift by construction --
rot is the only reason `--fix` runs -- while a ledger row locates itself by
NEEDLE in the current tree. So a file whose ledger-excused citation also moved
had that citation counted as a read anchor, and the gate reported a POPULATION
change over a population that never moved.

Measured twice, in two lanes, on two files: `security-plugin.ts` (7 reads + 1
citation, all displaced +20/+19 by an unrelated bootstrap edit, zero `isSystem`
lines added or removed) refused with "page anchors 8 distinct read line(s),
census finds 7"; `rest-server.ts` (6 + 2) with "7 ... finds 6", the contradicting
`[ledger-row-unused]` line sitting in the same run's output both times.

The remedy compares the page's DISTINCT anchor lines against the UNION of the two
classes of line the page may anchor -- census read sites and ledger citations --
and aligns them by order. That removes the miscount in both directions, including
the dangerous one, and buys a postcondition the per-class comparison cannot
state: the rewrite is a bijection onto the file's anchorable lines, so every file
`--fix` touches evaluates clean by construction.

⛔ The refusal itself is unchanged as a safety property: a site that genuinely
arrives or vanishes is still refused, now with both compared counts, both target
classes and the set difference printed, so an author can tell a displacement from
an arrival without running the census in two trees by hand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants