Skip to content

Commit e3190b9

Browse files
fix(organizations)!: retire claimOrphanOrgRows — the first organization no longer adopts organization-less rows (#25246)
Fixes #25204 Clause-②: no (narrowing) Metadata-protocol: none The build of decision A (retire) on #25204, from the measurement os-dev-report 6342754850 and the seat's decision 6342853688. ## What changes - **`claimOrphanOrgRows` is retired.** `packages/plugins/organizations/src/claim-orphan-org-rows.ts` and its test (8 mock-engine cases) are deleted. In `organizations-plugin.ts`, Middleware B's "Fallback A" is removed together with its `claimed N orphan seed row(s) for first organization` log line, and header item 2b is replaced by a statement of why both retired paths (the donor clone and the orphan claim) must not come back. The false "Idempotent: a no-op once an organization-tagged row exists" sentence lived only in the deleted file's header. - **The organization count read is gone.** It existed only to pick Fallback A (`limit: 2` on `sys_organization`). A first organization with no seed replay now logs the same "starts empty" line every later organization already logs. - **The export** `claimOrphanOrgRows` leaves `@objectstack/organizations`'s `index.ts`, and its README row goes with it. - **Regenerated, never by hand:** `scripts/engine-double-contract.pinned.json` (`node scripts/check-engine-double-contract.mjs --write`: the deleted test's row lost, the new pin's row added) and the tenant-audit census (`node scripts/tenant-audit-census.mjs --write`: the `claim-orphan-org-rows.ts` row leaves `counts.md`, 237 sites become 236). The census page's hand-written prose counts were restated to match, as `check-tenant-audit-census` requires (the historical carried figures in the first column are left as quoted). - **Prose, five sites:** `claim-org-seed-ownership.ts` header, `org-scoping-engine.ts` header, plugin-security `claim-seed-ownership.ts` header, `security-plugin.ts` (which also named the retired `cloneOrgSeedData`), and the runtime `app-plugin.ts` passage that quoted the plugin-security header (reworded rather than left quoting text that no longer exists). - `claim-org-seed-ownership.ts` changes in its header prose only. No ceremony change, no `ORGANIZATION_OWNERSHIP_SHAPE_GENERATION` change. ## Pins `packages/plugins/organizations/src/first-organization-orphan-rows.pin.test.ts`, against the real plugin's Middleware B over an in-memory engine whose `update()` opens with `assertEngineUpdateDispatch`: - CONTROL: the store holds no organization and three organization-less application rows the probe sees. - With no seed replay, creating the first organization leaves all three rows NULL and calls no `update`. - With a seed replay, the first organization gets the app's seed row (the replay ran once, for the new organization) and the organization-less rows stay NULL. - `claimOrgSeedOwnership` still re-owns exactly the rows the replay reported, and touches no organization-less row. **Ablation** (Fallback A put back through `scripts/ablation-replace.mjs`, with `claim-orphan-org-rows.ts` restored from `origin/main` under a trap; marker count 1 on disk before the run; restore proven by blob == HEAD and an empty `git diff HEAD`): `Tests 1 failed | 3 passed (4)`, the failure being the seedless case, `expected [] to deeply equal [ 'crm_account:acc_orphan', …(2) ]`: the claim wrote all three rows. The replay case stays green under the ablation by construction (the claim ran only when the replay landed nothing). ## Changeset `@objectstack/organizations` `patch`, with a BREAKING banner, the FROM/TO line for the retired export, and the ADR-0087 marker `not-required (runtime-export-retired @objectstack/organizations#claimOrphanOrgRows)`. ## Acceptance notes - Local proof (package tests, typecheck, derived gates, R1/R4 boots and the upgrade-harness legs) is recorded with commands and exit codes in the os-dev-report on #25204. - `check-governed-merges --branch`: 0 of 15 paths hit the register, not governed; 691 changed lines. --- _Generated by [Claude Code](https://claude.ai/code/session_0146UznqonWCqNdiHePZ9Xy3)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e58bd0e commit e3190b9

15 files changed

Lines changed: 316 additions & 387 deletions
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/organizations': patch
3+
---
4+
5+
fix(organizations)!: the first organization no longer adopts organization-less rows — `claimOrphanOrgRows` is retired
6+
7+
Clause-②: no (narrowing)
8+
Metadata-protocol: none
9+
10+
<!-- adr-0087: not-required (runtime-export-retired @objectstack/organizations#claimOrphanOrgRows) The one author-reachable removal is a runtime export: the function `claimOrphanOrgRows` leaves `@objectstack/organizations`'s published entry. No key of any metadata schema is removed, renamed or re-shaped, so there is nothing for `objectstack migrate meta` to rewrite and no tombstone. -->
11+
12+
**BREAKING** for a walled store (`group` or `isolated`) that holds application rows with no `organization_id` when its first organization is created, and for code that imports `claimOrphanOrgRows`. It ships as `patch`: no metadata an author writes changes.
13+
14+
- **The first organization adopts nothing.** When the per-org seed replay landed no row and the new organization was the store's first, `OrganizationsPlugin` used to write that organization's id onto every organization-less row of every application object (`claimed N orphan seed row(s) for first organization …`). Being created first names no owner, so this was an owner by guess, which ADR-0131 D9 and D10 forbid. Under a wall, the only posture that mounts this plugin, the organization-ownership ceremony reports such a row, leaves it NULL and holds it against certification; the claim then gave the same row to whichever organization existed. Those rows now stay NULL.
15+
- **A first organization with no seed replay logs that it starts empty**, the line every later organization already logs. The organization count the claim needed is no longer read.
16+
- **Unchanged:** the per-org seed replay, the organization stamp on insert, the Default Organization bootstrap, and `claimOrgSeedOwnership`, which hands an organization's seeded rows to its owner.
17+
18+
**What an operator does.** On a store this release creates or certifies there is no such row, so nothing. On a walled store an earlier release created that holds organization-less application rows, run `os migrate organization-ownership`: it attributes every row an anchor names, and lists each row it cannot attribute with its remedy instead of guessing an owner.
19+
20+
The retired export:
21+
22+
- FROM `claimOrphanOrgRows` TO no replacement; run `os migrate organization-ownership` for organization-less rows.

‎content/docs/permissions/tenant-audit-census.mdx‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ are reported as `undecidable` rather than assumed either way.
122122

123123
The same holds twice over for the context. An options argument spelled as a
124124
literal can be read; one spelled `options`, `{ ...opts }`, or handed through a
125-
forwarding shim cannot, and **51 of the 237 sites are spelled that way**. A
125+
forwarding shim cannot, and **51 of the 236 sites are spelled that way**. A
126126
context resolved from an inline literal or a local `const` can be tested for
127127
`isSystem`; one arriving from a helper call cannot.
128128

@@ -187,10 +187,10 @@ reproduce them. Where it disagrees, it disagrees on the page:
187187

188188
| carried figure | where it survives | this census |
189189
| :--- | :--- | ---: |
190-
| 175 write call sites | quoted in the merged changeset | **237** |
190+
| 175 write call sites | quoted in the merged changeset | **236** |
191191
| 24 carrying no tenant context | quoted in the merged changeset | **2** provable and tenancy-enabled; **29** more whose options argument is unreadable |
192-
| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card | **160 of 237** decidable, **77** undecidable |
193-
| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration** | **not reproduced**: 135 decidably elevated, 0 decidably not, 94 undecidable |
192+
| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card | **160 of 236** decidable, **76** undecidable |
193+
| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration** | **not reproduced**: 134 decidably elevated, 0 decidably not, 94 undecidable |
194194
| 141 and 132, two independent re-derivations | the card that filed this work | — |
195195

196196
**The differences are not reconciled, and deliberately so.** The old census's
@@ -207,11 +207,11 @@ would report a smaller number and would not say so.
207207

208208
The fourth row is the one worth flagging to anyone citing it. **The 135 / 77%
209209
figure has no surviving corroboration anywhere in the tree.** This census reads
210-
135 of 237 (57%) as decidably elevated, with 94 more whose elevation is a
210+
134 of 236 (57%) as decidably elevated, with 94 more whose elevation is a
211211
run-time fact — so the claim is neither confirmed nor refuted, and the honest
212212
answer is that a static reading cannot settle it.
213213

214-
⇒ **Cite `2 / 237`, and say what it is**: the sites whose options argument was
214+
⇒ **Cite `2 / 236`, and say what it is**: the sites whose options argument was
215215
READ and holds no tenant context, against a decidably tenancy-enabled object.
216216
That is the control's provable yield surface. ⛔ Do not cite it as "the sites
217217
without tenant context" — **29 further sites** have an options argument this
@@ -223,31 +223,31 @@ cannot read, and they are neither in nor out.
223223

224224
| what | count |
225225
| :--- | ---: |
226-
| write call sites on the application surface | **237** |
226+
| write call sites on the application surface | **236** |
227227
| …whose object name is statically decidable | 160 |
228-
| …whose object name is chosen at run time | 77 |
228+
| …whose object name is chosen at run time | 76 |
229229
| …against an object with tenancy ENABLED | 159 |
230230
| …against an object that declares tenancy off | 1 |
231-
| threading a tenant context | 178 |
231+
| threading a tenant context | 177 |
232232
| PROVABLY carrying none (options read, no context key) | **8** |
233233
| …of those, against a decidably tenancy-enabled object | **2** |
234234
| options argument UNREADABLE — may or may not carry one | 51 |
235235
| …of those, against a decidably tenancy-enabled object | 29 |
236-
| threading a decidably ELEVATED (`isSystem`) context | 135 |
236+
| threading a decidably ELEVATED (`isSystem`) context | 134 |
237237
| threading a context that is decidably NOT elevated | 0 |
238238
| threading a context whose elevation is a run-time fact | 94 |
239239

240240
| how the instrument reached the site | count |
241241
| :--- | ---: |
242-
| receiver carried a readable engine type | 196 |
242+
| receiver carried a readable engine type | 195 |
243243
| receiver erased, placed by the object NAME | 31 |
244244
| receiver erased, placed by an `object: string` PARAMETER | 5 |
245245
| receiver erased, placed by an `UNTYPED_RECEIVERS` row | 5 |
246246

247247
| object name spelled inline | 109 |
248248
| object name spelled through a `const` | 51 |
249249
| object name is an `object: string` parameter | 7 |
250-
| object name is some other run-time expression | 70 |
250+
| object name is some other run-time expression | 69 |
251251

252252
### Subtractions the census could NOT defend — enforced
253253

@@ -297,7 +297,7 @@ holds still. They are required to be HERE and to say WHEN they were true;
297297
their values are not compared. The reasoning, and the measurement behind it,
298298
are in `scripts/check-tenant-audit-census.mjs`.
299299

300-
Measured on 2026-10-22 at `452e9963c`.
300+
Measured on 2026-10-22 at `78b1ed16a`.
301301

302302
| corpus scale (not enforced) | count |
303303
| :--- | ---: |

‎docs/audits/2026-08-tenant-audit-write-call-sites.counts.md‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,17 @@ silent, and `node scripts/tenant-audit-census.mjs --write` is the resolution.
3333

3434
| Measure | Value |
3535
|---|---:|
36-
| Write call sites | 237 |
36+
| Write call sites | 236 |
3737
| Object name statically decidable | 160 |
38-
| Object name chosen at run time | 77 |
38+
| Object name chosen at run time | 76 |
3939
| Against a tenancy-enabled object | 159 |
4040
| Against an object declaring tenancy off | 1 |
41-
| Threading a tenant context | 178 |
41+
| Threading a tenant context | 177 |
4242
| Provably carrying none | 8 |
4343
| …and decidably tenancy-enabled | 2 |
4444
| Options argument unreadable | 51 |
4545
| …and decidably tenancy-enabled | 29 |
46-
| Threading a decidably elevated context | 135 |
46+
| Threading a decidably elevated context | 134 |
4747
| Threading a decidably non-elevated context | 0 |
4848
| Threading a context of undecidable elevation | 94 |
4949

@@ -90,7 +90,7 @@ holds still. They are required to be HERE and to say WHEN they were true;
9090
their values are not compared. The reasoning, and the measurement behind it,
9191
are in `scripts/check-tenant-audit-census.mjs`.
9292

93-
Measured on 2026-10-22 at `452e9963c`.
93+
Measured on 2026-10-22 at `78b1ed16a`.
9494

9595
| corpus scale (not enforced) | count |
9696
| :--- | ---: |
@@ -104,7 +104,6 @@ Measured on 2026-10-22 at `452e9963c`.
104104
| file | verb | object | tenancy | tenant context | n |
105105
|---|---|---|---|---|---:|
106106
| `packages/plugins/organizations/src/claim-org-seed-ownership.ts` | `update` | `schema.name` | undecidable | elevated | 1 |
107-
| `packages/plugins/organizations/src/claim-orphan-org-rows.ts` | `update` | `schema.name` | undecidable | elevated | 1 |
108107
| `packages/plugins/plugin-approvals/src/action-slot-backfill.ts` | `update` | `sys_approval_action` | enabled | elevated | 2 |
109108
| `packages/plugins/plugin-approvals/src/approval-service.ts` | `update` | `object` | undecidable | context, elevation undecidable | 1 |
110109
| `packages/plugins/plugin-approvals/src/approval-service.ts` | `insert` | `sys_approval_action` | enabled | elevated | 15 |

‎packages/plugins/organizations/README.md‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ Register it **before** `SecurityPlugin`, so the posture probe finds the service.
5252
| `OrganizationsPluginOptions` | type | Constructor options — currently `ensureDefaultOrganization`. |
5353
| `OrgScopingPlugin` | alias | Alias of `OrganizationsPlugin`, matching the package name. |
5454
| `OrgScopingPluginOptions` | alias | Alias of `OrganizationsPluginOptions`. |
55-
| `claimOrphanOrgRows` | function | One-time back-fill of `organization_id` on orphaned seed rows, for the first organization. |
5655
| `claimOrgSeedOwnership` | function | Hands an organization's seeded rows to its owner (`owner_id` back-fill, scoped to one org). |
5756
| `ensureDefaultOrganization` | function | Multi-org flavour of the default-org bootstrap; wraps the open `plugin-auth` helper and adds the per-org seed-ownership handoff. |
5857
| `assertWalledMembershipPolicyDeclared` | function | The boot gate: throws unless a walled deployment declared its membership policy. |

‎packages/plugins/organizations/src/claim-org-seed-ownership.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@
77
* first-admin handoff). Seeded rows land `owner_id = NULL` (the author leaves it
88
* unset and `cel`os.user.id`` resolves to NULL at seed time, since the owning
99
* admin does not exist yet). In multi-tenant mode those rows are scoped to an
10-
* org by `claimOrphanOrgRows` / per-org replay, but their `owner_id` stays NULL
10+
* org by the per-org replay, but their `owner_id` stays NULL
1111
* — so "My" views, owner reports and owner notifications are empty for the org's
1212
* members until ownership is assigned.
1313
*
1414
* This runs when the org's owner is established (e.g. `ensureDefaultOrganization`
1515
* binds the platform admin as the default org's `owner`) and assigns
16-
* `owner_id = ownerUserId` to that org's NULL-owned rows — the ownership
17-
* companion to `claimOrphanOrgRows`'s `organization_id` back-fill.
16+
* `owner_id = ownerUserId` to that org's NULL-owned rows. It never writes
17+
* `organization_id`: an organization-less row is the organization-ownership
18+
* ceremony's to attribute, not this plugin's (ADR-0131 D10).
1819
*
1920
* Scoped to a single org (`organization_id = organizationId`) so it never
2021
* touches another tenant's rows. Idempotent: only NULL-owned rows are updated.

‎packages/plugins/organizations/src/claim-orphan-org-rows.test.ts‎

Lines changed: 0 additions & 134 deletions
This file was deleted.

0 commit comments

Comments
 (0)