Skip to content

feat(devx): hold the census page's row N references to the table by SEAM, and refuse when the numbering moves under them - #15896

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-15869-census-row-references-held
Sep 5, 2026
Merged

feat(devx): hold the census page's row N references to the table by SEAM, and refuse when the numbering moves under them#15896
os-project-manager merged 2 commits into
mainfrom
claude/issue-15869-census-row-references-held

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Part of #15869

The census page's row N references are now held to the table by the seam they name, not by the number they carry. Scripts-only: scripts/check-system-context-census.mjs, +743/-3.

What was unheld

check-system-context-census held every file:line anchor on the page to the tree — that is what --fix rewrites and what the gate verified. Two other hand-maintained claims on the same page were held by nothing:

  1. the page's own prose row references (Row 34 is correct for the rule, row 50's doors, what row 30 is skipping, rows 1–61 above);
  2. the why: strings in the script's own NON_READ_ANCHORS, which name a row by number (:318, :323, :328, :333, :338, :343, :348, :353).

So a row inserted into the behaviour table renumbered everything below it and silently falsified every reference past the insertion point, under a green gate. That happened (PR #15687, three references at once) and it un-happened by coincidence (PR #15395, the same three), with nothing observing either event. This PR is about the mechanism; today's three values are true again and are not what is being fixed.

The mechanism: keyed by SEAM, never by number

A ledger recording "the reference at line L says row 34" would rot in the very edit that falsifies the reference — the ledger and the sentence would go stale together and agree while both were wrong.

So each reference declares the seam it is about: a literal that must occur in exactly one numbered row of the page's tables. The gate resolves the seam, reads the number the reference actually carries, and refuses when they disagree — naming the reference, the number it carries, and the row the key resolves to. Renumbering moves the seam's row and the refusal names the new number; the ledger itself never has to be renumbered.

Seams come from the referring sentence's own subject, never picked to make a number come out right. Two new declarations carry it:

  • PAGE_ROW_REFERENCES — one entry per row N on the page; context (a number-free literal that must match exactly one page line) plus exactly one of seam, section (for a range: the extent of a section, which is mechanical rather than editorial) or unheld (with the reason no key is derivable).
  • NON_READ_ANCHORS[].rowSeams — the seams for the row N mentions in that entry's why:, in the order they are written. A why: that names a row and declares no seam is refused.

Population, not just resolution: every row N spelling on the page must be claimed by exactly one entry. A reference nobody declared is a refusal naming the line — a ledger of what exists today can never see the reference someone writes tomorrow.

Every spelling on the page, enumerated with counts

12 occurrences on 12 distinct lines (broad scan: grep -noiE '[a-z-]*row[s]?[^a-z0-9]{0,4}[0-9]+'), in four spellings — Row N (2), row N (5), row N's (2), rows N–M (3) — plus a fifth, row-N, which occurs only inside a why: string. Each, and how it resolves on this tree:

page line reference key resolves to
:90 rows 2–6 UNHELD (see below)
:91 row 1 seam The whole security middleware short-circuits row 1 (:90)
:162 row 50 seam `manage_metadata` bypassed on metadata writes row 50 (:161)
:171 row 2 seam `owner_id` is not auto-stamped on INSERT row 2 (:91)
:212 Row 2 seam `owner_id` is not auto-stamped on INSERT row 2 (:91)
:238 row 30 seam on the record-`afterDelete` hook row 30 (:131)
:247 Row 21 seam Strict-drop refusal never fires row 21 (:115)
:251 Row 34 seam `revoke()` deletes directly row 34 (:135)
:259 row 2 seam `owner_id` is not auto-stamped on INSERT row 2 (:91)
:334 rows 1–61 section behaviour rows 1–61
:335 rows 62–65 section carry-onward rows 62–65
:423 row 30 UNHELD (see below)

And the why: strings, all 9 mentions across 8 ledger entries:

ledger entry mention key resolves to
security-plugin.ts (step 3.5) row 2 `owner_id` is not auto-stamped on INSERT row 2
the same entry row-1 The whole security middleware short-circuits row 1
engine.ts (early return) row 23 Tenant-audit warning silenced row 23
engine.ts (bypassTenantAudit) row 23 Tenant-audit warning silenced row 23
engine.ts (strict-drop) row 21 Strict-drop refusal never fires row 21
readonly-strict-errors.ts row 21 Strict-drop refusal never fires row 21
system-write-guard.ts row 24 append-only write guard bypassed row 24
sharing-service.ts row 34 `revoke()` deletes directly row 34
crud-nodes.ts row 60 Automation flow data nodes re-add the owner_id stamp row 60

⛔ The two references left UNHELD, listed rather than guessed

Both are declared in PAGE_ROW_REFERENCES with their reason, so they are still located (a reworded sentence reds [row-ref-context-stale]), but their numbers are not compared:

  • :90Lose: all of rows 2–6 at once. A range whose endpoints the sentence never names; it says only that row 1 costs you "all of" them. Nothing in it picks a first or a last row, and the two are not a section extent.
  • :423[Sharing Rules](/docs/permissions/sharing-rules) — what row 30 is skipping. The bullet's only subject is the linked doc title "Sharing Rules", and several rows are about sharing rules. No key in the sentence picks one of them.

Keying either one needs the page reworded to name its seam — and the page is out of scope for this PR (below). Hence Part of #15869, not a closing keyword: the mechanism half is done here, and the page half (reword those two sentences so they carry a key, then --fix renumbering of prose) is what remains.

⛔ What is deliberately out of scope

  • The page is not edited. content/docs/permissions/system-context.mdx is merge=os-regen routed and is touched by 8 open PRs (measured below), so the whole mechanism lives in the script.
  • --fix does not renumber prose. Doing so would write that page. Refuse-only here; the --fix leg for prose references is the follow-up once the page is free.
  • Nothing the gate already verified changed — anchors, the census, --fix for anchors, the counts, the ledger. The self-test's existing 43 cases all still run and still pass.

Measurement

All at the final head d56bf2d79. Exit codes captured before any pipe.

The gate, before and after

Before (origin/main + this worktree, no edits), through the shared verify lock:

check-system-context-census: OK — 106 elevation read sites in 19 packages across 44 files, all anchored; 140 anchors resolve, 27 declared non-read.
os-verify-lock: VERDICT command-exit 0 · held the lock 3s · waited 0s

After:

check-system-context-census: OK — 106 elevation read sites in 19 packages across 44 files, all anchored; 140 anchors resolve, 27 declared non-read; 19 row reference(s) resolve to their keyed row, 2 declared unheld.
os-verify-lock: VERDICT command-exit 0 · held the lock 3s · waited 0s

⇒ today's tree stays green, and the new figure says the check actually ran: 10 page references + 9 why: mentions = 19 held, 2 declared unheld.

The self-test, before and after

exit cases passed verdict line
before 0 43 check-system-context-census --self-test: all cases passed
after 0 66 check-system-context-census --self-test: all cases passed

No existing case dropped: 43 → 66, all 43 originals still registering under their own batteries (the roster floor would name any battery that stopped). One new battery, ⭐ ROW REFERENCES: held by seam, and the insertion that was silent (#15869), pinned at its real count of 23; SELF_TEST_BATTERY_FLOOR 17 → 18.

The 23 cases: a positive control (numbers that match pass), one per page spelling (Row N, row N's, row N, and both rows N–M extents) plus row-N inside a why:, a why: disagreement, a why: mention with no seam, a key that resolves to no row, a key that resolves to two rows, a stale context, the undeclared-reference population refusal, the unheld-is-still-located case, the zero-table-rows refusal, the real page today, and the ablation.

⭐ The ablation — one row inserted above row 34

Run twice: inside the self-test (temp dir via mkdtempSync, removed in a finally) and standalone (trap-guarded, trap '...' EXIT INT TERM). The real page is never written — the mutation is applied to a copy.

On-disk proof of the mutation, and that it landed on the copy only:

grep -c inserted row in fixture : 1
grep -c inserted row in REAL    : 0
numbered rows real / fixture    : 65 / 66
fixture byte delta              : 97 bytes

The check on the mutated copy — ABLATION_EXIT=1, 6 findings, including the exact three references PR #15687 falsified:

[row-ref-falsified] content/docs/permissions/system-context.mdx:252 says `Row 34`, but the seam that
sentence is about (``revoke()` deletes directly`) is row 35 (…:136). The table was renumbered under the
sentence, so row 34 is `**An inserted row (ablation)**` (…:135) -- a DIFFERENT seam. ⚠️ The reference
still resolves, which is why nothing noticed: a reader who follows it is told something false by a row
that exists.

[why-row-falsified] NON_READ_ANCHORS row for packages/plugins/plugin-sharing/src/sharing-service.ts says
`row 34` in its `why` (row 34 -- the CONFLICT guard `revoke()` deletes in front of), but the seam it keys
on (``revoke()` deletes directly`) is row 35 (…:136).

[why-row-falsified] NON_READ_ANCHORS row for packages/services/service-automation/src/builtin/crud-nodes.ts
says `row 60` in its `why` (row 60 -- the call site of the compensating owner stamp), but the seam it keys
on (`Automation flow data nodes re-add the `owner_id` stamp`) is row 61 (…:172).

plus row 50 at :163 (now row 51), and both stated extents (rows 1–61 → the behaviour section now runs 1–62; rows 62–65 → carry-onward now 63–66).

Restore leg, by bytes rather than by an exit code:

sha256 before : 21e6e7e455c80f3d5e2214070bc7ebe258d99e4bd0250df6239b41c4d2771955
sha256 after  : 21e6e7e455c80f3d5e2214070bc7ebe258d99e4bd0250df6239b41c4d2771955
REAL PAGE UNCHANGED: OK
git diff HEAD -- PAGE_PATH: EMPTY (exit 0)

⛔ No dist/ preflight applies: this gate is a plain .mjs run directly from source by node, and the ablation mutates a copy of a content page, not a built artifact.

Derived gate families — all 32 run, all exit 0

node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack → change set 1 path(s) vs merge base b398ad258, 32 commands. Every one run, every one exit 0, including node scripts/check-system-context-census.mjs, its --self-test, check-scripts-symbol-anchors, check-self-test-wired, check-self-test-workflow-commands, check-declaration-mirrors, pnpm check:pm-dispatch-gates, pnpm check:watch-hint-literal, pnpm check:nul-bytes, pnpm check:entry-guard, pnpm check:parse-guard.

The first derivation printed ⚠️ STALE TREE (4 family-defining files had moved on origin/main), so it was re-derived on a current tree: a detached worktree at origin/main 4f379125e carrying this same one-file diff. That run printed no stale warning and its command list is byte-identical to the 32 already run.

Also run, because the derivation marks them "roster under scripts, silence is not evidence in either direction": check-published-list-mirrors (+ --self-test), check:console-injection, check:engine-double-contract, check:i18n-stale-fill — all exit 0.

⊘ NOT MEASURED, by name:

  • pnpm check:published-readme-exportsexit 3, PREREQUISITE NOT MET (42 packages have no dist/*.d.ts; needs a full pnpm build). Not a finding, and this diff touches no README and no package export surface.
  • pnpm check:dts-closureexit 3, same prerequisite.
  • The three families whose argv takes a value from the workflow (check-cross-package-test-inputs --union-into "$RUNNER_TEMP/…", check-shard-attestation, check-test-completeness), which the derivation itself prints as NOT MEASURED.

Repo-wide lint — run in full, not narrowed

pnpm lint   (= node --stack-size=4000 eslint . --no-inline-config)
os-verify-lock: VERDICT command-exit 0 · held the lock 81s (1m21s) · waited 0s

Exit 0 over the whole repo, so no narrowing claim is needed. (The targeted run agrees: --format json on the one changed file reports files linted: 1 errors: 0 warnings: 0.)

Bytes, governance, mergeability

  • pnpm check:nul-bytesexit 0: check-nul-bytes: OK (scanned 7660 text file(s) … no raw ASCII control bytes). Independent sweep of the changed file with grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' → no hits.
  • node scripts/pm/check-governed-merges.mjs --test scripts/check-system-context-census.mjsexit 0: ✅ NOT governed — ordinary queue landing applies to a PR with exactly this file list.
  • Mergeability, sound probe only. A bare --shared clone of the worktree, in which the regen driver is provably absent — git --git-dir=probe.git config --get merge.os-regen.driverexit 1, empty (the source repo, for contrast, has it set). Then git --git-dir=probe.git merge-tree --write-tree 4f379125e d56bf2d79exit 0, tree 8b0a6e64df79b4a36688060f1dc827ee3f02e258. Clean merge. ⛔ No -c merge.os-regen.driver= anywhere.

Open PRs on the hot files — enumerated, 32 open PRs, every response HTTP 200

GET /repos/objectstack-ai/objectstack/pulls/N/files for all 32:

⭐ And a landing note, measured rather than assumed: none of the eight inserts or removes a numbered row — each patch adds and deletes an equal count of | N | lines, i.e. pure anchor rewrites. The lines carrying a row reference that #15878 and #15838 rewrite change only the anchor inside them; no seam text and no reference number moves. So this check stays green as those land, and the new [row-ref-…] refusals will not fire on them.

Scripts-only

No .changeset/*.md: this PR publishes nothing from any package. skip-changeset applied additively.

🤖 Generated with Claude Code

https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk


Generated by Claude Code

… SEAM, not by number

`check-system-context-census` held every `file:line` anchor on the page to the
tree, but nothing held the two other hand-maintained claims on the same page: the
page's own prose row references and the `why:` strings in `NON_READ_ANCHORS` that
name a row by number. A row inserted into the behaviour table renumbers every row
below it and silently falsifies every reference past the insertion point, with the
gate green — measured on #15687 (three references went stale) and #15395 (all
three became true again by coincidence, observed by nothing).

Each reference now declares the SEAM it is about — a literal occurring in exactly
one numbered row — and the gate resolves the seam, reads the number the reference
carries, and refuses when they disagree, naming the reference, its number and the
row the key resolves to. A ledger keyed by number would rot in the same edit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
…ertion as an ablation

23 cases: a positive control, one per spelling the page uses (`Row N`, `row N's`,
`row N`, `rows N–M`, and `row-N` inside a `why:`), a `why:` disagreement, keys that
resolve to no row and to two rows, the undeclared-reference population refusal, and
the real page today.

The ablation copies the REAL page into a temp dir, inserts one row above row 34 and
renumbers — the exact edit PR #15687 made under a green gate — and asserts the check
reddens naming `Row 34` (now row 35), the two `why:` strings for rows 34 and 60, and
both stated row extents. A byte assertion proves the real page was never written.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 5, 2026
@github-actions github-actions Bot added the size/l label Sep 5, 2026
@os-project-manager
os-project-manager marked this pull request as ready for review September 5, 2026 14:04
@os-project-manager
os-project-manager added this pull request to the merge queue Sep 5, 2026
Merged via the queue into main with commit 94d435d Sep 5, 2026
36 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-15869-census-row-references-held branch September 5, 2026 14:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants