Skip to content

docs(audits): #14096 census — tenancy ledger disagrees with namespace regexp on 8 objects - #15122

Merged
os-musk merged 4 commits into
mainfrom
claude/issue-14096-tenancy-ledger-vs-namespace-census
Sep 4, 2026
Merged

docs(audits): #14096 census — tenancy ledger disagrees with namespace regexp on 8 objects#15122
os-musk merged 4 commits into
mainfrom
claude/issue-14096-tenancy-ledger-vs-namespace-census

Conversation

@os-musk

@os-musk os-musk commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Part of #14096 — the census, per the director-seat ruling's step 1. Non-zero result: this STOPS here, nothing is implemented. (Not Fixes — a closing keyword on a card the ruling itself sends back to the decision box would delete the decision's home.)

The ruling, verbatim (总监批 #25, 2026-09-01, maintainer verbatim 「同意」, comment 5494594783)

  1. 普查先行(派发的第一交付物,纯测量零裁决):读 PLATFORM_OBJECT_TENANCY 台账,对每个键跑 /^(sys_|cloud_|ai_)/,量出台账与正则判得不同的对象数;
  2. A:两条 seed 路径维持正则,把「维持现状」记录为被选择的决定 —— 两个正则站点各加一行注记引用本裁决,卡关 completed,归档级降 p3;
  3. 非零 ⇒ ⛔ 停手,带对象清单回决策箱改判 p1 —— 那时这不是形式分歧,是两个写者对活数据不同意;B/C 届时上桌,且与 design: a THIRD tenancy state the 批 #9 re-ruling cannot express — objects that are CONDITIONALLY tenant-scoped, where org-less is a property of the ROW, not the object #13636(分类体系第三态)联裁:⛔ 不在分类体系可能要改的当口扩散它;
  4. C 的反向判例入册:Seed loader writes untenanted rows while the REST path stamps an organization — one single-tenant install runs two autonumber scopes and mints duplicate business identifiers, silently (17.0.0 GA) #8686 自己的姿态「已铸出的只报告、从不重写」—— 同车道既有裁定不支持对存量行的重写,C 即便届时也需独立强理由;
  5. Clause-②:普查与注记均 no;若走到 B/C 再按 diff 复判。

Predicate (stated, not left implicit)

The regexp is applied at both seed sites as a uniform rule: every namespace-matching name stays global/cross-tenant — that's the whole population's regexp verdict, always "out of scope". The ledger's verdict, classifyPlatformObjectTenancy(name), is one of tenant-scoped / global / unclassified. global and unclassified both map to "out of scope" (the ledger's own file documents unclassified as preserving today's — i.e. the same — behaviour). So a disagreement is exactly a ledger entry classified tenant-scoped.

Result: 8, not 0

Object Ledger Regexp Diverges from
sys_file tenant-scoped global both seed paths
sys_upload_session tenant-scoped global both
sys_approval_request tenant-scoped global both
sys_approval_action tenant-scoped global both
sys_approval_approver tenant-scoped global both
sys_automation_run tenant-scoped global both
sys_notification_delivery tenant-scoped global both
sys_record_share tenant-scoped global both

Cross-checked against the ledger's own pinned test (tenancy-by-object-classification.test.ts's tenantScopedPlatformObjects()), which lists the identical 8 names, and computed live from the real, built @objectstack/objectql module (not re-transcribed by hand).

Positive control (dispatch Zone 2.3) — what it covers, and what it doesn't

The ledger's header (platform-object-tenancy.ts:32-37) claims 84 platform-namespace objects registered, 25 resolving no tenant field, 59 carrying one. This census's script reproduces those exact numbers — after catching its own precedence bug (a first draft mishandled tenancy.enabled: false as overridable by a self-declared field, the way the managedBy skip is; resolveTenantFieldName actually checks tenancy.enabled === false first and unconditionally, before ever reading fields). The corrected script matches 84/25/59 exactly — left as a comment in the script since it's exactly the discipline the dispatch asks for.

Scope of this control, stated precisely (review round 2, see below): it is an AST census of the object files — it never reads PLATFORM_OBJECT_TENANCY at all. It validates the population and the tenant-field precedence logic (and is what caught the precedence bug above); it validates nothing about the ledger import that the disagreement count in the table above is computed from. That gap is now covered by a hard floor in the script itself (see "Review round 2" below), not by this control passing.

