Skip to content

Derive and gate the platform-object tenancy census - #15492

Merged
baozhoutao merged 4 commits into
mainfrom
claude/issue-14957-platform-object-tenancy-census
Sep 4, 2026
Merged

Derive and gate the platform-object tenancy census#15492
baozhoutao merged 4 commits into
mainfrom
claude/issue-14957-platform-object-tenancy-census

Conversation

@claude

@claude claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Fixes #14957

PLATFORM_OBJECT_TENANCY's header stated the census in prose — three hand-written digits and a parenthetical attributing them — with nothing re-deriving it. This deletes the digits and replaces them with a derived artefact, a generator and a gate.

The predicate, which is the deliverable

An object is inside the machinery's reach when:

resolveTenantFieldName(REGISTERED schema) !== null

Registered, not authored. applySystemFields provisions the tenant column before the engine ever sees the object, so the schema the resolver reads is not the one the author typed. An ordinary platform object declares no organization_id and is nonetheless in reach.

managedBy is not the predicate. It is one of several declarations that happen to make the predicate answer null, and counting it as if the resolver read it is the mistake that produced the wrong reason below.

The predicate is executed, never transcribed: the generator loads resolveInjectedSystemColumns (packages/spec/src/data/injected-system-columns.ts, documented by registry.ts as "the single source consumed by applySystemFields") and resolveTenantFieldName (packages/objectql/src/tenancy/system-write-organization.ts) from source and calls them. A transcription would be a second copy of a rule the engine owns, free to drift while reading as authoritative. It needs no build, so the gate runs in the lint job.

The derived numbers, and why they are not the card's

84 registered · 58 in reach · 26 outside — not the 84 / 25 / 59 the card and its triage state.

The card's numbers were correct when it was filed. They went stale 16 hours before this card was dispatched, and the disagreement is drift, not a derivation error. The control leg proves it: the same instrument, unchanged, reads the card's numbers on the pre-drift revision.

out of reach in reach
managedBy: 'better-auth' only 23
managedBy: 'better-auth' + tenancy.enabled: false 2 — sys_api_key, sys_sso_provider
systemFields.tenant: false 1 — sys_metadata_activation
resolves a tenant column 58
total 26 58

reasonTotals counts objects per reason (better-auth 25 · tenancy.enabled: false 2 · systemFields.tenant: false 1) and deliberately does not sum to 26: reasons are not mutually exclusive.

That table also settles grain 1 structurally rather than by rewording. The header said "24 managedBy: 'better-auth', plus sys_sso_provider's tenancy.enabled: false". sys_sso_provider is one of the better-auth set, not an addition to it, and sys_api_key carries the identical pair and went unnamed. Both now appear on a row that carries both declarations, so the mis-attribution cannot be restated.

The drift transcript

Control leg, trap-guarded, blob hashes both ways:

HEAD blob  sys-metadata-activation.object.ts = 3050a5bcb0e2efb47771d160218dce44f3cfbd1d
mutation   git checkout efb351317^ -- THE_OBJECT_FILE
on-disk    4a4760204ba61fee0185975d3c0e5e994aafbd11 = expected pre-#15155 blob (mutation PROVEN on disk)
           organization_id occurrences in planted file: 7
reading    at HEAD          : 84 registered, 58 in reach, 26 out
           at efb351317^    : 84 registered, 59 in reach, 25 out   == the card's numbers, reproduced
restore    git checkout HEAD -- ABSOLUTE_PATH  ->  3050a5bcb0e2efb47771d160218dce44f3cfbd1d = HEAD blob
           git diff HEAD -- THE_OBJECT_FILE: EMPTY (restore proven)

What the gate holds

  1. The artefact equals the tree. A drift check, deliberately not an anchor check — the artefact carries no line numbers, so displacement cannot move it, and there is one mechanical repair path (node scripts/platform-object-tenancy-census.mjs --write).
  2. The header still points here. Grain 1's fix was deletion, so what is left to protect is the pointer and the predicate sentence. Required to be present, not to be right: a count is exactly what must not come back.
  3. No unexplained exclusion. An object the predicate excludes with no declared mechanism to explain it is an error, never a default. The generator refuses to write the row and the gate reds, so a new exclusion mechanism is announced and adjudicated instead of absorbed into an existing bucket's total. This is tenant-audit-census.mjs's "an unplaceable receiver is an ERROR" shape.

Verification

Every exit code captured before any pipe; the printed verdict line is quoted.

The new gate, normal modenode scripts/check-platform-object-tenancy-census.mjs, exit 0:

✓ platform-object tenancy census matches the tree: 84 platform-namespace objects, 58 in the
  machinery's reach, 26 outside it, every exclusion explained by a declaration on its own schema.

--self-test — exit 0, 42 checks across the generator and the gate:

