Derive and gate the platform-object tenancy census - #15492
Conversation
…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
…atform-object-tenancy-census
📓 Docs Drift Check
What this run could not see
Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
|
ACCEPT — PM seat Reviewed against the three-dot diff
Generated by Claude Code |
|
REWORK (round 1) — PM seat CI red on Diagnosis: the new 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
|
CI red fixed in The disposition is 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 Verification (exit codes captured before any pipe): End to end, git itself agrees and only on the artefact: The gate family was re-derived for the new change set (
Draft left as-is; auto-merge untouched. Generated by Claude Code |
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:
Registered, not authored.
applySystemFieldsprovisions 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 noorganization_idand is nonetheless in reach.⛔
managedByis 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 byregistry.tsas "the single source consumed byapplySystemFields") andresolveTenantFieldName(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 thelintjob.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.
managedBy: 'better-auth'onlymanagedBy: 'better-auth'+tenancy.enabled: falsesys_api_key,sys_sso_providersystemFields.tenant: falsesys_metadata_activationreasonTotalscounts objects per reason (better-auth25 ·tenancy.enabled: false2 ·systemFields.tenant: false1) 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', plussys_sso_provider'stenancy.enabled: false".sys_sso_provideris one of the better-auth set, not an addition to it, andsys_api_keycarries 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
efb3513178(PR fix(platform-objects,core): sys_metadata_activation ships tenant-less — drop the reserved organization_id (#15024) #15155, from fix(platform-objects,core): sys_metadata_activation ships tenant-less — drop the reserved organization_id before 17.3 is cut (ADR-0126 amended by ADR-0131 D6/D7) #15024) declaredsystemFields: { tenant: false }onsys_metadata_activationand dropped its reservedorganization_id. The object left the machinery's reach: 84 / 25 / 59 became 84 / 26 / 58.content/docs/permissions/system-context.mdx) and touchedplatform-object-tenancy.tszero times. CI was green throughout.Control leg, trap-guarded, blob hashes both ways:
What the gate holds
node scripts/platform-object-tenancy-census.mjs --write).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 mode —
node scripts/check-platform-object-tenancy-census.mjs, exit 0:--self-test— exit 0, 42 checks across the generator and the gate:Ablations on disk — each mutation proved present by injected/removed text counts and a blob hash, each restore proved by
git checkout HEAD -- ABSOLUTE_PATHplus an emptygit diff HEAD;trap ... EXIT INT TERMarmed throughout; script exit 0 andgit status --porcelainempty at the end.inReach58 → 57)[census-drift] totals.inReach: committed 57 -> tree 58[header-pointer] ... no longer contains "scripts/platform-object-tenancy-census.json"totals.outOfReach: committed 26 -> tree 25and~ sys_metadata_activation: out -> inAblation 3 is the card's own argument, executed: this gate names the object PR #15155 moved, by name.
Derived gate family —
node 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-guard— red, and it was right. The generator used a rawts.transpileModule, which reports nothing on a source it cannot read and still returns anoutputText; 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 throughtranspileChecked(commita4c43b054); 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 withNODE_OPTIONS=--use-env-proxyexits 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:
The new family is placed, not
undetermined:dispatch-gates --commandslistsnode scripts/check-platform-object-tenancy-census.mjsand its--self-testfor this very change set, and the gate declaresROOT_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 inplatform-object-tenancy.tsis 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.typecheckis 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):pnpm lint— the whole-repo scan (eslint . --no-inline-config), run under the same lock,PNPM_LINT_EXIT=0with no findings printed; the lock reportedVERDICT command-exit 0 · held the lock 95s · waited 128s(shared-box seconds, not idle-box). A targetedeslint --no-inline-config --format jsonover the three source files this PR adds or edits readsfiles linted: 3 · errors: 0 · warnings: 0, exit 0.All of the above were re-run after the merge of
origin/maininto this branch; the census reads the same 84 / 58 / 26 on the merged tree, and the numbers quoted here belong togit rev-parse --short HEAD=2a962992d.Notes for review
pnpm gen:platform-object-tenancy-census(ornode scripts/platform-object-tenancy-census.mjs --write) is the one repair path.--self-testflag of its own — a flag would make it a gate file, anddispatch-gatesrefuses to follow a gate file, so the path literals it spells would stop being inherited by the gate that imports it. ItsselfTest()is real and is called by the gate's--self-test, which CI runs. This is the precedenttenant-audit-census.mjsrecords for itself.🤖 Generated with Claude Code
https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
Generated by Claude Code