Repository navigation
Commit 502f179
Fixes #19054
Clause-②: no
Executes the maintainer ruling recorded verbatim on the card:
「organizationField 撤出可授权面 同意你的建议」. `object.tenancy.organizationField`
leaves the authorable surface at protocol 18 (ADR-0049
enforce-or-remove). The divergence the key existed for is **not**
retired — only its authorability.
## What the key was, and why it could never be more than one table's
fact
It answered "which column says who this platform row is ABOUT", where
`tenancy.tenantField` answers "what is this object WALLED by". The
spec's own docblock stated the consequence: *"For ordinary objects the
two coincide and `organizationField` is never needed."* Re-measured at
head before this branch: the entire repository declared it **once**, on
`packages/platform-objects/src/identity/sys-api-key.object.ts` — the
better-auth credential table — and zero business objects declared it.
Its readers were three platform-row writers, scope-pinned by name, so an
application declaration was inert by construction while still being
authorable on every object.
## The shape of the change
`TenancyConfigSchema` is a `strictObject`, so this is the
strict-deletion route:
- the key is deleted from the shape, and `TENANCY_RETIRED_KEY_GUIDANCE`
gains its prescription beside the two v15.0 precedents
(`tenancy.strategy`, `tenancy.crossTenantAccess`). Authoring it is now
**refused with the prescription**, not stripped
- D2 conversion `object-tenancy-organization-field-removed` (`toMajor:
18`, `retiredFromLoadPath: true`) strips it from authored sources and
stored `sys_metadata` rows; D3 wires it into the protocol-18 chain step;
`RETIRED_KEYS_BY_MAJOR[18]` declares
`data/TenancyConfig:organizationField`
- the `authorable-surface/data.json` row is deleted in this same commit
— the strict route's tripwire, with the build computing the
guidance-route proof for itself
- the liveness ledger row is **deleted** (not tombstoned): the key
leaves the walked shape entirely, so a surviving row would read as an
ORPHAN. `liveness/README.md`'s `object` row records why, and
`state-counts.md` moves `object` 51 → 50 live
Limb 0 of the shared resolver now reads a platform-internal table
instead of a declaration:
```
PLATFORM_STAMP_ORGANIZATION_COLUMNS = { sys_api_key: 'active_organization_id' }
```
keyed by the object's registered NAME, read by the STAMP face alone.
`resolveRecordOrganizationField` and `createRecordOrganizationResolver`
keep their signatures — `check:api-surface` is byte-identical — and the
engine-bound face passes the name it was asked about rather than reading
`objectDef.name`, because several engine doubles in this monorepo return
a bare `{ tenancy, fields }` map with no `name`.
## The two facts the card said must survive
1. `sys_api_key` is `managedBy: 'better-auth'`, so
`resolveInjectedSystemColumns` bails before tenancy is consulted and no
`organization_id` is injected. Pinned, and the pin is now stated as the
better-auth bail rather than as key-blindness
(`packages/spec/src/data/injected-system-columns.test.ts`).
2. ⛔ The column is **not** renamed to `organization_id`. In this
platform "has an `organization_id` column" IS the wall, so the rename
would wall the credential table on an equality that excludes NULL.
`plugin-security`'s Layer-0 suite pins both halves against the real
shipped object.
The stamp/wall divergence pin is green:
`resolveRecordWallOrganizationField` never read the key and is
untouched.
## Base merge after #19600 landed, and the tombstone version it exposed
(2026-09-23)
The collision partner this section used to name, #19610, has landed, and
so has #19600 (card #15178, merged at 03:04:25Z as `d0f1845657`). #19600
is one of the three PRs in the serial on
`packages/spec/src/migrations/registry.ts` described in notice
`5780847968`. After it landed, this PR read `dirty`.
⚠️ **The sentence that stood here before was wrong in part.**
`registry.ts` is only partly generated. Its `<os-generated …>` regions
are regenerated. But `registry.ts:18-38` says outright that each step's
`rationale` and `conversionIds` are **hand-written and merge as text**.
No gate turns red when a paragraph is dropped from them.
The merge was done on the branch with no rebase and no force-push. It is
three commits:
1. **`bde765bf05` merges `origin/main` at `67add1301a`.** It is a merge
commit with parents `7cc0ca1b3d` and `67add1301a`, and it resolved two
textual conflicts by hand.
- `step18.rationale` keeps #19600's paragraph verbatim. Its last line is
re-terminated with a trailing space, and this PR's paragraph is appended
after it.
- `step18.conversionIds` keeps both
`'translation-per-app-settings-removed'` and
`'object-tenancy-organization-field-removed'`. That gives 33 ids, 33 of
them distinct.
- `packages/spec/src/conversions/registry.ts` keeps both D2 conversions
in `CONVERSIONS_BY_MAJOR[18]`, in landing order. The file's own rule is
「ordering within a major is application order」.
- The generated regions were regenerated and never hand-merged.
2. **`9d5fb0ba5f` is regeneration only.** It regenerates the two
reference pages that `os-regen-merge.sh` had deferred.
3. **`3fb1a4994c` is a CONTENT change, not merge resolution.** The merge
brought in `check:future-spec-major` (#19655), which landed after this
PR's old base, and CI went red on two sites. Under ADR-0087 (amended
2026-09-13), a tombstone names the npm release it ships in, never the
protocol major. This retirement ships `minor`, so it lands in 17.x. The
commit therefore changes the prescription at
`packages/spec/src/data/object.zod.ts:540` and its refusal pin at
`packages/spec/src/data/object.test.ts:1947` from `@objectstack/spec 18`
to `@objectstack/spec 17`. The protocol-major references (`toMajor: 18`,
`RETIRED_KEYS_BY_MAJOR[18]`, `os migrate meta --from 17`) are unchanged,
because the gate permits them.
**Measured by the dispatching seat against the committed trees, not
taken from the dev's narration:**
- The merge commit against the main parent `67add1301a`: 2 files,
+128/−1. Every hunk is this PR's.
- The merge commit against the branch parent `7cc0ca1b3d`: 2 files,
+656/−28. Every hunk is main's.
- On the merged `registry.ts`, each of the following appears exactly
once:
- the shared closing line `dataset. '`
- #19600's paragraph
- #19600's last line, continuing with a trailing space
- each of the two `conversionIds`
- the retired-key row
- A dark control phrase appears 0 times.
- This PR's paragraph follows #19600's.
⚠️ **The earlier contract review (`5765681233`) names head
`7cc0ca1b3d`.** Commit `3fb1a4994c` changes a string that review's AC2
pinned, so this head move is **not** regeneration-only, and the earlier
record does not govern the new head. `check-clause2-carriers.mjs --pair
19618` confirms it: exit 4, C6. A fresh contract review of the current
head is owed before landing.
## Verification
**Re-measured at the current head `3fb1a4994c`, after the base merge.**
- **CI**, measured by the seat from the head's check-runs: 35 checks,
latest run per name. 33 success, 2 skipped (`Console Pin Gate`,
`Packed-tarball smoke (opt-in)`), 0 failed. All five type-check lanes
pass. The legacy commit status is `success`.
- **Suites and gates**, from the os-dev report `5788876254`, which the
seat did not re-run:
| package or gate | result |
| --- | --- |
| `@objectstack/spec` | 516 files, 15065 passed, 1 todo |
| `@objectstack/metadata-core` | 16 files, 285 passed (unchanged) |
| `@objectstack/plugin-audit` | 25 files, 363 passed (unchanged) |
| typecheck, spec and metadata-core | exit 0 |
| `check:generated` | 15 of 15 current |
| `check:future-spec-major` | exit 0 |
| `dispatch-gates --ran` | 115 accounted: 112 run with exit 0, 3 NOT
MEASURED |
- The spec suite grew from the pre-merge 509 files / 14898 tests. The +7
files are exactly the seven spec test files `main` added in the merged
range.
- `check:future-spec-major` was checked against a lit control:
re-planting `18` makes it exit 1 with exactly one problem.
- The 3 NOT MEASURED gates were refused for build prerequisites (exit
3). They run in CI jobs that build first.
The tables below are the **pre-merge** readings, kept as history:
Every number in the tables below was taken at `7cc0ca1b3d`, the
pre-merge head.
**Reverse verification (both legs committed first, both restored
byte-identically, both via `scripts/ablation-replace.mjs`):**
| ablation | anchor → replacement landed | result |
| --- | --- | --- |
| the platform stamp row renamed (`sys_api_key` → `sys_api_key_ABLATED`)
| anchor 1 → 0, blob `0be02fdcc6b7` → `21856a4a20d0` | **4 of 19**
metadata-core tests RED; restore `blob == HEAD`, `git diff HEAD` empty |
| the prescription's first clause replaced with placeholder text |
anchor 1 → 0, blob `2e9e19825ef6` → `eea8b4c7f061` | refusal pin RED
with `expected 'Unrecognized key(s) on 'tenancy': 'or…' to contain
''tenancy.organizationField' was remov…'` — the pin measures the
PRESCRIPTION, not merely that parse throws; restore verified the same
way |
**Suites** (`pnpm test` per package, through the shared verify lock):
| package | result |
| --- | --- |
| `@objectstack/spec` | 509 files, 14898 passed, 1 todo |
| `@objectstack/metadata-core` | 16 files, 285 passed |
| `@objectstack/platform-objects` | 53 files, 848 passed |
| `@objectstack/plugin-security` | 117 files, 2249 passed |
| `@objectstack/plugin-audit` | 25 files, 363 passed |
**Typecheck:** `@objectstack/spec`, `@objectstack/metadata-core`,
`@objectstack/platform-objects`, `@objectstack/plugin-audit`,
`@objectstack/plugin-security` — all green, test layers included.
**Gates:** `node scripts/pm/dispatch-gates.mjs --ran` reconciles **114
derived / 114 run / 0 NOT-MEASURED / 0 UNRUN** against this diff. `pnpm
--filter @objectstack/spec check:generated` reports 15 of 15 artifacts
current. `pnpm lint` (`eslint . --no-inline-config`, the whole repo, no
narrowing) exits 0.
**The three sanctioned platform-row writers' pins stayed green
UNTOUCHED**, as the card required — `plugin-approvals` (`approval-node`,
`backfill-platform-row-organizations`), `service-automation`
(`suspended-run-store`), `service-storage`
(`backfill-sys-file-organizations`): 33 + 52 + 15 tests, zero edits. The
`driver-sql` and `trigger-schedule` read-neutrality suites are green
untouched too (36 + 61).
## Acceptance notes
**Declared widening of the dispatched file surface — three files, each
because this diff makes a statement in it FALSE.** None was edited for
tidiness; each is named with the measurement that forced it.
1. `packages/spec/src/shared/alias-integrity.test.ts` — RED. It pins the
exact key set of the folded `tenancy` guidance table: `expected [
'crossTenantAccess', …(2) ] to deeply equal [ 'crossTenantAccess',
'strategy' ]`. The retirement adds the third row, which is the only
channel the refusal travels on.
2. `packages/plugins/plugin-security/src/tenant-layer.test.ts` — RED. It
asserted the declaration off the shipped object: `expected undefined to
be 'active_organization_id'`. Rewritten to pin what this suite actually
owns: the stamp column exists as a field, `organization_id` does not,
and `tenancy` is exactly `{ enabled: false }`.
3. `packages/plugins/plugin-audit/src/audit-writers.test.ts` — RED, two
cases, and one of them is a **finding the card asked for**. See the next
section.
A fourth file,
`packages/spec/src/automation/schedule-organization.zod.ts`, carried a
docblock asserting "`tenancy.organizationField` wins there" — a
statement this diff falsifies, and one that **publishes**, into
`content/docs/references/automation/schedule-organization.mdx`.
Corrected in prose; the generated page follows.
**⭐ Finding — one sanctioned writer's pin DID have to be edited, and the
reason is not cosmetic.** Two `plugin-audit` cases went red:
- *"`organizationField` outranks `tenantField`"* pinned the precedence
on `crm_lead`, an object declaring BOTH keys, with the comment *"No
shipped object declares both; this pins the precedence so the day one
does is not a coin flip."* After the retirement no application can
declare a stamp column at all, so the question is **closed rather than
answered**. The case is rewritten to pin the closed set — an application
object carrying a lookalike column stamps from its own wall.
- *"control: without the declaration the credential table still stamps
the actor's org"* fed a `sys_api_key` schema with **no** `tenancy` block
and pinned the actor's org, proving the stamp came from the declaration
rather than from a column-name heuristic. Keying limb 0 by object name
makes that shape stamp `active_organization_id` instead. This is a
**real, deliberate behaviour change on a shape that is not reachable for
the shipped table** — `sys_api_key` is `managedBy: 'better-auth'` and
`protection: { lock: 'full' }`, so its block cannot be dropped. Recorded
in the rewritten case rather than smoothed over, and the `#5315` guard
that did not move (column absent ⇒ fall through to the actor's org) is
pinned beside it.
**⭐ Finding — two issue citations this repo carries in these files do
not resolve.** `check-issue-citations --base origin/main` judged 12
citations this change adds and refused all 12: `#8778` and `#8707` are
`allocated-but-absent` (minted, ≤ frontier 19616, not on the board;
deleted vs transferred NOT MEASURED). Both are pre-existing text — the
diff only re-adds them by rewriting the docblocks around them. Following
the gate's own prescription, the added lines now name the rulings in
prose and cite the cloud record that does resolve. ⛔ No number was
guessed. The standing occurrences on unchanged lines elsewhere in the
tree are untouched and are not this PR's to repair.
**Stale-but-green fixture residue, deliberately NOT touched** (green
today, outside the dispatched surface, and not a defect — the fixtures
feed drivers and engine doubles, never `TenancyConfigSchema`):
`packages/drivers/driver-sql/src/sql-driver-tenant-scope.test.ts`,
`packages/triggers/trigger-schedule/src/time-relative-trigger.test.ts`,
`packages/plugins/plugin-approvals/src/{approval-node,backfill-platform-row-organizations}.test.ts`,
`packages/services/service-automation/src/suspended-run-store.test.ts`,
`packages/services/service-storage/src/backfill-sys-file-organizations.test.ts`
still author `tenancy: { …, organizationField: … }` in raw
object-definition fixtures. Their assertions remain true; what has gone
vacuous is the *claim* that the driver / wall face is neutral about a
key nobody can write. `packages/lint/src/validate-object-field-refs.ts`
carries the key in a list of scalars it deliberately does not judge.
**No tree-scoped absence pin is added**, and that is a decision rather
than an omission: the playbook's tree-scoped form would have to declare
its radius in `scripts/cross-package-test-inputs.mjs` and `turbo.json`,
both far outside this card's surface, and it would go red against
exactly the six inert fixtures above. The absence is instead enforced
where it is cheap and exact — `authorable-surface/data.json` has no row,
and `check:authorable-surface` is the gate over that baseline.
## Clause-② re-judged from the diff
`no`, and the diff agrees. No hunk puts a new key on a published
payload: the guidance row is a prescription string, the
`RETIRED_KEYS_BY_MAJOR` / `CONVERSIONS_BY_MAJOR` entries are registry
rows, `json-schema/**` **loses** a key, and `api-surface/` is
byte-identical — `resolveRecordOrganizationField`'s signature is
unchanged. This is a pure retirement, which narrows.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2
---
_Generated by [Claude
Code](https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent e7a8bd4 commit 502f179
22 files changed
Lines changed: 688 additions & 287 deletions
File tree
- .changeset
- content/docs/references
- api
- automation
- data
- system
- packages
- metadata-core/src
- platform-objects/src/identity
- plugins
- plugin-audit/src
- plugin-security/src
- spec
- authorable-surface
- liveness
- src
- automation
- conversions
- data
- migrations
- entries/retired-keys
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
948 | 948 | | |
949 | 949 | | |
950 | 950 | | |
951 | | - | |
| 951 | + | |
952 | 952 | | |
953 | 953 | | |
954 | 954 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
86 | 86 | | |
87 | 87 | | |
88 | 88 | | |
89 | | - | |
90 | | - | |
91 | | - | |
92 | | - | |
93 | | - | |
94 | | - | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
95 | 98 | | |
96 | 99 | | |
97 | 100 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
153 | 153 | | |
154 | 154 | | |
155 | 155 | | |
156 | | - | |
| 156 | + | |
157 | 157 | | |
158 | 158 | | |
159 | 159 | | |
| |||
319 | 319 | | |
320 | 320 | | |
321 | 321 | | |
322 | | - | |
323 | 322 | | |
324 | 323 | | |
325 | 324 | | |
| |||
748 | 747 | | |
749 | 748 | | |
750 | 749 | | |
751 | | - | |
752 | 750 | | |
753 | 751 | | |
754 | 752 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
329 | 329 | | |
330 | 330 | | |
331 | 331 | | |
332 | | - | |
| 332 | + | |
333 | 333 | | |
334 | 334 | | |
335 | 335 | | |
| |||
614 | 614 | | |
615 | 615 | | |
616 | 616 | | |
617 | | - | |
| 617 | + | |
618 | 618 | | |
619 | 619 | | |
620 | 620 | | |
| |||
0 commit comments