✓ platform-object-tenancy-census generator self-test: all checks pass (84 objects)
✓ check-platform-object-tenancy-census self-test: all checks pass (84 objects, 26 outside the machinery)

Ablations on disk — each mutation proved present by injected/removed text counts and a blob hash, each restore proved by git checkout HEAD -- ABSOLUTE_PATH plus an empty git diff HEAD; trap ... EXIT INT TERM armed throughout; script exit 0 and git status --porcelain empty at the end.

# mutation verdict
baseline none exit 0
1 one digit in the committed artefact (inReach 58 → 57) exit 1[census-drift] totals.inReach: committed 57 -> tree 58
2 the artefact pointer removed from the header exit 1[header-pointer] ... no longer contains "scripts/platform-object-tenancy-census.json"
3 the pre-#15155 object re-planted exit 1totals.outOfReach: committed 26 -> tree 25 and ~ sys_metadata_activation: out -> in

Ablation 3 is the card's own argument, executed: this gate names the object PR #15155 moved, by name.

Derived gate familynode scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (78 commands, derived from the change set, not hand-listed). All run; 74 exit 0. The four that did not:

  • pnpm check:parse-guardred, and it was right. The generator used a raw ts.transpileModule, which reports nothing on a source it cannot read and still returns an outputText; an unparseable module would have been evaluated as whatever survived and the census would have scored the tree against wreckage with a clean exit. Fixed by routing through transpileChecked (commit a4c43b054); re-run exit 0.
  • node scripts/check-required-contexts.mjs --verify-required-set — exit 2 (NOT VERIFIED, HTTP 401) under the default env; re-run with NODE_OPTIONS=--use-env-proxy exits 0: required-set sweep: 7 live required context(s) on main, 0 registered-but-not-required, 1 required-but-unpinned. The unpinned one (Governed Surface Queue Guard) is pre-existing and untouched by this PR, which adds a step to an existing job rather than a job.
  • pnpm check:dual-build-cjs-loads — exit 3, Run pnpm build first. ⛔ This is NOT a pass: nothing was measured. Recorded as NOT MEASURED.
  • pnpm check:type-check-debt — exit 3, the re-measure classification. Recorded as NOT MEASURED.

Named gates, all exit 0 with their own verdict lines:

✓ dispatch-gates self-test: 1402 cases pass.
✓ check:declared-population-live — 205 of 256 famil(ies) declare a path population, and every one
  of them reaches this tree's 7500 tracked file(s).
✓ check-self-test-wired: every one of the 170 script(s) CI runs that ship a `--self-test` has that
  self-test run by CI.
check-nul-bytes: OK (scanned 7493 text file(s) ... no raw ASCII control bytes).

The new family is placed, not undetermined: dispatch-gates --commands lists node scripts/check-platform-object-tenancy-census.mjs and its --self-test for this very change set, and the gate declares ROOT_DIR_WATCH_HINTS = ['packages/**'] with a self-test pinning that declaration against the roots it actually walks, in both directions.

Declared narrowing. @objectstack/objectql's change is comment-only — every added and removed line in platform-object-tenancy.ts is a docblock line (git diff | grep -vE '^[+-] \*' returns nothing), no code, no export moves — so its test run is narrowed to the three suites that cover the touched module rather than all 271 files in the package. typecheck is run in full. CI runs the whole farm regardless.

@objectstack/objectql, under the shared verify lock (OS_VERIFY_LOCK_SLOT=issue-14957-objectql, acquired after 362s in queue):

BUILD_DEPS_EXIT=0                  pnpm --workspace-concurrency=2 --filter '@objectstack/objectql^...' build
OBJECTQL_TYPECHECK_EXIT=0          check:test-typecheck: OK — @objectstack/objectql's test layer compiles
                                   under packages/objectql/tsconfig.test.json
OBJECTQL_TENANCY_TESTS_EXIT=0      Test Files  3 passed (3) · Tests  64 passed (64)
                                   (tenancy-by-object-classification, system-write-organization, engine-data-events)

pnpm lint — the whole-repo scan (eslint . --no-inline-config), run under the same lock, PNPM_LINT_EXIT=0 with no findings printed; the lock reported VERDICT command-exit 0 · held the lock 95s · waited 128s (shared-box seconds, not idle-box). A targeted eslint --no-inline-config --format json over the three source files this PR adds or edits reads files linted: 3 · errors: 0 · warnings: 0, exit 0.

All of the above were re-run after the merge of origin/main into this branch; the census reads the same 84 / 58 / 26 on the merged tree, and the numbers quoted here belong to git rev-parse --short HEAD = 2a962992d.