Also avoided: the managedBy: 'better-auth' trap that caught a prior census this week (sys_member/sys_team/sys_invitation resolve a real tenant field despite that label — confirmed, and confirmed they don't change the disagreement count since all three are unclassified in the ledger, which already agrees with the regexp).

Three sites re-derived (dispatch Zone 2.5)

Card measured these at dda969cd71; this branch is off 35e94c96b (confirmed an ancestor). All three unchanged in shape:

  • Runtime write path: engine.ts:3744 / :3963 (called from :9934) — isPlatformObjectOutOfTenantAuditScope, the per-object ledger read.
  • seed-loader.ts:926fallbackOrgId, still gated by the namespace regexp.
  • seed-tenancy-backfill.ts:587,1251 — its own PLATFORM_NAMESPACE, still filtering by it.

Branch verdict and context that raises the stakes

Non-zero ⇒ STOP per ruling item 3. No option implemented; no seed path touched; no row on disk touched (ruling item 4's counter-precedent: #8686's own posture is "already-minted rows are reported, never rewritten"). The card returns to the decision box, re-graded p1 — and confirmed not alone: #13636 (the third tenancy state) is still open, pm:awaiting-maintainer + priority:p1, its implementation PR #14923 closed unmerged on the maintainer's own instruction this week. So this lands beside a card already in the maintainer's hands.

What this census does not claim (added, review round 2)

The disagreement is real and the count is 8 — three independent routes agree on it (a hand count, the ledger's own pinned test, and the script). This is not softened by scoping it: this census measured a static ledger-vs-regexp classification disagreement, and it did not measure (a) how many rows the two seed paths have actually written for these 8 objects on any deployment, or (b) when each ledger verdict was decided relative to the seed paths' code — i.e. whether the ledger later tightened and the seed paths didn't follow, or the seed paths were already out of step when written. Those have different remedies and neither was measured. Full paragraph in the audit doc's new ## 7. What this census does not claim.

Review round 2 — two findings fixed

Independent review reproduced a silent-failure mode in the census script: emptying PLATFORM_OBJECT_TENANCY to {}, rebuilding, and re-running still printed reproduced_matches_header: true and exited 0, with disagreement_count silently reading 0 — indistinguishable from a genuine zero, and the number this ruling branches on (a false zero would close a p1 tenancy question as "decided, nothing to do"). Root cause: the header control never reads the ledger at all (see the corrected Positive-control section above), so it validates nothing about the ledger import.

Fix: the script now asserts a hard floor (LEDGER_ENTRY_COUNT_FLOOR = 5; the real ledger holds 9 entries) on the imported PLATFORM_OBJECT_TENANCY and throws a loud, explicit error below it, instead of reporting a clean zero. Reproduced the reviewer's exact break on this branch (emptied ledger → rebuilt → before the fix: exit 0, disagreement 0; after the fix: exit 1, loud error naming the cause and the remedy), then restored the ledger file and proved the restore by git hash-object equality against git rev-parse HEAD:<path>, an empty git diff HEAD, and an empty git status --porcelain — never by exit code. Re-ran on the real, rebuilt ledger: still reports 8.

Also added the doc's missing "what this census does not claim" scope paragraph (above), and corrected the doc's "its other outputs can be trusted" sentence about the positive control to say precisely what it does and does not cover.

(Self-inflicted and self-caught in the same round: the first draft of the new error message spelled the rebuild hint as pnpm --filter '@objectstack/objectql^...' build, whose escaped quote made check:pnpm-filter-targets read --filter \ as a dead filter target. Reworded to reference the already-documented commands instead of respelling the invocation inline; gate re-run green.)

Clause-② and changeset

Clause-②: no (mechanical floor re-checked against the final diff: no exported symbol, no published payload key — two files under docs/audits/ and scripts/audits/ only, unchanged in this round). skip-changeset: a stop-at-non-zero outcome with no code change to a shipped package is exactly the case the dispatch names for this label.

Gates — node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths, derived off merge-base f594e70d7, same 2-file set at final HEAD)

All 25 derived commands, plus the 6 flagged "roster lives under scripts/, which this diff touches" (check-published-list-mirrors.mjs ×2, check:console-injection, check:engine-double-contract, check:i18n-stale-fill, check:published-readme-exports), plus check-governed-merges.mjs --test on the final file list:

  • 31/31 green (check:console-injection exits 0 and informationally skips its injection check — no built packages/console/dist, which needs the separate objectui:build, not part of pnpm build; unrelated to this diff either way).
  • check:published-readme-exports initially failed on an unbuilt tree (86 packages' dist/*.d.ts missing) — a full pnpm build (72/72 tasks) resolved it: 198/198 @objectstack/ specifier(s) resolved, 0 baseline instances.
  • check:pnpm-filter-targets initially failed against this round's own new error-message string (see "Review round 2" above) — fixed and re-run green.
  • check-governed-merges.mjs --test: NOT governed — 0/2 paths hit the register (5 surfaces).

No governed surface touched. No packages/objectql/src/engine.ts or packages/metadata-protocol/src/protocol.ts edit (both serial-constrained, confirmed untouched this round too).


🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68

…8 objects

Ruling item 1 (director seat, 总监批 #25, 2026-09-01, maintainer verbatim
「同意」): count how many platform-namespace objects the hand-adjudicated
tenancy ledger (`PLATFORM_OBJECT_TENANCY`) judges differently than the
namespace regexp `/^(sys_|cloud_|ai_)/` that both seed paths still use.

Result: 8, not 0 — sys_file, sys_upload_session, sys_approval_request,
sys_approval_action, sys_approval_approver, sys_automation_run,
sys_notification_delivery, sys_record_share. Per ruling item 3 this STOPS
here: no option (A/B/C) is implemented, no seed path is touched, no row on
disk is touched. The full predicate, the header control (84/25/59
reproduced), the three-site re-derivation and the disagreement table are in
the audit doc; the census script is self-contained and re-runnable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation labels Sep 4, 2026
@os-musk os-musk added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 4, 2026 — with Claude
…read

Reviewer on PR #15122 reproduced a silent-failure mode: emptying
PLATFORM_OBJECT_TENANCY to {} and rebuilding still prints
reproduced_matches_header: true and exits 0, with disagreement_count
silently reading 0 -- indistinguishable from a genuine zero. The header
control never reads the ledger (it's an AST census of the object files),
so it validates the population and tenant-field logic and proves nothing
about the ledger import.

Add a hard floor (LEDGER_ENTRY_COUNT_FLOOR = 5, current ledger has 9
entries) on the imported PLATFORM_OBJECT_TENANCY: below it, the script
throws a loud, explicit error instead of reporting a clean zero. Also
correct the audit doc's "its other outputs can be trusted" sentence to
say precisely what the control does and does not cover, and add a "What
this census does not claim" section scoping the result (does not measure
actual row counts on any deployment, or when each ledger verdict was
decided relative to the seed paths' code) without softening it -- the
count is still 8, confirmed three ways.

Reproduced the reviewer's break on this branch (empty ledger -> rebuild
-> exit 0, disagreement 0), applied the fix, reproduced again (same
mutation -> rebuild -> now exit 1, loud error), then restored the ledger
file and proved the restore by git hash-object equality against
git rev-parse HEAD, plus empty git diff HEAD and git status --porcelain.
Re-ran on the real, rebuilt ledger: still reports 8.

Part of #14096

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@github-actions github-actions Bot added size/l and removed size/m labels Sep 4, 2026
My own error-message string in the last commit wrote the rebuild hint as
`pnpm --filter '@objectstack/objectql^...' build`, using an escaped
single quote (\') around the filter target. check:pnpm-filter-targets
scans raw JS source text (masking comments, not string-literal escapes),
so it read the escape sequence as `--filter \` and correctly flagged
that as a filter naming no real package -- a real, self-inflicted
finding, not a bug in the gate.

Reworded to reference the existing documented commands (the module doc
comment above, and the audit doc's "Commands to re-run this census"
section) instead of respelling the pnpm invocation inline. Re-ran
check:pnpm-filter-targets: green. Re-ran the script against the real
ledger: still reports 8.

Part of #14096

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68

os-musk commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator Author

Review FAIL cleared — all three findings closed, two of them verified again by this seat

Independent review returned FAIL on three findings. All three are closed. The review's substantive verdict was that the census itself is sound, and that stands: the count, the predicate, all 13 file:line citations, the three-site re-derivation, the header control and its precedence-bug story, the stop (2 files, 0 deletions, no seed path / engine.ts / migration touched) and the Part of keyword were each re-derived by the reviewer and each held.

F1 — skip-changeset was missing and Check Changeset was red. PM seat's own miss, fixed by this seat.

The label was genuinely absent while the PR body described it as a completed step, and the real CI job was failing on exactly that. Applied and confirmed by comparison read-back: documentation / size/m / skip-changeset. Check Changeset now reports skipped, which is the correct state for a PR that publishes nothing. ⚠️ Worth naming plainly: the PR body asserted a step that had not been taken, and only the reviewer pulling the live label list and the live check-run conclusion caught it. A described step is not a done step.

F2 — the census script could report a false zero. Fixed, and proved against the original break.

The reviewer did not theorise this; they emptied PLATFORM_OBJECT_TENANCY to {}, rebuilt, and got exit 0 with disagreement_count: 0 and the positive control still printing "control passes". The cause was structural: the 84/25/59 header control is an independent AST census that never reads the ledger at all, so it validated the population logic and nothing about the import.

The fix is a floor guard at scripts/audits/14096-tenancy-ledger-namespace-regexp-census.mjs:234, and its error message names the stake rather than just the symptom:

PLATFORM_OBJECT_TENANCY imported with only 0 entries (floor: 5). This looks like an empty, stale, or mis-imported ledger … refusing to compute a disagreement count against it, because a false zero here would silently close a p1 tenancy question as "decided, nothing to do"

That is the ruling's item 2 branch understood correctly — zero ⇒ option A ⇒ close completed at p3 — which is precisely why a false zero could not be allowed to look like a measurement.

Proved by running the reviewer's own experiment on both sides: before the fix, emptied ledger → exit 0, silent zero (reproducing the reviewer exactly); after, emptied ledger → exit 1, no JSON, loud error. Both restores proved by stategit hash-object == git rev-parse HEAD:<path> (c439d95cc… both times), empty git diff HEAD, empty git status --porcelain — never by an exit code. The real ledger still reports 8.

Independently re-verified by this seat: the guard is at :234, and the real ledger holds 9 entries (8 tenant-scoped + 1 global), clearing the floor of 5 with margin — a count that matches this seat's own hand-derivation of the ledger.

The fix round also caught a regression it had just introduced, before pushing. The new error message spelled the rebuild hint as pnpm --filter '@objectstack/objectql^...' build with an escaped JS quote, and check:pnpm-filter-targets read that as a --filter naming no package. Reworded to point at the already-documented commands instead of re-spelling the invocation inline, and re-run green. That is the "re-read your own diff adversarially before you push" discipline doing its job on the very same round.

F3 — the document now states its own boundary

New ## 7. What this census does not claim: it measured a ledger-vs-regexp classification disagreement, and did not measure how many rows the seed paths actually wrote for those 8 objects, nor when each ledger verdict was decided relative to the seed-path code — two questions with different remedies, neither answered here. The overstated sentence about the positive control was corrected to say what it actually covers (population and tenant-field logic) and what it never touches (the ledger import).

⛔ The finding was not softened by adding the boundary. The disagreement is real and the count is 8, agreed by three independent routes: a hand count, the repo's own pinned test tenancy-by-object-classification.test.ts:162-171, and the script.

Gates and landing

All 31 gate invocations green at 0cfc7c61a — 25 dispatch-derived plus the 6 flagged because the roster lives under scripts/, which this diff touches. check:published-readme-exports needed a real full build to measure anything (a known false red on an unbuilt tree) and returned 198/198 after one, matching the reviewer's own number. check-governed-merges --test: NOT governed.

31 of 33 check runs are green or skipped; Lint & Repo Gates and Type Check · workspace are still running, so this is not yet at the enqueue bar — an in-progress check is not a green one. Auto-merge is armed once they report.

Part of #14096 unchanged and load-bearing: that card is back in the decision box at priority:p1, to be ruled jointly with #13636.


Generated by Claude Code

@os-musk
os-musk marked this pull request as ready for review September 4, 2026 02:32
@os-musk
os-musk enabled auto-merge September 4, 2026 02:33
@os-musk
os-musk added this pull request to the merge queue Sep 4, 2026
Merged via the queue into main with commit fcc42e6 Sep 4, 2026
36 checks passed
@os-musk
os-musk deleted the claude/issue-14096-tenancy-ledger-vs-namespace-census branch September 4, 2026 02:59
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/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