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 intoSep 5, 2026
Conversation
… 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
os-project-manager
marked this pull request as ready for review
September 5, 2026 14:04
os-project-manager
enabled auto-merge
September 5, 2026 14:04
os-project-manager
disabled auto-merge
September 5, 2026 14:04
os-project-manager
enabled auto-merge
September 5, 2026 14:04
os-project-manager
deleted the
claude/issue-15869-census-row-references-held
branch
September 5, 2026 14:28
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #15869
The census page's
row Nreferences 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-censusheld everyfile:lineanchor on the page to the tree — that is what--fixrewrites and what the gate verified. Two other hand-maintained claims on the same page were held by nothing:Row 34 is correct for the rule,row 50's doors,what row 30 is skipping,rows 1–61 above);why:strings in the script's ownNON_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 perrow Non the page;context(a number-free literal that must match exactly one page line) plus exactly one ofseam,section(for a range: the extent of a section, which is mechanical rather than editorial) orunheld(with the reason no key is derivable).NON_READ_ANCHORS[].rowSeams— the seams for therow Nmentions in that entry'swhy:, in the order they are written. Awhy:that names a row and declares no seam is refused.Population, not just resolution: every
row Nspelling 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
12occurrences on12distinct 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 awhy:string. Each, and how it resolves on this tree::90rows 2–6:91row 1The whole security middleware short-circuits:90):162row 50`manage_metadata` bypassed on metadata writes:161):171row 2`owner_id` is not auto-stamped on INSERT:91):212Row 2`owner_id` is not auto-stamped on INSERT:91):238row 30on the record-`afterDelete` hook:131):247Row 21Strict-drop refusal never fires:115):251Row 34`revoke()` deletes directly:135):259row 2`owner_id` is not auto-stamped on INSERT:91):334rows 1–61behaviour:335rows 62–65carry-onward:423row 30And the
why:strings, all 9 mentions across 8 ledger entries:security-plugin.ts(step 3.5)row 2`owner_id` is not auto-stamped on INSERTrow-1The whole security middleware short-circuitsengine.ts(early return)row 23Tenant-audit warning silencedengine.ts(bypassTenantAudit)row 23Tenant-audit warning silencedengine.ts(strict-drop)row 21Strict-drop refusal never firesreadonly-strict-errors.tsrow 21Strict-drop refusal never firessystem-write-guard.tsrow 24append-only write guard bypassedsharing-service.tsrow 34`revoke()` deletes directlycrud-nodes.tsrow 60Automation flow data nodes re-add theowner_idstamp⛔ The two references left UNHELD, listed rather than guessed
Both are declared in
PAGE_ROW_REFERENCESwith their reason, so they are still located (a reworded sentence reds[row-ref-context-stale]), but their numbers are not compared::90—Lose: 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--fixrenumbering of prose) is what remains.⛔ What is deliberately out of scope
content/docs/permissions/system-context.mdxismerge=os-regenrouted and is touched by 8 open PRs (measured below), so the whole mechanism lives in the script.--fixdoes not renumber prose. Doing so would write that page. Refuse-only here; the--fixleg for prose references is the follow-up once the page is free.--fixfor 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:After:
⇒ 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
check-system-context-census --self-test: all cases passedcheck-system-context-census --self-test: all cases passedNo 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_FLOOR17 → 18.The 23 cases: a positive control (numbers that match pass), one per page spelling (
Row N,row N's,row N, and bothrows N–Mextents) plusrow-Ninside awhy:, awhy:disagreement, awhy: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, theunheld-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 afinally) 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:
The check on the mutated copy —
ABLATION_EXIT=1, 6 findings, including the exact three references PR #15687 falsified:plus
row 50at: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:
⛔ No
dist/preflight applies: this gate is a plain.mjsrun directly from source bynode, 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 set1 path(s) vs merge base b398ad258, 32 commands. Every one run, every one exit 0, includingnode 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 onorigin/main), so it was re-derived on a current tree: a detached worktree atorigin/main4f379125ecarrying 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-exports— exit 3, PREREQUISITE NOT MET (42 packages have nodist/*.d.ts; needs a fullpnpm build). Not a finding, and this diff touches no README and no package export surface.pnpm check:dts-closure— exit 3, same prerequisite.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
Exit 0 over the whole repo, so no narrowing claim is needed. (The targeted run agrees:
--format jsonon the one changed file reportsfiles linted: 1 errors: 0 warnings: 0.)Bytes, governance, mergeability
pnpm check:nul-bytes→ exit 0:check-nul-bytes: OK (scanned 7660 text file(s) … no raw ASCII control bytes). Independent sweep of the changed file withgrep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'→ no hits.node scripts/pm/check-governed-merges.mjs --test scripts/check-system-context-census.mjs→ exit 0:✅ NOT governed — ordinary queue landing applies to a PR with exactly this file list.--sharedclone of the worktree, in which the regen driver is provably absent —git --git-dir=probe.git config --get merge.os-regen.driver→ exit 1, empty (the source repo, for contrast, has it set). Thengit --git-dir=probe.git merge-tree --write-tree 4f379125e d56bf2d79→ exit 0, tree8b0a6e64df79b4a36688060f1dc827ee3f02e258. 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/filesfor all 32:scripts/check-system-context-census.mjs— touched by NONE.content/docs/permissions/system-context.mdx— touched by EIGHT: feat(spec): the flowendnode declares its outcome —refusedwith an interpolatedmessage, and the run vocabulary gainsrefused#15889, feat(spec,rest): the authored label is the default locale's text — ResolveOptions.defaultLocale skips the fallback chain, and a chain-less caller no longer falls to a literal en (#15711) #15888, feat(plugin-auth): a membership that ends takes the session's claim on that organization with it — re-pointed if the user belongs elsewhere, revoked if not #15879, feat(security,objectql): the Layer 0 wall records its verdict on the operation, and the bulk data-event producer reads it instead of re-deriving the wall (#15813) #15878, docs(rest): drop the hand-written translatable-type list fromtranslateMetaItem's comment #15863, fix(plugin-security): retire seven dead{ records }find-result limbs, and stop the permission-set loader inventing an empty page #15838, fix(rest): consume the parsedapisub-config soRestApiConfigSchemaowns its defaults #15673, feat(plugin-sharing): expand thefieldsharing recipient per matched record — the services half of the #14103 ruling #15235. (The PM's 12:58Z amendment measured six; two more have arrived since. The amendment holds, harder.)⭐ 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-changesetapplied additively.🤖 Generated with Claude Code
https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
Generated by Claude Code