Notes for review

  • The artefact is derived; do not hand-edit a digit in it. pnpm gen:platform-object-tenancy-census (or node scripts/platform-object-tenancy-census.mjs --write) is the one repair path.
  • The generator deliberately ships no --self-test flag of its own — a flag would make it a gate file, and dispatch-gates refuses to follow a gate file, so the path literals it spells would stop being inherited by the gate that imports it. Its selfTest() is real and is called by the gate's --self-test, which CI runs. This is the precedent tenant-audit-census.mjs records for itself.
  • ⛔ No number in this PR was reconciled by hand in either direction. The header's digits were deleted; the artefact's digits are derived.

🤖 Generated with Claude Code

https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk


Generated by Claude Code

…4957)

`PLATFORM_OBJECT_TENANCY`'s header stated the census as prose — three
hand-written digits plus a parenthetical attributing them — with nothing
re-deriving it. It failed in both directions a prose count can.

A wrong REASON behind a right total: the parenthetical named
`sys_sso_provider`'s `tenancy.enabled: false` as an addition to the
`managedBy: 'better-auth'` set it was already in, and left `sys_api_key`'s
identical opt-out unnamed. `24 + 1 = 25` stayed right, so neither a reader
nor a gate caught it; that count reached a PR body and a filed card, and two
independent re-measurements were spent proving a correct file correct.

Then a stale TOTAL: PR #15155 declared `systemFields: { tenant: false }` on
`sys_metadata_activation`, the object left the machinery's reach, and the
ungated prose kept the old digits while the same commit updated the gated
page next door. CI was green throughout.

The digits are deleted, not corrected. The header points at the derived
artefact and states the predicate it was missing: `resolveTenantFieldName`
answering non-null on the REGISTERED schema, after `applySystemFields` has
injected the tenant column, because the injected column is what the engine
sees. `managedBy` is not the predicate.

The generator loads `resolveTenantFieldName` and
`resolveInjectedSystemColumns` from source and EXECUTES them rather than
re-spelling what they decide — a transcription would be a second copy of a
rule the engine owns, free to drift while reading as authoritative. It needs
no build, so the gate runs in the `lint` job.

Each excluded object records the declarations on its OWN schema, verbatim and
not mutually exclusive: an object carrying two keeps both, which is what makes
the mis-attribution unrepeatable. An excluded object with no declared
mechanism is an ERROR — the generator refuses to write the row and the gate
reds — so a new exclusion mechanism is adjudicated, never absorbed into an
existing bucket's total.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
…4957)

`check:parse-guard` caught a raw `ts.transpileModule` in the generator. The
raw call reports NOTHING on a source it could not read and still returns an
`outputText`, so an unparseable module would have been evaluated as whatever
survived and the census would have scored the tree against wreckage — with a
clean exit and a smaller, confident number.

That is this artefact's own failure class one layer down, so the fix is the
sanctioned wrapper rather than a diagnostics flag: `transpileChecked` forces
`reportDiagnostics: true` and refuses instead of emitting.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
@github-actions github-actions Bot added size/xl ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tooling labels Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/objectql/src/tenancy/platform-object-tenancy.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/objectql/src/tenancy/platform-object-tenancy.ts) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 615fac3a0c3184542f4d1e0573607d13192a2667packageMentionDocs.

