Skip to content

fix(platform-objects,core): sys_metadata_activation ships tenant-less — drop the reserved organization_id (#15024) - #15155

Merged
hotlong merged 3 commits into
mainfrom
claude/issue-15024-activation-ledger-tenantless
Sep 4, 2026
Merged

fix(platform-objects,core): sys_metadata_activation ships tenant-less — drop the reserved organization_id (#15024)#15155
hotlong merged 3 commits into
mainfrom
claude/issue-15024-activation-ledger-tenantless

Conversation

@hotlong

@hotlong hotlong commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Fixes #15024

The ADR-0126 activation ledger records that this environment switched a packaged
artifact off. That is deployment-level state, owned by no organization — so
sys_metadata_activation ships with no tenant column at all.

It briefly declared one: an organization_id marked "RESERVED", nullable, and written by
nobody, held for a per-organization dimension ADR-0126 §5 pre-charted. A reserved nullable
tenant column is exactly the shape the total-organization-ownership record proposed in
PR #14976 rules out, and this one had no reader either. This is a plain removal, not a
migration
— measured below, not assumed.

⛔ Nothing in the ADR-0126 machinery is reverted or weakened. The packaged-flow /
packaged-action disable + clone family ships in 17.3 as it stands; the operator gate under
walled postures and both consult seams are untouched and stay pinned.

What changed

Declaration systemFields: { tenant: false } and the field deleted. Both halves are needed — the tenant anchor is INJECTED at registration (resolveInjectedSystemColumnsapplySystemFields), so deleting the field alone would have left the column exactly where it was.
Index unique: 'organization'unique: 'global' over (metadata_type, name).
Store list() drops the organization_id == null filter and the org-row skip; setActive takes the single row its keyed read returns. ObjectStoreFlowActivationStore / ObjectStoreActionActivationStore inherit it.

⚠️ The index change is a re-spelling, not a change of materialized shape.
normalizeDeclaredIndex prepends the NULL-safe tenant key part only
if (idx.unique === 'organization' && tenantField), and with no tenant column
computeTenantField resolves to null — so 'organization' already degraded to exactly
these two columns. The DDL is identical; what changes is that the declaration now states the
boundary it actually gets. Still explicit rather than bare unique: true, which lint
unique/unscoped-declared-index warns on and protocol 18 rejects.

⚠️ Why systemFields.tenant and not tenancy.enabled. Both suppress the column, and
both reach plugin-security's tenancyDisabled — which is required, not incidental: a
Layer 0 wall composing an equality on a column the table does not have denies every row.
They differ in what else they reach: only tenancy.enabled: false trips the spec's
isTenancyDisabled (driver native scoping, the sticky per-table opt-out record), which this
table does not need, because with no column computeTenantField already returns null on
its own. The sibling sys_sso_provider uses tenancy.enabled: false for the opposite
shape — a table that KEEPS its tenant column and needs the wall over it stood down.

Verification

git rev-parse --short HEAD = 35ed1e107 — every result below is from that tree, and
the working tree is clean at it.

Acceptance grep returns nothing, on both implementation files:

git grep -n organization_id -- packages/core/src/utils/metadata-activation-store.ts \
  packages/platform-objects/src/system/sys-metadata-activation.object.ts
=> EXIT=1 (no hits)

Positive control: the same pattern returns 3 hits on sys-sso-provider.object.ts, a table
that does carry the column. To reach zero while keeping the reasoning, both files name the
tenant column in words rather than as the literal token — the absence is the contract, so
it should be checkable by the one-line grep an auditor reaches for.

⚠️ Deviation, declared: the same grep still hits the test files, and must. Pinning a
column's absence requires naming what is absent, and the card's own Scope §4 asks for exactly
those pins. The zero holds where the column would live.

The column is gone from the emitted schema — read, not inferred. Two independent levels:

  • resolveInjectedSystemColumns(SysMetadataActivation) reports tenant: false and a names
    set without organization_id — the spec derivation that decides whether the column
    exists, and the one applySystemFields consumes. Anti-vacuity: the same set still contains id.
  • In a real booted stack (packaged-activation-ledger-reach.dogfood.test.ts, 11/11), the
    activation row returned by the driver's own SELECT has no organization_id key.
    Anti-vacuity: the same key set is asserted to contain id, metadata_type, name, active.

⚠️ That dogfood assertion previously read expect(row.organization_id ?? null).toBeNull().
It could not be carried forward: once the column is gone the property is undefined,
undefined ?? null is null, and it would have passed while measuring nothing — green for
precisely the reason it should have gone red. It is inverted into a key-set assertion.

Reverse verification (ablation). Removing the systemFields: { tenant: false } line turns
the new absence pin redAssertionError: expected true to be false, 1 failed / 14
passed. Mutation confirmed on disk before the run (anchor occurrences 1 → 0, hash-object
changed); restore proven after (git diff HEAD empty, and the working blob hash equals the
HEAD blob hash, 3050a5bc… on both sides), under a trap … EXIT INT TERM with absolute
paths. No rebuild leg was required and none is claimed: vitest resolves that file from
source via a same-package relative import, not through exportsdist.

Never shipped — measured. git merge-base --is-ancestor: this fix is not an ancestor
of @objectstack/account@17.2.0; positive control, @objectstack/account@17.2.0~1 is, so
the predicate can answer YES. And the object's own adding commit (428f9b24a, 2026-08-25) is
not an ancestor of the tag (2026-08-23) — the table postdates 17.2.0, so no released
version ever carried the column.

Suites (all green, at 35ed1e107):

package result
@objectstack/objectql 269 files / 4616 passed
@objectstack/runtime 220 files / 3160 passed
@objectstack/service-automation 103 files / 1225 passed
@objectstack/core 48 files / 1159 passed
@objectstack/platform-objects 33 files / 518 passed
@objectstack/dogfood (ledger reach) 1 file / 11 passed

typecheck green for core / platform-objects / objectql / runtime — and
check:test-typecheck compiled the test layer in each, so the rewritten pins were
genuinely type-checked rather than skipped by a *.test.ts exclusion.

The seams the card says must not move, verified unchanged (A5): no consult seam reads the
column in code — every hit outside the two implementation files was docblock prose. The
isolated-posture operator gate passes unchanged, including
REFUSES a tenant org admin, loudly, and never writes the row and
ALLOWS the platform operator; so does the execute() seam, including
refuses a disabled record-change flow with FLOW_DISABLED, and re-enabling restores firing
and refuses on the SUBFLOW entry path.

Gates. Families derived from the real diff with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (48 families).
47 measured green, each read from its own verdict line with the exit code captured before
any pipe. Two needed a full pnpm build first and were re-run after it rather than reported
on their prerequisite failure:

  • check:dual-build-cjs-loads✓ … 102 published require entry point(s) across 66 package(s) load; 610 emitted CommonJS file(s) parse
  • check:published-readme-exports✓ … 60 published document(s) across 79 workspace package(s)

⚠️ One is NOT MEASURED, not green and not red:
@objectstack/spec check:react-declaration-parityMANIFEST is not set — there is no registry side to compare against. This gate did NOT run. It needs an sdui.manifest.json
dumped from a sibling objectui checkout. Environmental, and unrelated to this diff (it
entered the family list only because the dogfood test path matches a broad
packages/qa/dogfood/** CI trigger). CI runs it with the manifest available.

⚠️ Declared: scripts/pm/os-verify-lock.sh ran in UNLOCKED (declared) mode on this
host — no usable flock on this host, so the shared verify lock was NEVER taken and NOTHING was serialized. Every heavy command above went through the entry point, but none of it was
serialized against other agents in this container.

⚠️ Declared: dispatch-gates.mjs reported a STALE TREE — this branch is 2 commits
behind origin/main and one file it derives from (scripts/check-type-check-coverage.mjs)
changed in that range. That gate is not in the derived family list, and I did not rebase.

Also in this diff, and why

  • Two pending changesets corrected. .changeset/sys-metadata-activation-ledger.md and
    .changeset/ledger-convergence-registration-and-one-store.md are unreleased and describe
    the reserved column and the org-row skip as shipping behaviour. Left alone they would
    announce a column 17.3 does not have, in the release notes for the very release this card
    exists to get right.
  • Row-shape docblocks in action-activation.ts, flow-activation-store.ts,
    service-automation/engine.ts, runtime/domains/actions.ts and
    runtime/domains/activation-gate.ts described the removed column; each is a one-passage
    mechanical correction in the same defect class.
  • Pins rewritten, not deleted — including the two consumer suites' SKIPS a row carrying an organization_id tests, which pinned the behaviour being deliberately removed and would
    otherwise have gone red for the right reason with no replacement.

Out of scope, filed

⚠️ Unrelated and pre-existing: pnpm check:platform-checklist reports 4 UNCLASSIFIED
coverage problems (batch_endpoints, crud_endpoints, metadata_endpoints,
route_generation). Verified byte-identical at origin/main (fcc42e6c1) in a throwaway
worktree, so it is not from this diff. That gate is deliberately not wired into CI.

🤖 Generated with Claude Code

hotlong and others added 2 commits September 4, 2026 11:26
Drop the reserved organization column from the activation ledger: the
object opts out of tenant-column injection, the declared unique index
states the 'global' scope it actually materializes, and the store's
NULL-filter and org-row skip go with the column they guarded.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ence

Rewrites the pins that asserted the reserved column and the org-row skip
so they pin the column's ABSENCE instead of being deleted, updates the
row-shape docblocks in both consumers and the runtime activation doors,
corrects the two pending changesets that would otherwise describe the
column in 17.3's release notes, and adds this change's changeset.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions github-actions Bot added the size/l label Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/core, @objectstack/objectql, @objectstack/platform-objects, @objectstack/runtime, @objectstack/service-automation, touching 10 documentable anchor(s). ⚠️ 4 changed file(s) yielded no anchor (packages/objectql/src/action-activation.ts, packages/runtime/src/domains/actions.ts, packages/runtime/src/domains/activation-gate.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

26 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 5c584231a1bcefd15b62f91992b3e2f4ca00bbba.

4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 4 changed file(s) yielded no anchor (packages/objectql/src/action-activation.ts, packages/runtime/src/domains/actions.ts, packages/runtime/src/domains/activation-gate.ts, …) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 47 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 5c584231a1bcefd15b62f91992b3e2f4ca00bbbapackageMentionDocs.

Which tree this was computed on

This run read content/docs from 2a4a244161196690cbcdee91c4e80788bc43ef6a — the merge of head 36695cd33ac9a012a3714956dbdb3e1907e1002b into base 5c584231a1bcefd15b62f91992b3e2f4ca00bbba, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 2a4a244161196690cbcdee91c4e80788bc43ef6a && git checkout 2a4a244161196690cbcdee91c4e80788bc43ef6a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5c584231a1bcefd15b62f91992b3e2f4ca00bbba 36695cd33ac9a012a3714956dbdb3e1907e1002b && git checkout -B drift-repro 5c584231a1bcefd15b62f91992b3e2f4ca00bbba && git merge --no-ff 36695cd33ac9a012a3714956dbdb3e1907e1002b

node scripts/docs-audit/affected-docs.mjs --json 5c584231a1bcefd15b62f91992b3e2f4ca00bbba

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 5c584231a1bcefd15b62f91992b3e2f4ca00bbba → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@hotlong

hotlong commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Patch round 1 — one red, pure line rot. Lint & Repo Gates fails only in check-system-context-census: the docblock edits in packages/runtime/src/domains/activation-gate.ts moved the two ec.isSystem reads from lines 139/190 to 138/189, so the anchors on content/docs/permissions/system-context.mdx now point one line off (4 problems: 2 site-without-a-row, 2 anchor-is-not-a-read-site, same two sites). The gate names its own repair: node scripts/check-system-context-census.mjs --fix (the #15030 dev hit the identical shape). Every other check on 35ed1e10 is green (30 success, 3 skipped). Dev is continued on the same branch; ready → queue after the fix commit is green.

🤖 Generated with Claude Code

…lock shift

The activation-gate docblock correction in this branch is one line shorter
than the text it replaced, so both `ec.isSystem` elevation reads moved up
by one line. Row 56's anchors on the system-context census page still
pointed at the old lines, which the census gate reports from both
directions at once: [site-without-a-row] for :138 and :189, and
[anchor-is-not-a-read-site] for the stale :139 and :190.

Pure line rot, repaired by the gate's own `--fix`. Only the two anchor
numbers move; no elevation behaviour text changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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 tests tooling

Projects

None yet

1 participant