@baozhoutao
baozhoutao marked this pull request as ready for review September 4, 2026 18:20
@claude

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT — PM seat domain:devx @ objectstack (#6023), session session_012zGPuVVX3deAx9LdjK8jCk.

Reviewed against the three-dot diff origin/main...claude/issue-14957-platform-object-tenancy-census (merge base a06faebbe, head 2a962992d): 7 files, +1706/−13, matching the PR file list; delivered under the ruling in comment 5544205979 (Q1 = A, Q2 = A artefact + B gate).

Fixes #14957 closes the card on merge. Flipping ready and arming auto-merge now.


Generated by Claude Code

@claude

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

REWORK (round 1) — PM seat domain:devx @ objectstack (#6023), session session_012zGPuVVX3deAx9LdjK8jCk.

CI red on 2a962992d: Lint & Repo Gates, step Merge-driver wiring gate (pnpm check:merge-drivergit-merge-regen --self-test, run 33904989630):

✗ generator(s) with NO recorded merge disposition:
  gen:platform-object-tenancy-census   [@objectstack/spec-monorepo]
  Every generator must be in ONE of the two ledgers in scripts/regen-artifacts.mjs.
✗ merge driver wiring is inconsistent — 1 failure(s) (cases and floor); see above.

Diagnosis: the new gen: script row needs a merge disposition. The artefact is fully derived, so the expected answer is a REGEN_ARTIFACTS row plus the matching .gitattributes line in one commit (a NOT_DRIVER_MANAGED entry only if a hand-written region turns out to exist). The dev is resumed on the same branch; auto-merge stays armed and re-runs on the push. Not caught locally because the derived family for the change set did not include check:merge-driver — noted for the dispatch-gates population of package.json gen: rows (a card follows if the derivation is the gap rather than the run).


Generated by Claude Code

…ver (#14957)

CI red on `pnpm check:merge-driver`: `gen:platform-object-tenancy-census` had
no recorded merge disposition, and every generator must be in one of the two
ledgers in `scripts/regen-artifacts.mjs`.

The honest answer is `REGEN_ARTIFACTS`, not `NOT_DRIVER_MANAGED`.
`renderArtefact` renders the whole file in memory, its `$comment` preamble
included, so nothing on disk survives into the output and there is no
hand-written region a deferral could launder — which is the question that
table exists to ask. No `mixed` for the same reason.

The routing earns its place: two PRs that each add an object land disjoint row
sets, and the merged tree's census equals neither side — its `totals` and
`reasonTotals` least of all, which a union of rows would leave arithmetically
wrong while looking merged.

⚠️ The driver is LOCAL and is never the protection. The load-bearing half is
`check:platform-object-tenancy-census` in `lint.yml`, on `pull_request` and
`merge_group` with no `paths:` filter, re-deriving from the tree rather than
reading the file back — so it also catches the silent case where two branches'
rows do not overlap and the text merge exits 0 over a file describing neither.

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

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

CI red fixed in 001c8205fpnpm check:merge-driver / git-merge-regen --self-test reported gen:platform-object-tenancy-census as a generator with no recorded merge disposition.

The disposition is REGEN_ARTIFACTS, not NOT_DRIVER_MANAGED, and the file's own question is what decides it: renderArtefact renders the whole artefact in memory — its $comment preamble included — so nothing on disk survives into the output and there is no hand-written region a deferral could launder. No mixed for the same reason. Added in one commit: the REGEN_ARTIFACTS row (scripts/regen-artifacts.mjs, owner: ROOT_OWNER since both script names live in the root manifest) and the matching .gitattributes line, aligned with its neighbours.

The routing earns its place rather than merely satisfying the ledger: two PRs that each add an object land disjoint row sets, and the merged tree's census equals neither side — its totals and reasonTotals least of all, which a union of rows would leave arithmetically wrong while looking merged. As with its neighbours, the driver is local and is never the protection: check:platform-object-tenancy-census runs in lint.yml on pull_request and merge_group with no paths: filter and re-derives from the tree.

Verification (exit codes captured before any pipe):

pnpm check:merge-driver                                  EXIT=0
  ✓ .gitattributes ↔ regen-artifacts.mjs agree on 18 path(s)      (was 17)
  ✓ entryForPath agrees with git check-attr on all 338 routed file(s)
  ✓ merge driver wiring is consistent (24 path(s) deliberately excluded).
  ✓ check-regen-pending self-test passed.
node scripts/check-regen-pending.mjs                     EXIT=0  (clean, no output)
node scripts/check-platform-object-tenancy-census.mjs     EXIT=0
  ✓ platform-object tenancy census matches the tree: 84 platform-namespace objects,
    58 in the machinery's reach, 26 outside it, every exclusion explained by a
    declaration on its own schema.
  ... --self-test                                        EXIT=0
pnpm -s check:pm-dispatch-gates                          EXIT=0  ✓ 1402 cases pass.
pnpm check:nul-bytes                                     EXIT=0  (7498 files, no control bytes)
pnpm lint (whole-repo eslint . --no-inline-config)         EXIT=0  (no findings printed)
  lock VERDICT: command-exit 0 · held the lock 86s · waited 0s (shared-box seconds)

End to end, git itself agrees and only on the artefact:

scripts/platform-object-tenancy-census.json:       merge: os-regen
scripts/platform-object-tenancy-census.mjs:        merge: unspecified
scripts/check-platform-object-tenancy-census.mjs:  merge: unspecified

The gate family was re-derived for the new change set (dispatch-gates --commands --repo objectstack-ai/objectstack, now 81 commands vs 78) and every command run: 78 exit 0, plus the two standing NOT MEASURED rows (check:dual-build-cjs-loads and check:type-check-debt, both exit 3 / prerequisite) and check-required-contexts --verify-required-set (exit 2 without the proxy, exit 0 with NODE_OPTIONS=--use-env-proxy).

⚠️ Worth a separate card, not fixed here: check:merge-driver was not derivable from the change that broke it. Measured on this branch — at 5629e6f98, package.json already carried the gen:platform-object-tenancy-census row (the thing that made the gate red) and dispatch-gates --commands did not list check:merge-driver; it appears only once .gitattributes / scripts/regen-artifacts.mjs are edited, i.e. once the problem is already fixed. A dev who runs exactly the derived family ships this red every time. Reported to the PM with the measurement; not filed from here because both dedup channels were down this session (REST /search/issues → HTTP 403, MCP search_issues → "API rate limit already exceeded"), and an unchecked card is worse than a reported finding.

Draft left as-is; auto-merge untouched.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/xl tooling

Projects

None yet

2 participants