From 601aba53ffeb9f81fdb6fb8f758dde6d5a879916 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Fri, 4 Sep 2026 00:11:11 +0800 Subject: [PATCH 01/24] =?UTF-8?q?docs(adr):=20ADR-0131=20=E2=80=94=20organ?= =?UTF-8?q?ization=20ownership=20is=20total:=20no=20NULL=20organization=5F?= =?UTF-8?q?id,=20a=20platform=20organization=20owns=20deployment-level=20r?= =?UTF-8?q?ows,=20sharing=20is=20declared?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Proposed record cut from the maintainer's 2026-09-03/04 ruling on #13564 (verbatim 「或者说我们数据库中,不应该有允许 org_id 为空的状况」/「认可这个方向,起草 ADR 和分阶段的卡」). Docs-only; empty changeset. Governed surface — hand-merge only (Prime Directive #14). Refs #13564 Co-Authored-By: Claude Fable 5.1 --- .../adr-0131-total-organization-ownership.md | 7 + ...ation-ownership-no-null-organization-id.md | 544 ++++++++++++++++++ 2 files changed, 551 insertions(+) create mode 100644 .changeset/adr-0131-total-organization-ownership.md create mode 100644 docs/adr/0131-total-organization-ownership-no-null-organization-id.md diff --git a/.changeset/adr-0131-total-organization-ownership.md b/.changeset/adr-0131-total-organization-ownership.md new file mode 100644 index 0000000000..14d4da8399 --- /dev/null +++ b/.changeset/adr-0131-total-organization-ownership.md @@ -0,0 +1,7 @@ +--- +--- + +Docs-only: adds ADR-0131 (total organization ownership — retire NULL `organization_id`, +a platform organization owns deployment-level rows, sharing is declared). Releases nothing — +no package changes. The ADR is Proposed; implementation cards are cut from it after the +maintainer's hand-merge (Prime Directive #14), never ahead of it. Refs #13564. diff --git a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md new file mode 100644 index 0000000000..24a6642e5f --- /dev/null +++ b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md @@ -0,0 +1,544 @@ +# ADR-0131: Organization ownership is total — no NULL `organization_id`, a platform organization owns deployment-level rows, and sharing is declared + +**Status**: Proposed (2026-09-04) — awaiting the maintainer's hand-merge, which is itself the +acceptance act for a governed surface (Prime Directive #14). ⛔ Nothing below is settled until +this record merges; the implementation cards are cut **from** the merged ADR, never ahead of it. +**Deciders**: ObjectStack maintainer, 2026-09-03/04, live chat on +[#13564](https://github.com/objectstack-ai/objectstack/issues/13564), verbatim and untranslated — +first the premise: 「这个是很严肃的问题,平台自带的角色、岗位、权限集、系统元数据、设置项、通知模板。 +这些不是从代码加载的元数据吗?理论上不需要写到数据库中啊,在单独多组织隔离模式下,我理解是使用需要墙的。」 +then the principle this record implements: 「或者说我们数据库中,不应该有允许 org_id 为空的状况。」 +and the approval to draft: 「认可这个方向,起草 ADR 和分阶段的卡」. +**Builds on**: [ADR-0005](./0005-metadata-customization-overlay.md) (the metadata overlay whose +environment layer is keyed today by a NULL organization — amended by D4), +[ADR-0066](./0066-unified-authorization-model.md) D2 (`tenancy.enabled:false` as the platform-global +posture — kept, and given the single meaning "no tenant column" by D1), +[ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md) D2/D5 (the membership reconciler; degraded +tenancy fails fast — the reason the measured leak's precondition is a refused boot today), +[ADR-0095](./0095-authz-kernel-tenant-layer-and-posture-ladder.md) D1 (Layer 0, the strict tenant +wall this record makes agree with the driver by construction), +[ADR-0105](./0105-group-tenancy-posture-and-first-class-org-scope.md) D2 (the `group` union scope +that the single computed scope of D6 generalizes), +[ADR-0120](./0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md) D3/D4/D7 +(`COALESCE(organization_id, '__global__')`, the migration ceremony, and the 17.x → protocol 18 +staging this record's D11 aligns with), +[ADR-0123](./0123-no-active-organization-session-semantics.md) D2–D4 ("no silent NULL stamping" for +the no-active-organization session — generalized by D7 to every writer in every posture), +[ADR-0049](./0049-no-unenforced-security-properties.md) (a declared shape that is not enforced is removed — the +posture D10's retirements follow), and the two censuses on #13564 (the 2026-08-31 write-side +reuse and the 2026-09-02 read-side ledger) that supplied the object-by-object inventory D8 consumes. +**Evidence**: #13564 (both ledgers and the 2026-09-03 cloud-side supplement), +[#10103](https://github.com/objectstack-ai/objectstack/issues/10103) (two implementations of one +predicate, the catalog reading zero under a wall), [#13491](https://github.com/objectstack-ai/objectstack/issues/13491) +(the tenant-audit control cut by object classification), [#2734](https://github.com/objectstack-ai/objectstack/issues/2734) +(why the NULL arm was added), [#12699](https://github.com/objectstack-ai/objectstack/issues/12699) +(the deployment-level platform-global declaration), [#14484](https://github.com/objectstack-ai/objectstack/issues/14484) +and [#14547](https://github.com/objectstack-ai/objectstack/issues/14547) (two live NULL-row defects +found while this record was being discussed), cloud#1232 / cloud#1239 (the measured cross-tenant +read of a credentials table through the NULL arm), cloud#1663 / cloud#1664 (the label-the-row repairs +and the control plane's wall). Every framework anchor below was re-verified against `origin/main` +`2514d49f3` while drafting; cloud anchors against `cloud` `main` `3856fbf7`. +**Consumers**: `@objectstack/spec` (`injected-system-columns.ts`, the org-scoping entitlement in +`tenancy-posture.ts`), `@objectstack/objectql` (`engine.ts` `buildDriverOptions`, +`tenancy/system-write-organization.ts`, `tenancy/platform-object-tenancy.ts`), every driver that +implements a tenant scope (`driver-sql` `applyTenantScope`, `driver-memory` and `driver-mongodb` +tenancy guards, `driver-turso`, `driver-sqlite-wasm`), `plugin-security` (`tenant-layer.ts`, +`per-organization-catalog.ts`, the five bootstrap seeders, `bootstrap-platform-admin.ts`), +`plugin-auth` (`ensure-default-organization.ts`), `@objectstack/core` +(`security/resolve-authz-context.ts`), `metadata-core` / `metadata-protocol` (the overlay), +`service-settings`, `plugin-email`, `plugin-sharing`, `plugin-audit`, `service-storage`, +`plugin-approvals`, `service-automation`, and — in their own repository — `cloud`'s control plane +(`control-plane-platform-global.ts`, `control-plane-org-scope-plugin.ts`, `migrations/org-id-backfill.ts`) +and `objectos-ee`. + +--- + +## TL;DR + +Today a NULL `organization_id` means two things at once: **"this row belongs to the platform"** and +**"whoever wrote this row forgot to say whose it is."** The database cannot tell them apart, so the +SQL driver's tenant predicate — `(organization_id = :tenant OR organization_id IS NULL)`, added by +#2734 so that seeded roles stay visible — hands a forgotten row to every tenant. cloud#1239 measured +exactly that on a credentials table. + +This record makes NULL mean **nothing**: + +- **D1** — `organization_id` is NOT NULL wherever it exists. An object whose rows have no tenant has + **no column**, not a nullable one. +- **D2** — Every deployment has one **platform organization**, created before any seeder runs, in + every posture. It is the owner of every deployment-level row. +- **D3** — The RBAC catalog is per-organization in every posture (`single` = one Default Organization). +- **D4** — Registries, templates, the settings global rung and the ADR-0005 environment layer are + owned by the platform organization. +- **D5** — Whether tenants may read platform-organization rows of an object is a **declaration**, per + object, per deployment — generalizing #12699. +- **D6** — Layer 0 and every driver consume **one** computed scope, so they cannot disagree (closes + #10103's cause 1). +- **D7** — A write without a resolvable organization is **refused**, in every posture. ⛔ Nothing ever + defaults to the platform organization. +- **D8** — Existing NULL rows get one of three fates — column dropped, platform-owned, or attributed to + their real organization — loudly, per table, with no reaping and no guessing. +- **D9–D11** — Postures differ only in enforcement; the compatibility arms are retired when the data + is clean; the constraint lands at protocol 18. + +The two censuses on #13564 are not discarded by this record — they become its migration inventory. + +--- + +## 1. Context + +### 1.1 One column, two meanings + +The SQL driver's read-side tenant chokepoint, `applyTenantScope` +(`packages/drivers/driver-sql/src/sql-driver.ts`), emits two arms with a NULL disjunct: the equality +arm `where(field, tenantId).orWhereNull(field)` and, under the `group` posture, the union arm +`whereIn(field, tenantIds).orWhereNull(field)`. Its own docblock states why: + +> a NULL tenant column marks a GLOBAL/platform row (bootstrap-seeded positions and permission sets, +> business units, pre-org first-boot seeds). Such a row belongs to no OTHER tenant, so the +> cross-tenant wall must not hide it: with strict equality every tenant admin saw ZERO RBAC rows on a +> fresh deployment, because every platform row is org-less (#2734). + +The rationale is true of the rows it names. It is also true of every row a writer forgot to stamp — +and the database holds both populations in the same NULL. cloud#1239 measured the consequence +end-to-end: an organization admin of tenant A read tenant B's `sys_environment_credential` rows +(`secret_ciphertext`, `encryption_key_id`) and `sys_package_installation` rows, because both objects +receive an injected `organization_id` that their raw-driver writers never populated, and the NULL arm +was the only wall standing on that deployment. + +### 1.2 What the censuses established (#13564, two rounds) + +1. **Whether the arm fires is a property of the caller, not the object.** `buildDriverOptions` + (`packages/objectql/src/engine.ts`) threads `execCtx.tenantId` for every object that is not + `tenancy.enabled:false` or federated. The dominant read shape in the platform namespace is a bare + `{ isSystem: true }` context — no `tenantId`, therefore **no scoping at all** (over 100 sites across + 30 objects). The arm's live consumers are the internal `{ isSystem: true, tenantId }` passes. +2. **On a walled deployment running `plugin-security`, tenants already do not see NULL rows.** + `computeTenantLayer0Filter` (`packages/plugins/plugin-security/src/tenant-layer.ts`) composes a + strict `organization_id = :tenant`; ANDed over the driver's arm, the conjunction is the strict + equality alone. This is #10103's symptom: on a real `isolated` deployment every principal listed + zero positions, permission sets and sharing rules while the tables held rows. +3. **The measured leak ran on a degraded posture.** cloud's control plane requested `isolated` without + mounting `@objectstack/organizations`; the posture resolved to `single`, Layer 0 was inert, and + the arm was the only wall. Today that precondition is a refused boot in the framework + ([ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md) D5, escape hatch + `OS_ALLOW_DEGRADED_TENANCY`), in `objectos-ee` (cloud#1020) and on the control plane (cloud#1664, + `supportedPostures = ['isolated']`). +4. **The arm has more load-bearing dependents than the driver comment names.** Beyond `sys_position`'s + authorization read (`resolve-authz-context.ts` §6a: organization-less rows "stay REACHABLE on + purpose") and `per-organization-catalog.ts`'s residue detection ("depends on that arm being + there"), the deployment-level platform-global declaration of #12699 disarms **only Layer 0**; the + engine still threads `tenantId`, so a tenant's read of a declared-global object sees the + deployment's NULL rows **through the arm**. cloud's control plane declares 32 such objects + (`control-plane-platform-global.ts`) and pins their tenant reads in + `unscoped-control-plane-tenant-wall.test.ts`. Neither census round could see this from inside + the framework repository. +5. **The read-side ledger placed all 59 tenant-column platform objects**: 9 load-bearing, 12 accidental, + 38 undetermined in four named sub-groups — and the undetermined sub-groups are undetermined because + NULL cannot say which meaning it carries. Two live producers surfaced while it was being written: + every `sys_record_share` row lands NULL (#14484); a seeded `sys_business_unit` at NULL silently + defeats sharing rules because a service open-coded the strict equality (#14547). + +### 1.3 Two implementations of one predicate + +#10103 named the structural defect: the wall's predicate exists twice — governed in the driver +(`check-tenant-chokepoint` re-derives that every read door routes through it) and ungoverned in +`plugin-security` — and they disagree on NULL. Every option on #13564 ("keep the arm and add rules +above it", "narrow it to declared-global objects", "switch on posture") keeps the two implementations +and keeps NULL as a meaning; each therefore reproduces one of the two failures (#10103's blank catalog +or cloud#1239's leak) on some path. + +### 1.4 What already exists + +The record is small because most of its parts are built: + +- The open framework already bootstraps a **Default Organization** under `single` + (`packages/plugins/plugin-auth/src/ensure-default-organization.ts`, slug `default`, system context, + idempotent) — but on `kernel:ready`, **after** the seeders, which is why seeds land org-less + (measured 1.3 s ahead of the first `sys_organization` on a fresh walled rig). +- Under a walled posture the RBAC catalog is **already per-organization** (#10103 Option C, + `catalogIsPerOrganization(posture)` in `per-organization-catalog.ts`); the platform bucket there is + "meant to be unreadable through the tenant wall" (cloud's own words). +- A system write with no organization is **already derived** when exactly one organization exists and + **refused** on a walled posture (`resolveSystemInsertOrganization`, + `packages/objectql/src/tenancy/system-write-organization.ts`; #8844, #13491) — for objects the + #13491 ledger classified. ⛔ Its docblock already forbids silently defaulting to `__global__`. +- An object can already declare that it has **no tenant column** (`systemFields.tenant: false` / + `tenancy.enabled: false`, `packages/spec/src/data/injected-system-columns.ts`). +- A deployment can already declare objects **platform-global** for Layer 0 (#12699, + `deployment-org-scoping-entitlement.ts`), and cloud's control plane maintains such a list with a + written three-clause derivation rule. +- A named owner instead of NULL has precedent: the autonumber counter files org-less rows under the + `__global__` sentinel ([ADR-0120](./0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md) D3). +- Per-table backfills with a maintainer order each exist for `sys_file` + (`packages/services/service-storage/src/backfill-sys-file-organizations.ts`), the approval family + (`packages/plugins/plugin-approvals/src/backfill-platform-row-organizations.ts`) and, in cloud, six + raw-driver objects (`migrations/org-id-backfill.ts`, cloud#1663). + +What is missing is the principle that ties them together: **ownership is total**, so NULL has nothing +left to mean. + +--- + +## 2. Decision + +### D1 — NULL is not a state + +Every object that carries `organization_id` carries it **NOT NULL** — at the DDL (emitted by the +schema sync once D8 has cleared the table, see D11) and at the engine (D7). There is no third shape: +an object whose rows belong to no organization declares `systemFields.tenant: false` (or +`tenancy.enabled: false`, [ADR-0066](./0066-unified-authorization-model.md) D2) and has **no column**; +an object that has the column has an owner on every row. + +This is the whole record in one sentence. Everything below exists to make D1 true without changing +what any tenant can see. + +### D2 — The platform organization + +Every deployment has exactly one **platform organization**: a `sys_organization` row with the reserved +slug `platform`, created by the kernel **before any seeder runs**, in every posture. Under `single`, +the Default Organization (`ensureDefaultOrganization`) is created at the same point, so no seeder ever +runs against an installation with zero organizations. + +The platform organization is an **owner of rows, not a container of principals**: + +- it has no members; it does not appear in membership-derived listings (better-auth's + `organization/list`), the org switcher, or the membership reconciler ([ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md) D2); +- `resolveSystemInsertOrganization`'s "exactly one organization ⇒ derive" counts **non-platform** + organizations only, so the Default Organization — not the platform one — is what a `single` + deployment derives; +- quota, billing and per-organization seeding sweeps (`listSeedOrganizationIds`) skip it. + +Platform-level standing stays config-derived (#13514 L4, `OS_PLATFORM_OWNER_EMAIL`); the grant rows +that express it today at NULL (the `single`-posture first-user `admin_full_access` row, Choice 4A) are +owned by the platform organization under D4. Whether platform standing may later be *modelled* as +membership in the platform organization is deferred (§6 Q2), not decided here. + +### D3 — The catalog is per-organization in every posture + +`catalogIsPerOrganization` becomes unconditionally true. The four catalog seeders +(`bootstrapBuiltinRoles`, `bootstrapDeclaredPositions`, `bootstrapDeclaredPermissions`, +`bootstrapDeclaredSharingRules`) and `bootstrapPlatformAdmin`'s `defaultPermissionSets` write **one +pass per organization**; under `single` that is one pass for the Default Organization. The +organization-less "platform bucket" of `sys_position` / `sys_permission_set` / +`sys_position_permission_set` ceases to be produced. `seedCtx()` without an organization becomes a +programming error rather than a `single`-posture branch. + +This is the maintainer's premise made structural: the walled posture already worked this way; `single` +is now the same code path with one organization. + +### D4 — Deployment-level rows are owned by the platform organization + +Rows that belong to the deployment and not to any tenant are stamped with the platform organization: + +- the [ADR-0005](./0005-metadata-customization-overlay.md) **environment layer** of `sys_metadata` + (every type that is not `allowOrgOverride`), `sys_metadata_activation` install-level rows, + environment-level `sys_view_definition` rows, and env-wide `sys_metadata_audit` / `_commit` / + `_history` rows — the overlay's resolution order (organization row → environment row → in-memory + registry) is unchanged; only the key of the middle layer changes from NULL to the platform + organization. **ADR-0005 is amended accordingly** (its "no overlay row = platform-global" sentence + about the registry is untouched); +- the platform's **capability registry** rows (`sys_capability`, `managed_by: 'platform'`); +- **seeded templates** (`sys_email_template`, `sys_notification_template`); +- the **settings global rung** (`sys_setting` `scope: 'global'`, and its audit rows); +- **platform-global sharing rules** (`sharing-rule-service.ts::criteriaContext`'s "owned by no + organization" class) and **global** `sys_user_position` assignments; +- the platform-standing grant rows of D2. + +The `__global__` sentinel ([ADR-0120](./0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md) D3) +resolves to the platform organization's id; the `COALESCE` becomes inert and is removed under D10. + +### D5 — Sharing is declared, per object, per deployment + +Whether a **tenant** principal may read the platform organization's rows of an object — alongside its +own — is a declaration, never an inference from the column's value. The existing deployment-level +declaration of #12699 (`platformGlobalObjects` on the org-scoping entitlement) is generalized to +`platformSharedObjects` with these semantics: + +- **listed**: tenant reads admit platform-organization rows in addition to the tenant's own; writes to + platform-organization rows require platform standing; +- **not listed**: platform-organization rows are invisible to tenants, exactly like any other + organization's rows. + +The framework ships a **default** list for its own platform objects whose platform rows are meant to be +shared in a tenant runtime (the D4 owners: capability registry, templates, the metadata environment +layer, the settings global rung); a deployment extends or shrinks it — because the same object is +per-organization on a tenant runtime and deployment-wide on a control plane (`sys_setting` is cloud's +own example), the declaration cannot be a static property of the object. The per-object authoring +channel keeps its D1 meaning ("no column") and is **not** the sharing declaration. + +Declaring an object shared is a widening, and stays a governed decision on the deployment's side — the +cloud control plane's three-clause derivation rule ("registered here; Layer 0 would otherwise wall it; +rows belong to the deployment, never to an organization — a question about **writers**") is adopted as +the written bar for adding a name. + +### D6 — One scope, computed once, consumed by every wall + +The engine computes one **tenant read scope** per operation — +`{ organizationIds: [...], sharedOrganizationIds: [...] }` — from the posture, the context +(`tenantId` under `isolated`, `accessible_org_ids` under `group`, [ADR-0105](./0105-group-tenancy-posture-and-first-class-org-scope.md) D2) +and the D5 declaration, and threads **that object** both to Layer 0 (`computeTenantLayer0Filter`) and to +the driver (`DriverOptions`). Both emit `organization_id IN (…organizationIds, …sharedOrganizationIds)`. +No layer re-derives the scope; no layer has a NULL arm. + +Two corrections fall out of this by construction: + +- #10103 cause 1 (two implementations disagreeing) cannot recur — there is one input; +- #12699's "declared global ⇒ Layer 0 stands down entirely" over-widening is replaced by "admit the + platform organization": a declared-shared object is still walled between tenants. + +Every driver that implements a tenant scope is held to the same predicate; `check-tenant-chokepoint` +is extended to refuse an `IS NULL` arm on a tenant column. + +### D7 — A missing stamp is a refused write, in every posture + +`resolveSystemInsertOrganization` generalizes [ADR-0123](./0123-no-active-organization-session-semantics.md) D3 +to every writer: a write on a tenant-column object that carries no organization is **derived** when +exactly one non-platform organization exists, and **refused** otherwise — under `single` as well as +under a wall — with a message naming what is missing ([ADR-0123](./0123-no-active-organization-session-semantics.md) D4). + +⛔ **Nothing ever defaults to the platform organization.** A writer that means the platform names it +explicitly (a `platformCtx()` sibling of `seedCtx()`); a writer that forgets gets a refusal, never a +shared row. Silently stamping the platform organization would rebuild the shared pile under a new +name — the one outcome this record exists to make impossible. + +The #13491 classification (`tenant-scoped` / `global` / `unclassified`) becomes structural: an object +with the column is in scope; an object without it is not; there is nothing left to be `unclassified` +about. The ledger `platform-object-tenancy.ts` is retired under D10 once D8 has run. + +### D8 — Migration: three fates, loud, per table, no reaping + +Existing NULL rows are resolved by **one generic migration family** driven by an inventory that the two +#13564 ledgers seed (59 platform objects + 28 example objects; the cloud supplement covers the +cloud-declared ones). Every object gets exactly one fate: + +1. **Column dropped** (D1 tenant-less objects — the read-side ledger's U-A plumbing group and every + object whose writer confirms "infrastructure rows, not tenant data"), through the + [ADR-0120](./0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md) D4 migration ceremony + and an [ADR-0087](./0087-metadata-protocol-upgrade-contract.md) registry entry. +2. **Platform organization** (D4 objects): `NULL → platform` in one statement per table. +3. **Real organization** (accidental rows): derived from a declared parent anchor + (`childKey` / `parentObject` / `parentOrgColumn`, the shape cloud's `org-id-backfill.ts` and this + repository's two backfills already share), per table, with the citation recorded in the inventory. + RBAC residue — pre-fix organization-less catalog rows that grants point at by id — is **adopted** + into per-organization copies with the grants remapped (cloud#1664 item 5's runbook made + first-class), superseding #10103's warn-not-reap posture now that the adopt path is defined. + +A row whose owner cannot be derived is **left NULL and reported**: the boot report lists, per table, +the count of unattributed rows and the remedy. ⛔ No row is deleted; no unattributed row is assigned to +the platform organization. The NOT NULL constraint (D1) is applied to a table **only when that table +reports zero NULL rows** — a table that still carries them stays red in the report and keeps its +nullable column until an operator resolves them. + +### D9 — Postures differ only in enforcement + +Ownership, seeding and constraints are identical across `single`, `group` and `isolated`. What differs +is whether the wall is enforced: under `single` Layer 0 contributes nothing (as today) and the driver +scopes to the Default Organization plus the shared set; under a wall both layers enforce D6's scope. +Degraded tenancy stays a refused boot ([ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md) D5). +`single` is no longer a reason for any row to be org-less. + +### D10 — Retirements (ADR-0049) + +When D8 reports zero NULL rows on every table of a deployment, the following are removed rather than +kept as dead compatibility: both `orWhereNull` arms in `applyTenantScope` and any sibling driver's +equivalent; the NULL readings in `resolve-authz-context.ts` (§4, §6, §6a, §6b), +`per-organization-catalog.ts` (`warnPreFixOrganizationLessRows`, residue detection), +`sharing-rule-service.ts::criteriaContext`, `settings-service.ts`'s global-rung special case, +`meta-write-org-scope.ts`'s NULL layer, `bootstrap-system-capabilities.ts`'s +`organization_id: null` identity predicate; the `__global__` sentinel and ADR-0120 D3's `COALESCE`; +the `platform-object-tenancy.ts` ledger and `isPlatformObjectOutOfTenantAuditScope`; #12699's +stand-down semantics (replaced by D5/D6). Each removal is an [ADR-0087](./0087-metadata-protocol-upgrade-contract.md) +entry where it retires an authorable shape. + +### D11 — Staging + +Phases that add — the platform organization (D2), the computed scope and declaration (D5/D6), the +seeders and writers (D3/D4/D7 in refusing mode behind the existing #13491 gate), the migration (D8) — +ship in **17.x**, with the driver's arms kept as the compatibility path. The NOT NULL constraint, the +every-posture refusal and the arm removal (D1/D7/D10) land at **protocol 18**, in the same staging +[ADR-0120](./0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md) D7 set for the +uniqueness work they compose with. Until D10 runs, ⛔ no card narrows or removes an arm. + +--- + +## 3. Non-goals + +- **Changing what tenants can see.** Platform rows that are shared today remain shared where the + deployment declares them so (D5); the record changes the *key* under which they are owned and the + *mechanism* by which sharing is expressed, not the visible result set. +- **Moving code-declared metadata into or out of the database.** Object/field/view metadata declared + in code stays in the in-memory registry (ADR-0005's base layer); the RBAC catalog's persistence + (Setup editability, provenance, grants by id — ADR-0078, ADR-0086 D5) is not revisited here. Whether + declared catalog rows *should* be persisted at all is a separate question the maintainer's premise + raises and this record does not answer. +- **better-auth-managed tables.** They have no tenant column (`managedBy: 'better-auth'` suppresses + injection) and are untouched. +- **The platform organization as a principal container.** No membership, no login, no SSO binding + (deferred, §6 Q2). +- **Reaping.** No data row is deleted by any part of this record (D8). +- **cloud's data model beyond ownership.** Scope rules anchored on `environment_id` / `invoice_id` + (cloud#1239, cloud#1255) stay; only the injected `organization_id`'s owner changes. + +--- + +## 4. Consequences + +**What becomes true.** + +- A forgotten stamp is a failed write (D7) and, once D1 lands, a constraint violation — it can no longer + land in a pile every tenant reads. The class cloud#1239 belongs to is closed structurally, not by + per-table repairs after each measurement. +- "Is this platform row visible to tenants?" has one answer in one place (D5), consumed by both walls + (D6). #10103's blank catalog and cloud#1239's leak are the same disagreement; D6 removes the second + implementation. +- The #13564 undetermined groups dissolve: an object is either tenant-less (no column), platform-owned, + or tenant-owned. The read-side ledger's 38 rows each acquire one of three fates in D8's inventory. +- ADR-0120 D3's NULL-safe uniqueness simplifies to plain per-organization uniqueness. +- #14547's class ("a service open-coded the strict equality and lost the seeded rows") cannot recur: + seeded units belong to an organization (D3/D7) and there is no NULL arm to forget. + +**What it costs.** + +- Boot ordering changes (D2): two bootstraps move ahead of every seeder; the plugin ordering is + declared ([ADR-0116](./0116-plugin-ordering-declared-contract.md)), not incidental. +- Every platform writer is touched once (D3/D4/D7) — the read-side ledger enumerates them; the + dominant shape (bare `SYSTEM_CTX`) does not need a tenant on reads, but every **write** site names + its owner. +- Tests that pin NULL semantics are rewritten to pin the new predicate (`deal_p1`, + `sql-driver-tenant-scope`, `memory-tenancy-guard`, `rule-criteria-org-scope`, + `per-organization-catalog`, cloud's `unscoped-control-plane-tenant-wall`). +- A live migration on customer databases (D8), staged per table and gated per table; unattributable + rows are an operator task, surfaced loudly, never resolved by the platform guessing. +- `sys_organization` gains a reserved row; any admin surface that lists *all* organizations (not + membership-derived) must mark or hide it (verified per surface; objectui card if one exists). +- Under `group`, the driver's `IN` list grows by one id (the platform organization) on declared-shared + objects; index shape is unchanged. + +**Risks.** + +- A writer "helpfully" stamping the platform organization to make a refusal go away. D7 forbids it in + terms; review and the chokepoint gate look for it; the boot report of D8 would show the shared pile + reforming under the platform id. +- A declared-shared list that grows by convenience. D5 adopts cloud's written three-clause bar and + keeps the list a governed deployment decision. +- A `single` deployment whose Default Organization bootstrap fails (it is best-effort today). D2 makes + it load-bearing: a failure is a boot error, not a degraded warning. + +--- + +## 5. Alternatives considered + +| Alternative | Why not | +|---|---| +| **(a) Keep the arm; add per-object scope rules above the driver** (cloud#1239's pattern; #13564's option a) | Keeps NULL's double meaning. Every new writer inherits fail-open; every repair is per table after a measurement. It is the status quo the maintainer rejected: 「不应该有允许 org_id 为空的状况」. | +| **(b) Narrow the arm to declared-global objects** | "Global" is per deployment, not per object (`sys_setting`), so the driver — constructed from connection config with no kernel access — cannot know it; and #12699's list would still ride the arm. It moves the fail-open behind a list without removing it. | +| **(c) Switch the arm on posture** | Keyed on effective posture it would have kept the arm on cloud#1239's degraded deployment (the leak); keyed on requested posture it blanks every #12699 object and revokes pre-fix RBAC grants silently. Either way NULL keeps a meaning under `single`. | +| **NOT NULL without a platform organization** (per-organization copies of everything) | Impossible for the shared-schema environment layer (ADR-0005: object/field overlays cannot be per-organization in a shared DB) and for the capability registry; it would also multiply every seeded template per tenant. | +| **A string sentinel (`__global__`) instead of a real organization row** | No FK to `sys_organization`, so lookups, RLS and admin surfaces treat it as a special case forever; ADR-0120 D3 already shows the COALESCE this costs. A real row lets every layer treat the platform like any other owner. | +| **Serve the declared catalog from the registry and persist nothing** | Answers the maintainer's first question differently and may be right — but it changes Setup editability, provenance and grants-by-id (ADR-0078, ADR-0086 D5) and is independent of NULL: persisted rows would still need an owner. Recorded as a follow-up question, not folded in (§6 Q3). | + +--- + +## 6. Open questions for the maintainer's merge decision + +1. **Identity of the platform organization.** Slug `platform` and a fixed display name are proposed; + should it be visible (marked) or hidden in platform-admin organization lists? +2. **Platform standing as membership.** D2 keeps standing config-derived. Should a later record allow + "member of the platform organization" to *be* platform standing (replacing the legacy grant anchor)? + Deferred here. +3. **Persisting the declared catalog at all.** The premise 「理论上不需要写到数据库中」 is not decided + by this record (§3). Should a follow-up card measure what Setup, provenance and grants-by-id would + lose if declared roles/positions/permission sets were served from the registry and only + runtime-created ones persisted? +4. **cloud's recipient-anchored group** (`sys_inbox_message`, `sys_notification*`, + `sys_user_preference`): D1 (no column, anchored on `user_id`) or D4 (platform-owned, shared)? cloud's + own reading ("their inbox follows them, not their active workspace") points at D1. +5. **Staging confirmation.** D11 aligns with ADR-0120 D7 (17.x additive, protocol 18 constraint). + Confirm, or name a different boundary. + +--- + +## 7. Verification notes — where citations did not survive re-verification + +- The issue body of #13564 cites the arm at `sql-driver.ts:7320`; on `origin/main` `2514d49f3` the + arms are inside `applyTenantScope` (~line 12016–12066) and there are **two** of them. This record + cites the symbol, not the line. +- `ensure-default-organization.ts` cites "ADR-0081 D1" for the Default Organization; in this + repository `docs/adr/0081` is the trusted React page tier. The framework record that describes the + bootstrap is [ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md) (§D6/D7 compose with it); + the "0081" is cloud's numbering. This record cites the module by path. +- The read-side ledger stated the arm's live tenant-facing surface is empty on a walled deployment + running `plugin-security`. Re-verified — with the addition in §1.2 item 4: the #12699 deployment + declaration is a tenant-facing dependent the ledger could not see, because it lives in cloud. +- `driver-memory` and `driver-mongodb` carry tenancy **guards** (`memory-tenancy-guard.ts`, + `mongodb-tenancy-guard.ts`) keyed on `tenancy.enabled === true`, not a NULL-armed predicate. D6 holds + them to the same computed scope; the audit of each driver's actual predicate is a card, not an + assumption recorded here. + +--- + +## 8. Execution plan (cards are cut from the merged record) + +One epic tracks the family. Phases 1–3 are additive (17.x); phase 4 is protocol 18. + +| # | Card | Decisions | Blocked by | +|---|---|---|---| +| C1 | Platform organization exists before the first seeder, in every posture | D2, D9 | ADR merge | +| C2 | One tenant read scope; `platformSharedObjects` threads to Layer 0 and every driver | D5, D6 | ADR merge | +| C3 | Catalog per-organization in every posture; registry, platform grants, authz reads on the platform organization | D3, D4 | C1, C2 | +| C4 | ADR-0005 environment layer owned by the platform organization | D4 | C1 | +| C5 | Every remaining platform writer names its owner — platform organization or no column; U-A/U-B adjudicated | D1, D4, D7 | C1 | +| C6 | Generic NULL-organization backfill: three fates, per-table boot report | D8 | C3, C4, C5 | +| C7 | NOT NULL; unstamped writes refused in every posture; both arms retired — protocol 18 | D1, D7, D10, D11 | C6 | +| C8 | cloud: control plane and objectos-ee adopt the record (cloud repository) | D1, D4, D5, D8 | C2, then C7 | +| C9 | Docs and card-family close-out (#13564, #11611, #10103 residue posture) | — | C7, C8 | + +⛔ Rules every card inherits: no card touches the driver's arms before C7; no writer ever defaults to +the platform organization; no row is reaped; `content/docs/releases/` is never edited in a code PR. + +--- + +## 9. References + +- Issues: [#13564](https://github.com/objectstack-ai/objectstack/issues/13564) · + [#10103](https://github.com/objectstack-ai/objectstack/issues/10103) · + [#13491](https://github.com/objectstack-ai/objectstack/issues/13491) · + [#2734](https://github.com/objectstack-ai/objectstack/issues/2734) · + [#12699](https://github.com/objectstack-ai/objectstack/issues/12699) · + [#14484](https://github.com/objectstack-ai/objectstack/issues/14484) · + [#14547](https://github.com/objectstack-ai/objectstack/issues/14547) · + [#11611](https://github.com/objectstack-ai/objectstack/issues/11611) · + [#8844](https://github.com/objectstack-ai/objectstack/issues/8844) · + [#13514](https://github.com/objectstack-ai/objectstack/issues/13514) · cloud#1020 · cloud#1232 · + cloud#1239 · cloud#1255 · cloud#1663 · cloud#1664. +- Records: [ADR-0005](./0005-metadata-customization-overlay.md) · + [ADR-0049](./0049-no-unenforced-security-properties.md) · [ADR-0066](./0066-unified-authorization-model.md) · + [ADR-0078](./0078-no-silently-inert-metadata.md) · [ADR-0086](./0086-authz-metadata-config-boundary-and-cross-package-composition.md) · + [ADR-0087](./0087-metadata-protocol-upgrade-contract.md) · [ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md) · + [ADR-0095](./0095-authz-kernel-tenant-layer-and-posture-ladder.md) · + [ADR-0105](./0105-group-tenancy-posture-and-first-class-org-scope.md) · + [ADR-0116](./0116-plugin-ordering-declared-contract.md) · + [ADR-0120](./0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md) · + [ADR-0123](./0123-no-active-organization-session-semantics.md). +- Code (framework, `origin/main` `2514d49f3`): `packages/drivers/driver-sql/src/sql-driver.ts` + (`applyTenantScope`, `injectTenantOnInsert`, `auditMissingTenant`); + `packages/objectql/src/engine.ts` (`buildDriverOptions`); + `packages/objectql/src/tenancy/system-write-organization.ts`; + `packages/objectql/src/tenancy/platform-object-tenancy.ts`; + `packages/plugins/plugin-security/src/tenant-layer.ts` (`computeTenantLayer0Filter`); + `packages/plugins/plugin-security/src/per-organization-catalog.ts`; + `packages/plugins/plugin-security/src/bootstrap-platform-admin.ts`; + `packages/plugins/plugin-security/src/bootstrap-system-capabilities.ts`; + `packages/plugins/plugin-security/src/deployment-org-scoping-entitlement.ts`; + `packages/plugins/plugin-auth/src/ensure-default-organization.ts`; + `packages/core/src/security/resolve-authz-context.ts`; + `packages/metadata-core/src/meta-write-org-scope.ts`; + `packages/metadata-protocol/src/protocol.ts` (`getMetaItem`); + `packages/spec/src/data/injected-system-columns.ts`; `scripts/check-tenant-chokepoint.mjs`. +- Code (cloud, `main` `3856fbf7`): `packages/service-cloud/src/control-plane-platform-global.ts`; + `packages/service-cloud/src/control-plane-org-scope-plugin.ts`; + `packages/service-cloud/src/control-plane-organizations.ts`; + `packages/service-cloud/src/migrations/org-id-backfill.ts`; + `apps/cloud/test/unscoped-control-plane-tenant-wall.test.ts`; + `apps/objectos-ee/objectstack.config.ts`. From ff2d6e511dfecc80702029bbb29ac43f957b875a Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Fri, 4 Sep 2026 00:23:47 +0800 Subject: [PATCH 02/24] =?UTF-8?q?docs(adr):=20ADR-0131=20D12=20=E2=80=94?= =?UTF-8?q?=20the=20group=20posture:=20group-shared=20rows=20have=20an=20o?= =?UTF-8?q?wner,=20org-axis=20red=20lines=20stand?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Answers the maintainer's 2026-09-04 question 「集团多组织架构考虑了吗」: fixes the owner ADR-0105 D10's reserved 'platform-global' template rows must take (never NULL — platform organization or group root, declared), keeps D6's red lines, adds §6 Q6. Refs #13564 Co-Authored-By: Claude Fable 5.1 --- ...ation-ownership-no-null-organization-id.md | 85 ++++++++++++++++++- 1 file changed, 81 insertions(+), 4 deletions(-) diff --git a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md index 24a6642e5f..197a446941 100644 --- a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md +++ b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md @@ -17,8 +17,9 @@ posture — kept, and given the single meaning "no tenant column" by D1), tenancy fails fast — the reason the measured leak's precondition is a refused boot today), [ADR-0095](./0095-authz-kernel-tenant-layer-and-posture-ladder.md) D1 (Layer 0, the strict tenant wall this record makes agree with the driver by construction), -[ADR-0105](./0105-group-tenancy-posture-and-first-class-org-scope.md) D2 (the `group` union scope -that the single computed scope of D6 generalizes), +[ADR-0105](./0105-group-tenancy-posture-and-first-class-org-scope.md) D2/D6/D9/D10 (the `group` +union scope that the single computed scope of D6 generalizes; the org-axis red lines D12 keeps; the +`$root` derivation D12 reuses; the reserved layered-master-data shape whose owner D12 fixes), [ADR-0120](./0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md) D3/D4/D7 (`COALESCE(organization_id, '__global__')`, the migration ceremony, and the 17.x → protocol 18 staging this record's D11 aligns with), @@ -80,6 +81,9 @@ This record makes NULL mean **nothing**: their real organization — loudly, per table, with no reaping and no guessing. - **D9–D11** — Postures differ only in enforcement; the compatibility arms are retired when the data is clean; the constraint lands at protocol 18. +- **D12** — Under the `group` posture, group-shared business rows (ADR-0105 D10's templates) have an + owner too: never NULL — the platform organization or the group's root organization, by declaration. + The org-axis red lines stand. The two censuses on #13564 are not discarded by this record — they become its migration inventory. @@ -138,6 +142,18 @@ was the only wall standing on that deployment. NULL cannot say which meaning it carries. Two live producers surfaced while it was being written: every `sys_record_share` row lands NULL (#14484); a seeded `sys_business_unit` at NULL silently defeats sharing rules because a service open-coded the strict equality (#14547). +6. **The `group` posture has a reserved NULL of its own.** + [ADR-0105](./0105-group-tenancy-posture-and-first-class-org-scope.md) D1 defines `group` as + "organizations = membership boundaries over one shared dataset", walled by + `organization_id IN accessible_org_ids` (D2); D6 draws two lint-enforced red lines (no permission + inheritance along `parent_organization_id`; business-unit trees stay org-internal); D9 lets an + approver target `$root`, deriving the group root by walking the chain once + (`plugin-approvals/src/approver-org-scope.ts`). D10 — layered master data, the SAP + material-master / 用友-金蝶 distribution shape — is **reserved, not implemented**, and its reserved + wording is "group-level template rows (**platform-global** or group-org-owned, read-shared) plus + per-org override rows". The "platform-global" half is a NULL row. A record that retires NULL must + say what owns a group template before D10's follow-up ADR is written, or that ADR re-creates the + state this one removes. ### 1.3 Two implementations of one predicate @@ -214,6 +230,14 @@ that express it today at NULL (the `single`-posture first-user `admin_full_acces owned by the platform organization under D4. Whether platform standing may later be *modelled* as membership in the platform organization is deferred (§6 Q2), not decided here. +**The platform organization is not the group root.** On a `group` deployment the top of the +`parent_organization_id` tree — the headquarters — is an ordinary organization with members, quotas +and business data. The platform organization owns the **runtime's** rows (registries, templates, the +environment layer); the group's business rows are owned by the group's organizations (D12). The two +are operated by the same people on many deployments and are still two owners, because a deployment +may host more than one group and because business master-data authorship must not require platform +standing. + ### D3 — The catalog is per-organization in every posture `catalogIsPerOrganization` becomes unconditionally true. The four catalog seeders @@ -360,6 +384,51 @@ every-posture refusal and the arm removal (D1/D7/D10) land at **protocol 18**, i [ADR-0120](./0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md) D7 set for the uniqueness work they compose with. Until D10 runs, ⛔ no card narrows or removes an arm. +### D12 — The `group` posture: group-shared rows have an owner, and the org-axis red lines stand + +What this record already covers for `group` without further decision: + +- **The wall.** D6's `organizationIds` **is** `accessible_org_ids` + ([ADR-0105](./0105-group-tenancy-posture-and-first-class-org-scope.md) D2); the union predicate, + the fail-closed empty set and the active organization as write target are unchanged. +- **The catalog.** D3's per-organization seeding is what #10103 Option C already does under `group`, + and it is the only shape consistent with ADR-0105 D6 ①: a headquarters-authored position is **not** + inherited by a plant; a declared position reaches every organization from code, one copy each. +- **Cross-organization approvals** (ADR-0105 D9): the request row is owned by the plant; the `$root` + approver reads it through membership union or the system-context mirror. NOT NULL changes nothing. +- **The cut-line deployment** (ADR-0105 Appendix A: one organization, factories as business units): + the Default Organization owns everything under D3/D9. + +What this record decides for `group`: + +1. **D6's red lines are not amended by D5.** A sharing declaration names a fixed **owner set** for an + object; it is not an RLS policy, a sharing rule or a scope that reads `parent_organization_id` + per row, and it grants no permission — the reader still needs the object grant. Permission + inheritance along the org tree stays retired and lint-enforced + (`packages/lint/src/validate-org-axis-red-lines.ts`). +2. **ADR-0105 D10's group template rows have an owner, and it is never NULL.** Two owners are + admissible; D10's follow-up ADR chooses **per distribution policy** (集团统管 / 分级 / 自由) under + this constraint: + - **(i) the platform organization + D5** — the template is a deployment-level row. Right when the + deployment *is* the group and its master data is operated by the group's platform team; wrong + for a deployment hosting several groups, and it puts business master-data authorship behind + platform standing. + - **(ii) the group root organization + a second D5 source, `group-root`** — for an object declared + so, the computed scope adds the **caller's root organization** to `sharedOrganizationIds`. The + root is derived **once per request** by the same `parent_organization_id` chain walk D9 already + performs for `$root` (`approver-org-scope.ts`), inside the scope resolver — never per row, never + in a Layer 1 policy — so red line ① is not crossed; the lint gains one allowlisted shape (the D5 + declaration itself) and keeps refusing every other read of the parent reference. Override rows + stay owned by the plant, linked to the template by D10's linkage field; the read-path resolution + rule (org override wins) is D10's to detail. Several groups in one deployment each share to their + own tree. + - **This record recommends (ii) for business master data and keeps (i) for the runtime's own rows.** + The choice is the maintainer's (§6 Q6); until it is made, D10's follow-up ADR may not be + drafted with a NULL owner. +3. **Migration.** A deployment that already expresses a group template as a NULL row takes D8 fate 3 + with the derived owner being the group root (the inventory records the rule per object), or fate + 1 (column dropped) where the object was never tenant data. + --- ## 3. Non-goals @@ -413,7 +482,11 @@ uniqueness work they compose with. Until D10 runs, ⛔ no card narrows or remove - `sys_organization` gains a reserved row; any admin surface that lists *all* organizations (not membership-derived) must mark or hide it (verified per surface; objectui card if one exists). - Under `group`, the driver's `IN` list grows by one id (the platform organization) on declared-shared - objects; index shape is unchanged. + objects — by two if D12 (ii) is chosen (the group root); index shape is unchanged. +- If D12 (ii) is chosen, `validateOrgAxisRedLines` gains exactly one allowlisted shape (the D5 + `group-root` declaration) and its docblock records why that shape is not the inheritance it + forbids; the scope resolver gains a per-request root walk with the cycle guard + `approver-org-scope.ts` already carries. **Risks.** @@ -456,6 +529,10 @@ uniqueness work they compose with. Until D10 runs, ⛔ no card narrows or remove own reading ("their inbox follows them, not their active workspace") points at D1. 5. **Staging confirmation.** D11 aligns with ADR-0120 D7 (17.x additive, protocol 18 constraint). Confirm, or name a different boundary. +6. **Owner of group templates under `group` (D12).** (i) platform organization + shared declaration, + or (ii) group root organization + a `group-root` sharing source resolved once per request. + Recommendation: (ii) for business master data, (i) for the runtime's own rows. This fixes the + constraint ADR-0105 D10's follow-up ADR must draft under. --- @@ -485,7 +562,7 @@ One epic tracks the family. Phases 1–3 are additive (17.x); phase 4 is protoco | # | Card | Decisions | Blocked by | |---|---|---|---| | C1 | Platform organization exists before the first seeder, in every posture | D2, D9 | ADR merge | -| C2 | One tenant read scope; `platformSharedObjects` threads to Layer 0 and every driver | D5, D6 | ADR merge | +| C2 | One tenant read scope; `platformSharedObjects` threads to Layer 0 and every driver; the `group-root` source and the lint allowlist if §6 Q6 = (ii) | D5, D6, D12 | ADR merge | | C3 | Catalog per-organization in every posture; registry, platform grants, authz reads on the platform organization | D3, D4 | C1, C2 | | C4 | ADR-0005 environment layer owned by the platform organization | D4 | C1 | | C5 | Every remaining platform writer names its owner — platform organization or no column; U-A/U-B adjudicated | D1, D4, D7 | C1 | From e2fd907bf84b378acb94f49132286813ade6dbd8 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Fri, 4 Sep 2026 00:28:47 +0800 Subject: [PATCH 03/24] docs(adr): ADR-0131 D12 records the withdrawal of ADR-0105 D10 (group template rows / layered master data) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maintainer rulings 2026-09-04, verbatim: 「不考虑集团级模板行,作废相关需求」「不考虑 分层主数据」. D12 no longer offers an owner choice; §6 Q6 removed; ADR-0105 status line, D10 paragraph and Phase 2 list annotated withdrawn. Refs #13564 Co-Authored-By: Claude Fable 5.1 --- ...nancy-posture-and-first-class-org-scope.md | 6 +- ...ation-ownership-no-null-organization-id.md | 78 ++++++++----------- 2 files changed, 37 insertions(+), 47 deletions(-) diff --git a/docs/adr/0105-group-tenancy-posture-and-first-class-org-scope.md b/docs/adr/0105-group-tenancy-posture-and-first-class-org-scope.md index c4af108121..7bdf33799b 100644 --- a/docs/adr/0105-group-tenancy-posture-and-first-class-org-scope.md +++ b/docs/adr/0105-group-tenancy-posture-and-first-class-org-scope.md @@ -1,6 +1,6 @@ # ADR-0105: Group Tenancy Posture — Organization Scope as a First-Class Authorization Dimension -**Status**: Accepted (2026-07-27; proposed 2026-07-25) — Phase 0/1 implemented (#3559). Amended 2026-07-27: **D12 correction** — `group` posture activation is entitled, not open (#3570; see the D12 Amendment). Phase 2 **D8** and **D9** implemented 2026-07-28 — D8: #3645 (host seam) → #3663 (placement engine) → #3674 (`/security/my-delegable-scope`) → #3695 (issuer-grant resolution) → #3722 (`delegated_admin` + invitation role cap, #3697) → #3767 (`sys_member` governed), console objectui#2868/#2891, e2e cloud#886; the membership-role channel D8's placement replaces is closed by [ADR-0108](./0108-membership-grade-is-not-a-capability-channel.md). D9: #3824 + #3873 (see the D9 amendment below). D10 stays reserved pending its follow-up ADR; D13 not started +**Status**: Accepted (2026-07-27; proposed 2026-07-25) — Phase 0/1 implemented (#3559). Amended 2026-07-27: **D12 correction** — `group` posture activation is entitled, not open (#3570; see the D12 Amendment). Phase 2 **D8** and **D9** implemented 2026-07-28 — D8: #3645 (host seam) → #3663 (placement engine) → #3674 (`/security/my-delegable-scope`) → #3695 (issuer-grant resolution) → #3722 (`delegated_admin` + invitation role cap, #3697) → #3767 (`sys_member` governed), console objectui#2868/#2891, e2e cloud#886; the membership-role channel D8's placement replaces is closed by [ADR-0108](./0108-membership-grade-is-not-a-capability-channel.md). D9: #3824 + #3873 (see the D9 amendment below). D10 **withdrawn** 2026-09-04 by maintainer ruling (「不考虑集团级模板行,作废相关需求」「不考虑 分层主数据」; recorded in [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D12 — see the note under D10); D13 not started **Deciders**: ObjectStack Protocol Architects **Builds on**: [ADR-0049](./0049-no-unenforced-security-properties.md) (enforce-or-remove), [ADR-0057](./0057-erp-authorization-core-business-units-and-scope-depth.md) (business units + scope depth), [ADR-0066](./0066-unified-authorization-model.md) (unified authz, superuser bypass), [ADR-0086](./0086-authz-metadata-config-boundary-and-cross-package-composition.md), [ADR-0090](./0090-permission-model-v2-concept-convergence.md) (permission set / position / business unit), [ADR-0091](./0091-grant-lifecycle-and-recertification.md) (validity windows), [ADR-0092](./0092-sys-user-profile-field-delegation.md) (identity write guard + field whitelist), [ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md) (tenancy service), [ADR-0095](./0095-authz-kernel-tenant-layer-and-posture-ladder.md) (tenant Layer 0, posture ladder), [ADR-0103](./0103-managedby-write-policy-and-engine-write-guard.md); cloud ADR-0016 (open/paid boundary: 强制免费、治理收费), cloud ADR-0081 (`@objectstack/organizations`) **Tracking**: #3541 (P0 findings F1/F2 became #3539/#3540, closed by #3559); cloud-side tracking cloud #874 @@ -305,6 +305,8 @@ linked to the template, with a resolution rule (org override wins). Mechanics policies: 集团统管 / 分级 / 自由) to be detailed in a follow-up ADR; this ADR reserves the concept and its place in Phase 2. +*Withdrawn 2026-09-04.* Maintainer ruling, verbatim and untranslated: 「不考虑集团级模板行,作废相关需求」 and 「不考虑 分层主数据」. No follow-up ADR will be drafted; group-level template rows shared down an organization tree are not a platform concept. Recorded in [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D12, which also states the consequence: a template row "platform-global or group-org-owned, read-shared" would need either a NULL owner — a state ADR-0131 removes — or a tree-derived one, which D6 keeps out of visibility. The `group` posture keeps exactly one kind of cross-organization visibility, membership union (D2). + **D11 — Enforce-or-remove cleanup (ADR-0049 debts on the org axis).** - `PermissionSet.contextVariables`: **remove** from the spec (no consumer; its use cases are covered by `rlsMembership` and literal predicates). @@ -410,7 +412,7 @@ environment). three-plants-one-group dogfood — plant admins configure process data mutually invisibly, group reads all plants on one screen, zero custom security code. -- **Phase 2 (group product depth)**: D8, D9, D10, "all my organizations" +- **Phase 2 (group product depth)**: D8, D9, D10 (withdrawn 2026-09-04), "all my organizations" console affordances, D13 promotion tool. - **Phase 3 (governance, commercial)**: SoD constraints, certification campaigns over ADR-0091 validity/recert data, deny/muting layer (ADR-0066 diff --git a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md index 197a446941..a9f29485f0 100644 --- a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md +++ b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md @@ -17,9 +17,10 @@ posture — kept, and given the single meaning "no tenant column" by D1), tenancy fails fast — the reason the measured leak's precondition is a refused boot today), [ADR-0095](./0095-authz-kernel-tenant-layer-and-posture-ladder.md) D1 (Layer 0, the strict tenant wall this record makes agree with the driver by construction), -[ADR-0105](./0105-group-tenancy-posture-and-first-class-org-scope.md) D2/D6/D9/D10 (the `group` -union scope that the single computed scope of D6 generalizes; the org-axis red lines D12 keeps; the -`$root` derivation D12 reuses; the reserved layered-master-data shape whose owner D12 fixes), +[ADR-0105](./0105-group-tenancy-posture-and-first-class-org-scope.md) D2/D6/D10 (the `group` +union scope that the single computed scope of D6 generalizes; the org-axis red lines D12 keeps; and +D10, the reserved layered-master-data shape, **withdrawn by the maintainer on 2026-09-04** — D12 +records the withdrawal and this record amends ADR-0105 accordingly), [ADR-0120](./0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md) D3/D4/D7 (`COALESCE(organization_id, '__global__')`, the migration ceremony, and the 17.x → protocol 18 staging this record's D11 aligns with), @@ -81,9 +82,10 @@ This record makes NULL mean **nothing**: their real organization — loudly, per table, with no reaping and no guessing. - **D9–D11** — Postures differ only in enforcement; the compatibility arms are retired when the data is clean; the constraint lands at protocol 18. -- **D12** — Under the `group` posture, group-shared business rows (ADR-0105 D10's templates) have an - owner too: never NULL — the platform organization or the group's root organization, by declaration. - The org-axis red lines stand. +- **D12** — Under the `group` posture there are **no group-shared business rows**: ADR-0105 D10 + (group-level template rows) is withdrawn by the maintainer. The only shared owner is the platform + organization (D4/D5); cross-organization visibility is membership union; the org-axis red lines + stand. The two censuses on #13564 are not discarded by this record — they become its migration inventory. @@ -151,9 +153,9 @@ was the only wall standing on that deployment. (`plugin-approvals/src/approver-org-scope.ts`). D10 — layered master data, the SAP material-master / 用友-金蝶 distribution shape — is **reserved, not implemented**, and its reserved wording is "group-level template rows (**platform-global** or group-org-owned, read-shared) plus - per-org override rows". The "platform-global" half is a NULL row. A record that retires NULL must - say what owns a group template before D10's follow-up ADR is written, or that ADR re-creates the - state this one removes. + per-org override rows". The "platform-global" half is a NULL row. A record that retires NULL has + to resolve that reservation one way or the other; the maintainer resolved it by **withdrawing the + requirement** rather than by fixing an owner for it (D12). ### 1.3 Two implementations of one predicate @@ -233,10 +235,9 @@ membership in the platform organization is deferred (§6 Q2), not decided here. **The platform organization is not the group root.** On a `group` deployment the top of the `parent_organization_id` tree — the headquarters — is an ordinary organization with members, quotas and business data. The platform organization owns the **runtime's** rows (registries, templates, the -environment layer); the group's business rows are owned by the group's organizations (D12). The two +environment layer); every business row is owned by the organization it was written in (D12). The two are operated by the same people on many deployments and are still two owners, because a deployment -may host more than one group and because business master-data authorship must not require platform -standing. +may host more than one group and because the runtime's rows are not any group's business data. ### D3 — The catalog is per-organization in every posture @@ -405,29 +406,22 @@ What this record decides for `group`: object; it is not an RLS policy, a sharing rule or a scope that reads `parent_organization_id` per row, and it grants no permission — the reader still needs the object grant. Permission inheritance along the org tree stays retired and lint-enforced - (`packages/lint/src/validate-org-axis-red-lines.ts`). -2. **ADR-0105 D10's group template rows have an owner, and it is never NULL.** Two owners are - admissible; D10's follow-up ADR chooses **per distribution policy** (集团统管 / 分级 / 自由) under - this constraint: - - **(i) the platform organization + D5** — the template is a deployment-level row. Right when the - deployment *is* the group and its master data is operated by the group's platform team; wrong - for a deployment hosting several groups, and it puts business master-data authorship behind - platform standing. - - **(ii) the group root organization + a second D5 source, `group-root`** — for an object declared - so, the computed scope adds the **caller's root organization** to `sharedOrganizationIds`. The - root is derived **once per request** by the same `parent_organization_id` chain walk D9 already - performs for `$root` (`approver-org-scope.ts`), inside the scope resolver — never per row, never - in a Layer 1 policy — so red line ① is not crossed; the lint gains one allowlisted shape (the D5 - declaration itself) and keeps refusing every other read of the parent reference. Override rows - stay owned by the plant, linked to the template by D10's linkage field; the read-path resolution - rule (org override wins) is D10's to detail. Several groups in one deployment each share to their - own tree. - - **This record recommends (ii) for business master data and keeps (i) for the runtime's own rows.** - The choice is the maintainer's (§6 Q6); until it is made, D10's follow-up ADR may not be - drafted with a NULL owner. -3. **Migration.** A deployment that already expresses a group template as a NULL row takes D8 fate 3 - with the derived owner being the group root (the inventory records the rule per object), or fate - 1 (column dropped) where the object was never tenant data. + (`packages/lint/src/validate-org-axis-red-lines.ts`). This record adds **no** new sharing source: + `sharedOrganizationIds` holds the platform organization and nothing else. +2. **ADR-0105 D10 — layered master data (group template rows + per-org overrides) — is withdrawn.** + Maintainer ruling, 2026-09-04, verbatim and untranslated: 「不考虑集团级模板行,作废相关需求」. + No follow-up ADR will be drafted; group-level template rows are not a platform concept. This + record amends ADR-0105 by marking D10 withdrawn (the D10 paragraph carries the note). The + consequence for the `group` posture is that there is exactly **one** kind of cross-organization + visibility — membership union (ADR-0105 D2) — plus the runtime's own platform-organization rows on + declared-shared objects (D5). A group that wants headquarters-authored business data visible in a + plant expresses it the way the posture already provides: the readers are members of the + organization that owns the data. No tree-derived owner, no `group-root` source, no lint + allowlist. +3. **Migration.** Under this ruling a NULL business row on a `group` deployment is by definition not a + "group template": it takes D8 fate 3 (attributed to the organization its parent anchor names) or + fate 1 (column dropped where the object was never tenant data). Nothing is attributed to a group + root; nothing is attributed to the platform organization unless the object is a D4 owner. --- @@ -446,6 +440,8 @@ What this record decides for `group`: - **The platform organization as a principal container.** No membership, no login, no SSO binding (deferred, §6 Q2). - **Reaping.** No data row is deleted by any part of this record (D8). +- **Group-level template rows.** ADR-0105 D10 is **withdrawn**, not deferred (D12); this record + adds no mechanism for headquarters-authored rows shared down an organization tree. - **cloud's data model beyond ownership.** Scope rules anchored on `environment_id` / `invoice_id` (cloud#1239, cloud#1255) stay; only the injected `organization_id`'s owner changes. @@ -482,11 +478,7 @@ What this record decides for `group`: - `sys_organization` gains a reserved row; any admin surface that lists *all* organizations (not membership-derived) must mark or hide it (verified per surface; objectui card if one exists). - Under `group`, the driver's `IN` list grows by one id (the platform organization) on declared-shared - objects — by two if D12 (ii) is chosen (the group root); index shape is unchanged. -- If D12 (ii) is chosen, `validateOrgAxisRedLines` gains exactly one allowlisted shape (the D5 - `group-root` declaration) and its docblock records why that shape is not the inheritance it - forbids; the scope resolver gains a per-request root walk with the cycle guard - `approver-org-scope.ts` already carries. + objects; index shape is unchanged. Nothing else about the `group` posture moves (D12). **Risks.** @@ -529,10 +521,6 @@ What this record decides for `group`: own reading ("their inbox follows them, not their active workspace") points at D1. 5. **Staging confirmation.** D11 aligns with ADR-0120 D7 (17.x additive, protocol 18 constraint). Confirm, or name a different boundary. -6. **Owner of group templates under `group` (D12).** (i) platform organization + shared declaration, - or (ii) group root organization + a `group-root` sharing source resolved once per request. - Recommendation: (ii) for business master data, (i) for the runtime's own rows. This fixes the - constraint ADR-0105 D10's follow-up ADR must draft under. --- @@ -562,7 +550,7 @@ One epic tracks the family. Phases 1–3 are additive (17.x); phase 4 is protoco | # | Card | Decisions | Blocked by | |---|---|---|---| | C1 | Platform organization exists before the first seeder, in every posture | D2, D9 | ADR merge | -| C2 | One tenant read scope; `platformSharedObjects` threads to Layer 0 and every driver; the `group-root` source and the lint allowlist if §6 Q6 = (ii) | D5, D6, D12 | ADR merge | +| C2 | One tenant read scope; `platformSharedObjects` threads to Layer 0 and every driver | D5, D6 | ADR merge | | C3 | Catalog per-organization in every posture; registry, platform grants, authz reads on the platform organization | D3, D4 | C1, C2 | | C4 | ADR-0005 environment layer owned by the platform organization | D4 | C1 | | C5 | Every remaining platform writer names its owner — platform organization or no column; U-A/U-B adjudicated | D1, D4, D7 | C1 | From 47db4f616d77016de8dc4081f652233ac66ae132 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Fri, 4 Sep 2026 01:04:40 +0800 Subject: [PATCH 04/24] =?UTF-8?q?docs(adr):=20ADR-0131=20v2=20=E2=80=94=20?= =?UTF-8?q?declared=20metadata=20stays=20in=20code,=20rows=20only=20when?= =?UTF-8?q?=20an=20organization=20authored=20them,=20no=20platform=20organ?= =?UTF-8?q?ization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewritten on the maintainer's 2026-09-04 rulings (verbatim in the record): declared metadata is never seeded; Studio/build-agent metadata is DB-resident and UI-editable with no organization column; organization-level template editing stays closed (copy-on-write if ever opened); references by name, registry-first; deployment-level state has no column; #13636 / PR #14923 superseded (§1.6); the platform organization of the first draft is recorded as a rejected alternative. Refs #13564 #13636 Co-Authored-By: Claude Fable 5.1 --- .../adr-0131-total-organization-ownership.md | 9 +- ...ation-ownership-no-null-organization-id.md | 1008 ++++++++--------- 2 files changed, 506 insertions(+), 511 deletions(-) diff --git a/.changeset/adr-0131-total-organization-ownership.md b/.changeset/adr-0131-total-organization-ownership.md index 14d4da8399..658ce4c268 100644 --- a/.changeset/adr-0131-total-organization-ownership.md +++ b/.changeset/adr-0131-total-organization-ownership.md @@ -1,7 +1,8 @@ --- --- -Docs-only: adds ADR-0131 (total organization ownership — retire NULL `organization_id`, -a platform organization owns deployment-level rows, sharing is declared). Releases nothing — -no package changes. The ADR is Proposed; implementation cards are cut from it after the -maintainer's hand-merge (Prime Directive #14), never ahead of it. Refs #13564. +Docs-only: adds ADR-0131 (organization ownership is total — no NULL `organization_id`; declared +metadata stays in code and is never seeded; a row exists only when an organization authored it; +references by name; deployment-level state has no organization column) and annotates ADR-0105 D10 +as withdrawn. Releases nothing — no package changes. The ADR is Proposed; implementation cards are +cut from it after the maintainer's hand-merge (Prime Directive #14), never ahead of it. Refs #13564. diff --git a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md index a9f29485f0..7e05b0b092 100644 --- a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md +++ b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md @@ -1,93 +1,92 @@ -# ADR-0131: Organization ownership is total — no NULL `organization_id`, a platform organization owns deployment-level rows, and sharing is declared +# ADR-0131: Organization ownership is total — no NULL `organization_id`; declared metadata stays in code; a row exists only when an organization authored it **Status**: Proposed (2026-09-04) — awaiting the maintainer's hand-merge, which is itself the acceptance act for a governed surface (Prime Directive #14). ⛔ Nothing below is settled until this record merges; the implementation cards are cut **from** the merged ADR, never ahead of it. **Deciders**: ObjectStack maintainer, 2026-09-03/04, live chat on -[#13564](https://github.com/objectstack-ai/objectstack/issues/13564), verbatim and untranslated — -first the premise: 「这个是很严肃的问题,平台自带的角色、岗位、权限集、系统元数据、设置项、通知模板。 -这些不是从代码加载的元数据吗?理论上不需要写到数据库中啊,在单独多组织隔离模式下,我理解是使用需要墙的。」 -then the principle this record implements: 「或者说我们数据库中,不应该有允许 org_id 为空的状况。」 -and the approval to draft: 「认可这个方向,起草 ADR 和分阶段的卡」. -**Builds on**: [ADR-0005](./0005-metadata-customization-overlay.md) (the metadata overlay whose -environment layer is keyed today by a NULL organization — amended by D4), -[ADR-0066](./0066-unified-authorization-model.md) D2 (`tenancy.enabled:false` as the platform-global -posture — kept, and given the single meaning "no tenant column" by D1), -[ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md) D2/D5 (the membership reconciler; degraded -tenancy fails fast — the reason the measured leak's precondition is a refused boot today), -[ADR-0095](./0095-authz-kernel-tenant-layer-and-posture-ladder.md) D1 (Layer 0, the strict tenant -wall this record makes agree with the driver by construction), -[ADR-0105](./0105-group-tenancy-posture-and-first-class-org-scope.md) D2/D6/D10 (the `group` -union scope that the single computed scope of D6 generalizes; the org-axis red lines D12 keeps; and -D10, the reserved layered-master-data shape, **withdrawn by the maintainer on 2026-09-04** — D12 -records the withdrawal and this record amends ADR-0105 accordingly), -[ADR-0120](./0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md) D3/D4/D7 -(`COALESCE(organization_id, '__global__')`, the migration ceremony, and the 17.x → protocol 18 -staging this record's D11 aligns with), -[ADR-0123](./0123-no-active-organization-session-semantics.md) D2–D4 ("no silent NULL stamping" for -the no-active-organization session — generalized by D7 to every writer in every posture), -[ADR-0049](./0049-no-unenforced-security-properties.md) (a declared shape that is not enforced is removed — the -posture D10's retirements follow), and the two censuses on #13564 (the 2026-08-31 write-side -reuse and the 2026-09-02 read-side ledger) that supplied the object-by-object inventory D8 consumes. -**Evidence**: #13564 (both ledgers and the 2026-09-03 cloud-side supplement), -[#10103](https://github.com/objectstack-ai/objectstack/issues/10103) (two implementations of one -predicate, the catalog reading zero under a wall), [#13491](https://github.com/objectstack-ai/objectstack/issues/13491) -(the tenant-audit control cut by object classification), [#2734](https://github.com/objectstack-ai/objectstack/issues/2734) -(why the NULL arm was added), [#12699](https://github.com/objectstack-ai/objectstack/issues/12699) -(the deployment-level platform-global declaration), [#14484](https://github.com/objectstack-ai/objectstack/issues/14484) -and [#14547](https://github.com/objectstack-ai/objectstack/issues/14547) (two live NULL-row defects -found while this record was being discussed), cloud#1232 / cloud#1239 (the measured cross-tenant -read of a credentials table through the NULL arm), cloud#1663 / cloud#1664 (the label-the-row repairs -and the control plane's wall). Every framework anchor below was re-verified against `origin/main` -`2514d49f3` while drafting; cloud anchors against `cloud` `main` `3856fbf7`. -**Consumers**: `@objectstack/spec` (`injected-system-columns.ts`, the org-scoping entitlement in -`tenancy-posture.ts`), `@objectstack/objectql` (`engine.ts` `buildDriverOptions`, -`tenancy/system-write-organization.ts`, `tenancy/platform-object-tenancy.ts`), every driver that -implements a tenant scope (`driver-sql` `applyTenantScope`, `driver-memory` and `driver-mongodb` -tenancy guards, `driver-turso`, `driver-sqlite-wasm`), `plugin-security` (`tenant-layer.ts`, -`per-organization-catalog.ts`, the five bootstrap seeders, `bootstrap-platform-admin.ts`), -`plugin-auth` (`ensure-default-organization.ts`), `@objectstack/core` -(`security/resolve-authz-context.ts`), `metadata-core` / `metadata-protocol` (the overlay), -`service-settings`, `plugin-email`, `plugin-sharing`, `plugin-audit`, `service-storage`, -`plugin-approvals`, `service-automation`, and — in their own repository — `cloud`'s control plane -(`control-plane-platform-global.ts`, `control-plane-org-scope-plugin.ts`, `migrations/org-id-backfill.ts`) -and `objectos-ee`. +[#13564](https://github.com/objectstack-ai/objectstack/issues/13564), verbatim and untranslated, +in the order the model was built: the premise 「我理解只有代码定义的元数据是跨租户的,对象、字段、视图等 +元数据,不落库;其他诸如角色、岗位、权限集 代码声明的会 seed 进库 … 但是应该是每个组织一套?」; the +principle 「或者说我们数据库中,不应该有允许 org_id 为空的状况。」; the rejection of per-tenant seeding +「既然你不让改,又要求每个租户 seed 一遍,好像很蠢,有没有更好的方案。」; the templates ruling +「在组织没有提出编辑诉求前,强制为每个组织 seed 也是很蠢。我宁可先不让他编辑。」; the withdrawal of +group-level template rows 「不考虑集团级模板行,作废相关需求」「不考虑 分层主数据」; the posture that +breaks every remaining tie: 「我们现在创业阶段,应该定一套最稳定可靠的方案,而不是盲目追求功能。」; and the +provenance rule for editing: 「可以先约定代码推送过来的元数据就只能在代码中修改。studio 界面上配置的元数据, +保存在库中,可以在界面上修改,包括 ../cloud build agent 构建的元数据,因为也是保存在库中,可以在界面上修改。」 +**Builds on**: [ADR-0005](./0005-metadata-customization-overlay.md) (the metadata overlay — its +environment layer is re-keyed by D6), [ADR-0049](./0049-no-unenforced-security-properties.md) +(enforce or remove — the posture of D13), [ADR-0066](./0066-unified-authorization-model.md) D2 +(`tenancy.enabled:false` — given the single meaning "no tenant column" by D1), +[ADR-0078](./0078-no-silently-inert-metadata.md) (declared-but-unread metadata is a defect — the +reason the seeders existed, answered differently by D2/D4), +[ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md) D5 (degraded tenancy fails fast) and its +Default Organization bootstrap (made load-bearing by D3), [ADR-0094](./0094-sys-permission-set-pure-projection.md) +D1 (the metadata layer is the only authoritative store for a permission-set definition — D2 +generalizes this to the whole declared catalog and stops materializing the projection), +[ADR-0095](./0095-authz-kernel-tenant-layer-and-posture-ladder.md) D1 (Layer 0), +[ADR-0105](./0105-group-tenancy-posture-and-first-class-org-scope.md) D2/D6/D10 (the `group` union +scope; the org-axis red lines; D10 withdrawn — D12), [ADR-0120](./0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md) +D3/D4/D7 (`COALESCE(organization_id, '__global__')`, the migration ceremony, the 17.x → protocol 18 +staging), [ADR-0123](./0123-no-active-organization-session-semantics.md) D2–D4 (no silent NULL +stamping — generalized by D9), [ADR-0129](./0129-object-name-is-the-canonical-id.md) D1 (the `name` +is the canonical id — extended by D4 to catalog items). +**Evidence**: the two censuses on #13564 (2026-08-31, 2026-09-02) and the 2026-09-03 cloud-side +supplement; [#10103](https://github.com/objectstack-ai/objectstack/issues/10103); +[#13491](https://github.com/objectstack-ai/objectstack/issues/13491); +[#2734](https://github.com/objectstack-ai/objectstack/issues/2734); +[#12699](https://github.com/objectstack-ai/objectstack/issues/12699); +[#14484](https://github.com/objectstack-ai/objectstack/issues/14484); +[#14547](https://github.com/objectstack-ai/objectstack/issues/14547); +[#13636](https://github.com/objectstack-ai/objectstack/issues/13636) and its draft PR #14923 (the +write-side diagnosis of the same root, and the mechanism this record supersedes — §1.6); cloud#1232 / +cloud#1239 / cloud#1663 / cloud#1664. Framework anchors re-verified against `origin/main` `2514d49f3`; +cloud anchors against `cloud` `main` `3856fbf7`. +**Consumers**: `@objectstack/spec` (`injected-system-columns.ts`, the org-scoping entitlement), +`@objectstack/objectql` (`engine.ts` `buildDriverOptions`, `tenancy/system-write-organization.ts`, +`tenancy/platform-object-tenancy.ts`), every driver with a tenant scope (`driver-sql` +`applyTenantScope` and the `driver-memory` / `driver-mongodb` / `driver-turso` / `driver-sqlite-wasm` +guards), `plugin-security` (`tenant-layer.ts`, `per-organization-catalog.ts`, the five bootstrap +seeders, `bootstrap-platform-admin.ts`, `permission-set-projection.ts`), `plugin-auth` +(`ensure-default-organization.ts`), `@objectstack/core` (`security/resolve-authz-context.ts`), +`plugin-email` (`bootstrap-declared-email-templates.ts`, `email-template-provenance.ts`, +`template-loader.ts`), `plugin-sharing`, `metadata-core` / `metadata-protocol`, `service-settings`, +`plugin-audit`, `service-storage`, `plugin-approvals`, `service-automation`, `objectui` (Setup / +Studio / pickers — D7), and in their own repository `cloud`'s control plane and `objectos-ee`. --- ## TL;DR -Today a NULL `organization_id` means two things at once: **"this row belongs to the platform"** and -**"whoever wrote this row forgot to say whose it is."** The database cannot tell them apart, so the -SQL driver's tenant predicate — `(organization_id = :tenant OR organization_id IS NULL)`, added by -#2734 so that seeded roles stay visible — hands a forgotten row to every tenant. cloud#1239 measured -exactly that on a credentials table. - -This record makes NULL mean **nothing**: - -- **D1** — `organization_id` is NOT NULL wherever it exists. An object whose rows have no tenant has - **no column**, not a nullable one. -- **D2** — Every deployment has one **platform organization**, created before any seeder runs, in - every posture. It is the owner of every deployment-level row. -- **D3** — The RBAC catalog is per-organization in every posture (`single` = one Default Organization). -- **D4** — Registries, templates, the settings global rung and the ADR-0005 environment layer are - owned by the platform organization. -- **D5** — Whether tenants may read platform-organization rows of an object is a **declaration**, per - object, per deployment — generalizing #12699. -- **D6** — Layer 0 and every driver consume **one** computed scope, so they cannot disagree (closes - #10103's cause 1). -- **D7** — A write without a resolvable organization is **refused**, in every posture. ⛔ Nothing ever - defaults to the platform organization. -- **D8** — Existing NULL rows get one of three fates — column dropped, platform-owned, or attributed to - their real organization — loudly, per table, with no reaping and no guessing. -- **D9–D11** — Postures differ only in enforcement; the compatibility arms are retired when the data - is clean; the constraint lands at protocol 18. -- **D12** — Under the `group` posture there are **no group-shared business rows**: ADR-0105 D10 - (group-level template rows) is withdrawn by the maintainer. The only shared owner is the platform - organization (D4/D5); cross-organization visibility is membership union; the org-axis red lines - stand. - -The two censuses on #13564 are not discarded by this record — they become its migration inventory. +Today a NULL `organization_id` means two things at once — **"this row belongs to the platform"** +and **"whoever wrote this row forgot to say whose it is"** — and the SQL driver's tenant predicate +`(organization_id = :tenant OR organization_id IS NULL)` (#2734) hands the second kind to every +tenant. cloud#1239 measured that on a credentials table. + +The obvious repair — give the platform's rows an owner — was drafted and rejected: it needed a +"platform organization", a concept that is an organization for the database and not for anyone +else. The maintainer's model is simpler and this record adopts it whole: + +1. **Metadata is the registry, and the registry has two provenances.** Code-declared metadata — + objects, fields, views, and equally positions, permission sets, capabilities, declared sharing + rules, email and notification templates — is edited **in code only** and is **never materialized + as rows**; there is nothing to seed, per tenant or otherwise. Metadata authored at runtime in + Studio or by the cloud build agent is **saved in the database and edited in the UI**; it is the + environment's, not any organization's, and its ledger carries **no organization column**. Both + provenances are cross-tenant by nature: every organization runs the same schema. +2. **A row with an organization column exists only when an organization authored it** — a position an + admin created in Setup, a set they cloned, an assignment they made. Every such row carries its + organization, NOT NULL. Registry items are referenced **by name**, the way ADR-0129 already treats + objects. +3. **A table with no organization column is deployment-level or code-level**, governed by permission, + not by a wall. There is no third shape. + +Consequences: NULL has nothing left to mean (D1); the driver's NULL arm and Layer 0's strict +equality stop disagreeing because there is one predicate (D8); a forgotten stamp is a refused write +(D9); the seeders, the per-organization catalog machinery and the #13491 ledger retire (D13); runtime +editing of declared templates stays **closed** until an organization asks, and opens — if ever — as +copy-on-write, never as a seed (D6). Startup posture throughout: the stable answer, not the +feature-complete one. --- @@ -95,106 +94,93 @@ The two censuses on #13564 are not discarded by this record — they become its ### 1.1 One column, two meanings -The SQL driver's read-side tenant chokepoint, `applyTenantScope` -(`packages/drivers/driver-sql/src/sql-driver.ts`), emits two arms with a NULL disjunct: the equality -arm `where(field, tenantId).orWhereNull(field)` and, under the `group` posture, the union arm -`whereIn(field, tenantIds).orWhereNull(field)`. Its own docblock states why: +`applyTenantScope` (`packages/drivers/driver-sql/src/sql-driver.ts`) emits two arms with a NULL +disjunct — the equality arm and, under `group`, the `whereIn` union arm — and states why: > a NULL tenant column marks a GLOBAL/platform row (bootstrap-seeded positions and permission sets, -> business units, pre-org first-boot seeds). Such a row belongs to no OTHER tenant, so the -> cross-tenant wall must not hide it: with strict equality every tenant admin saw ZERO RBAC rows on a -> fresh deployment, because every platform row is org-less (#2734). - -The rationale is true of the rows it names. It is also true of every row a writer forgot to stamp — -and the database holds both populations in the same NULL. cloud#1239 measured the consequence -end-to-end: an organization admin of tenant A read tenant B's `sys_environment_credential` rows -(`secret_ciphertext`, `encryption_key_id`) and `sys_package_installation` rows, because both objects -receive an injected `organization_id` that their raw-driver writers never populated, and the NULL arm -was the only wall standing on that deployment. - -### 1.2 What the censuses established (#13564, two rounds) - -1. **Whether the arm fires is a property of the caller, not the object.** `buildDriverOptions` - (`packages/objectql/src/engine.ts`) threads `execCtx.tenantId` for every object that is not - `tenancy.enabled:false` or federated. The dominant read shape in the platform namespace is a bare - `{ isSystem: true }` context — no `tenantId`, therefore **no scoping at all** (over 100 sites across - 30 objects). The arm's live consumers are the internal `{ isSystem: true, tenantId }` passes. -2. **On a walled deployment running `plugin-security`, tenants already do not see NULL rows.** - `computeTenantLayer0Filter` (`packages/plugins/plugin-security/src/tenant-layer.ts`) composes a - strict `organization_id = :tenant`; ANDed over the driver's arm, the conjunction is the strict - equality alone. This is #10103's symptom: on a real `isolated` deployment every principal listed - zero positions, permission sets and sharing rules while the tables held rows. -3. **The measured leak ran on a degraded posture.** cloud's control plane requested `isolated` without - mounting `@objectstack/organizations`; the posture resolved to `single`, Layer 0 was inert, and - the arm was the only wall. Today that precondition is a refused boot in the framework - ([ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md) D5, escape hatch - `OS_ALLOW_DEGRADED_TENANCY`), in `objectos-ee` (cloud#1020) and on the control plane (cloud#1664, - `supportedPostures = ['isolated']`). -4. **The arm has more load-bearing dependents than the driver comment names.** Beyond `sys_position`'s - authorization read (`resolve-authz-context.ts` §6a: organization-less rows "stay REACHABLE on - purpose") and `per-organization-catalog.ts`'s residue detection ("depends on that arm being - there"), the deployment-level platform-global declaration of #12699 disarms **only Layer 0**; the - engine still threads `tenantId`, so a tenant's read of a declared-global object sees the - deployment's NULL rows **through the arm**. cloud's control plane declares 32 such objects - (`control-plane-platform-global.ts`) and pins their tenant reads in - `unscoped-control-plane-tenant-wall.test.ts`. Neither census round could see this from inside - the framework repository. -5. **The read-side ledger placed all 59 tenant-column platform objects**: 9 load-bearing, 12 accidental, - 38 undetermined in four named sub-groups — and the undetermined sub-groups are undetermined because - NULL cannot say which meaning it carries. Two live producers surfaced while it was being written: - every `sys_record_share` row lands NULL (#14484); a seeded `sys_business_unit` at NULL silently - defeats sharing rules because a service open-coded the strict equality (#14547). -6. **The `group` posture has a reserved NULL of its own.** - [ADR-0105](./0105-group-tenancy-posture-and-first-class-org-scope.md) D1 defines `group` as - "organizations = membership boundaries over one shared dataset", walled by - `organization_id IN accessible_org_ids` (D2); D6 draws two lint-enforced red lines (no permission - inheritance along `parent_organization_id`; business-unit trees stay org-internal); D9 lets an - approver target `$root`, deriving the group root by walking the chain once - (`plugin-approvals/src/approver-org-scope.ts`). D10 — layered master data, the SAP - material-master / 用友-金蝶 distribution shape — is **reserved, not implemented**, and its reserved - wording is "group-level template rows (**platform-global** or group-org-owned, read-shared) plus - per-org override rows". The "platform-global" half is a NULL row. A record that retires NULL has - to resolve that reservation one way or the other; the maintainer resolved it by **withdrawing the - requirement** rather than by fixing an owner for it (D12). - -### 1.3 Two implementations of one predicate - -#10103 named the structural defect: the wall's predicate exists twice — governed in the driver -(`check-tenant-chokepoint` re-derives that every read door routes through it) and ungoverned in -`plugin-security` — and they disagree on NULL. Every option on #13564 ("keep the arm and add rules -above it", "narrow it to declared-global objects", "switch on posture") keeps the two implementations -and keeps NULL as a meaning; each therefore reproduces one of the two failures (#10103's blank catalog -or cloud#1239's leak) on some path. - -### 1.4 What already exists - -The record is small because most of its parts are built: - -- The open framework already bootstraps a **Default Organization** under `single` - (`packages/plugins/plugin-auth/src/ensure-default-organization.ts`, slug `default`, system context, - idempotent) — but on `kernel:ready`, **after** the seeders, which is why seeds land org-less - (measured 1.3 s ahead of the first `sys_organization` on a fresh walled rig). -- Under a walled posture the RBAC catalog is **already per-organization** (#10103 Option C, - `catalogIsPerOrganization(posture)` in `per-organization-catalog.ts`); the platform bucket there is - "meant to be unreadable through the tenant wall" (cloud's own words). -- A system write with no organization is **already derived** when exactly one organization exists and - **refused** on a walled posture (`resolveSystemInsertOrganization`, - `packages/objectql/src/tenancy/system-write-organization.ts`; #8844, #13491) — for objects the - #13491 ledger classified. ⛔ Its docblock already forbids silently defaulting to `__global__`. -- An object can already declare that it has **no tenant column** (`systemFields.tenant: false` / - `tenancy.enabled: false`, `packages/spec/src/data/injected-system-columns.ts`). -- A deployment can already declare objects **platform-global** for Layer 0 (#12699, - `deployment-org-scoping-entitlement.ts`), and cloud's control plane maintains such a list with a - written three-clause derivation rule. -- A named owner instead of NULL has precedent: the autonumber counter files org-less rows under the - `__global__` sentinel ([ADR-0120](./0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md) D3). -- Per-table backfills with a maintainer order each exist for `sys_file` - (`packages/services/service-storage/src/backfill-sys-file-organizations.ts`), the approval family - (`packages/plugins/plugin-approvals/src/backfill-platform-row-organizations.ts`) and, in cloud, six - raw-driver objects (`migrations/org-id-backfill.ts`, cloud#1663). - -What is missing is the principle that ties them together: **ownership is total**, so NULL has nothing -left to mean. +> business units, pre-org first-boot seeds) … with strict equality every tenant admin saw ZERO RBAC +> rows on a fresh deployment, because every platform row is org-less (#2734). + +True of the rows it names; equally true of every row a writer forgot to stamp. cloud#1239 measured an +organization admin of tenant A reading tenant B's `sys_environment_credential` rows through exactly +this arm, on a control plane whose requested `isolated` posture had degraded to `single`. + +### 1.2 What the censuses established + +1. Whether the arm fires is a property of the **caller**: the dominant platform read shape is a bare + `{ isSystem: true }` context, which scopes nothing at all; the arm's live consumers are internal + `{ isSystem: true, tenantId }` passes. +2. On a walled deployment running `plugin-security`, tenants already do not see NULL rows — Layer 0's + strict equality ANDs over the arm and wins (#10103's symptom, the other face of the same coin). +3. The measured leak's precondition — many organizations with Layer 0 inert — is today a refused boot + ([ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md) D5, cloud#1020, cloud#1664). +4. Beyond `sys_position`'s authorization read and the catalog's residue detection, the #12699 + deployment-level platform-global declaration depends on the arm: it disarms Layer 0 only, so a + tenant's read of a declared-global object sees the deployment's NULL rows *through the driver* + (cloud's control plane declares 32 such objects). Neither census could see this from the framework. +5. 59 platform objects carry the column; 38 could not be placed as "by design" or "accidental", + because NULL cannot say which it is. + +### 1.3 Why the seeders exist, and what they actually do + +The maintainer's first question was whether declared roles, positions and permission sets need to be +in the database at all. Read from the seeders' own docblocks: + +- **Declared items are seeded so the admin surface can see them** (Setup reads the table; + [ADR-0078](./0078-no-silently-inert-metadata.md) names the alternative "false compliance"), so + runtime resolution can join on rows (`resolveExecutionContext → sys_position → + sys_position_permission_set`), and so uninstall and provenance have an axis (`managed_by`, + `package_id`). +- **Seeded rows are read-only mirrors of code.** The machine name is immutable after creation; + package-managed rows refuse `update` and `delete` through the admin door ("change it by editing its + package and re-publishing"); platform-managed built-ins are "visible but not repurposable"; every + boot re-seeds package rows from the declaration. The maintainer's 2026-08-24 ruling — lock the base, + clone to customize — closed the last edit path. Customization is a **new org-owned row**, never an + edit of the mirror. +- **For permission sets the mirror is not even the authority**: [ADR-0094](./0094-sys-permission-set-pure-projection.md) + D1 makes the metadata layer the only authoritative store; the evaluator resolves registry-first; + the row is a derived read-model. +- **Under a walled posture the mirror is copied once per organization** (#10103 Option C), and under + `single` once with no owner — the NULL population this record is about. + +So the seed's purpose was never "so admins can modify them". Its three real purposes — visibility, +id-resolution, provenance — are each answerable without rows (D2, D4, D7). + +### 1.4 Templates are the one place editing is a requirement — and still not a reason to seed + +`sys_email_template` is record-authoritative today: declared and built-in templates are materialized +into rows on every boot (`seedTemplates`, `bootstrapDeclaredEmailTemplates`), an admin edit stamps +`customized: true` and the seeder skips that row thereafter, and `sendTemplate` resolves by +`(name, locale)` with **no organization in the key**. Two opposite doctrines therefore coexist for +"declared in code, editable at runtime": permission sets are metadata-authoritative with a projected +row; templates are row-authoritative with a seeded row. The maintainer's ruling settles both under +one rule: the requirement is real, the seed is not the way to meet it, and until an organization asks, +the door stays closed (D6). + +### 1.5 The rejected middle: a platform organization + +The first draft of this record gave deployment-level rows an owner — a reserved `sys_organization` +row with no members. It is recorded here as the alternative considered (§5) because it is the +natural repair and the wrong one: it keeps every mixed table mixed, adds a concept every organization +enumerator must special-case, and exists only to give NULL a new name. Under the maintainer's model +the mixed tables un-mix instead (D6, D7), and no owner is needed. + +### 1.6 The same root, diagnosed from the write side: #13636 and PR #14923 + +While #13491's tenant-audit control was being implemented, its seat found a **third** tenancy state +the two-state ruling could not express: objects holding both org-stamped and legitimately org-less +rows, where org-less is a property of the **row** (specimens: `sys_metadata`'s env-wide writes under +the #6190 ruling; `sys_audit_log`'s records about objects that have no organization column). #13636's +own root sentence is this record's: 「today the same `NULL` means both "deliberate" and "bug"」. Its +option B — **declare** a legitimately org-less write per call, loud and countable — is implemented in +draft PR #14923 (`orgless-write-declaration.ts`, ~1400 lines), paused by the maintainer. + +This record resolves the third state the other way round. A row that is legitimately org-less is a row +on an object that should have **no organization column**: `sys_metadata` becomes the tenant-less +environment ledger (D6/D7); `sys_audit_log` is a deployment ledger whose organization attribution is a +plain data field, not the tenancy anchor (D7). Once the column is gone there is nothing to declare, and +a declared-NULL mechanism would keep alive exactly the state D1 removes. #14923 therefore does not +merge; #13636 closes as superseded when this record merges (§8, C11). --- @@ -202,248 +188,245 @@ left to mean. ### D1 — NULL is not a state -Every object that carries `organization_id` carries it **NOT NULL** — at the DDL (emitted by the -schema sync once D8 has cleared the table, see D11) and at the engine (D7). There is no third shape: -an object whose rows belong to no organization declares `systemFields.tenant: false` (or -`tenancy.enabled: false`, [ADR-0066](./0066-unified-authorization-model.md) D2) and has **no column**; -an object that has the column has an owner on every row. - -This is the whole record in one sentence. Everything below exists to make D1 true without changing -what any tenant can see. - -### D2 — The platform organization - -Every deployment has exactly one **platform organization**: a `sys_organization` row with the reserved -slug `platform`, created by the kernel **before any seeder runs**, in every posture. Under `single`, -the Default Organization (`ensureDefaultOrganization`) is created at the same point, so no seeder ever -runs against an installation with zero organizations. - -The platform organization is an **owner of rows, not a container of principals**: - -- it has no members; it does not appear in membership-derived listings (better-auth's - `organization/list`), the org switcher, or the membership reconciler ([ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md) D2); -- `resolveSystemInsertOrganization`'s "exactly one organization ⇒ derive" counts **non-platform** - organizations only, so the Default Organization — not the platform one — is what a `single` - deployment derives; -- quota, billing and per-organization seeding sweeps (`listSeedOrganizationIds`) skip it. - -Platform-level standing stays config-derived (#13514 L4, `OS_PLATFORM_OWNER_EMAIL`); the grant rows -that express it today at NULL (the `single`-posture first-user `admin_full_access` row, Choice 4A) are -owned by the platform organization under D4. Whether platform standing may later be *modelled* as -membership in the platform organization is deferred (§6 Q2), not decided here. - -**The platform organization is not the group root.** On a `group` deployment the top of the -`parent_organization_id` tree — the headquarters — is an ordinary organization with members, quotas -and business data. The platform organization owns the **runtime's** rows (registries, templates, the -environment layer); every business row is owned by the organization it was written in (D12). The two -are operated by the same people on many deployments and are still two owners, because a deployment -may host more than one group and because the runtime's rows are not any group's business data. - -### D3 — The catalog is per-organization in every posture - -`catalogIsPerOrganization` becomes unconditionally true. The four catalog seeders -(`bootstrapBuiltinRoles`, `bootstrapDeclaredPositions`, `bootstrapDeclaredPermissions`, -`bootstrapDeclaredSharingRules`) and `bootstrapPlatformAdmin`'s `defaultPermissionSets` write **one -pass per organization**; under `single` that is one pass for the Default Organization. The -organization-less "platform bucket" of `sys_position` / `sys_permission_set` / -`sys_position_permission_set` ceases to be produced. `seedCtx()` without an organization becomes a -programming error rather than a `single`-posture branch. - -This is the maintainer's premise made structural: the walled posture already worked this way; `single` -is now the same code path with one organization. - -### D4 — Deployment-level rows are owned by the platform organization - -Rows that belong to the deployment and not to any tenant are stamped with the platform organization: - -- the [ADR-0005](./0005-metadata-customization-overlay.md) **environment layer** of `sys_metadata` - (every type that is not `allowOrgOverride`), `sys_metadata_activation` install-level rows, - environment-level `sys_view_definition` rows, and env-wide `sys_metadata_audit` / `_commit` / - `_history` rows — the overlay's resolution order (organization row → environment row → in-memory - registry) is unchanged; only the key of the middle layer changes from NULL to the platform - organization. **ADR-0005 is amended accordingly** (its "no overlay row = platform-global" sentence - about the registry is untouched); -- the platform's **capability registry** rows (`sys_capability`, `managed_by: 'platform'`); -- **seeded templates** (`sys_email_template`, `sys_notification_template`); -- the **settings global rung** (`sys_setting` `scope: 'global'`, and its audit rows); -- **platform-global sharing rules** (`sharing-rule-service.ts::criteriaContext`'s "owned by no - organization" class) and **global** `sys_user_position` assignments; -- the platform-standing grant rows of D2. - -The `__global__` sentinel ([ADR-0120](./0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md) D3) -resolves to the platform organization's id; the `COALESCE` becomes inert and is removed under D10. - -### D5 — Sharing is declared, per object, per deployment - -Whether a **tenant** principal may read the platform organization's rows of an object — alongside its -own — is a declaration, never an inference from the column's value. The existing deployment-level -declaration of #12699 (`platformGlobalObjects` on the org-scoping entitlement) is generalized to -`platformSharedObjects` with these semantics: - -- **listed**: tenant reads admit platform-organization rows in addition to the tenant's own; writes to - platform-organization rows require platform standing; -- **not listed**: platform-organization rows are invisible to tenants, exactly like any other - organization's rows. - -The framework ships a **default** list for its own platform objects whose platform rows are meant to be -shared in a tenant runtime (the D4 owners: capability registry, templates, the metadata environment -layer, the settings global rung); a deployment extends or shrinks it — because the same object is -per-organization on a tenant runtime and deployment-wide on a control plane (`sys_setting` is cloud's -own example), the declaration cannot be a static property of the object. The per-object authoring -channel keeps its D1 meaning ("no column") and is **not** the sharing declaration. - -Declaring an object shared is a widening, and stays a governed decision on the deployment's side — the -cloud control plane's three-clause derivation rule ("registered here; Layer 0 would otherwise wall it; -rows belong to the deployment, never to an organization — a question about **writers**") is adopted as -the written bar for adding a name. - -### D6 — One scope, computed once, consumed by every wall - -The engine computes one **tenant read scope** per operation — -`{ organizationIds: [...], sharedOrganizationIds: [...] }` — from the posture, the context -(`tenantId` under `isolated`, `accessible_org_ids` under `group`, [ADR-0105](./0105-group-tenancy-posture-and-first-class-org-scope.md) D2) -and the D5 declaration, and threads **that object** both to Layer 0 (`computeTenantLayer0Filter`) and to -the driver (`DriverOptions`). Both emit `organization_id IN (…organizationIds, …sharedOrganizationIds)`. -No layer re-derives the scope; no layer has a NULL arm. - -Two corrections fall out of this by construction: - -- #10103 cause 1 (two implementations disagreeing) cannot recur — there is one input; -- #12699's "declared global ⇒ Layer 0 stands down entirely" over-widening is replaced by "admit the - platform organization": a declared-shared object is still walled between tenants. - -Every driver that implements a tenant scope is held to the same predicate; `check-tenant-chokepoint` -is extended to refuse an `IS NULL` arm on a tenant column. - -### D7 — A missing stamp is a refused write, in every posture - -`resolveSystemInsertOrganization` generalizes [ADR-0123](./0123-no-active-organization-session-semantics.md) D3 -to every writer: a write on a tenant-column object that carries no organization is **derived** when -exactly one non-platform organization exists, and **refused** otherwise — under `single` as well as -under a wall — with a message naming what is missing ([ADR-0123](./0123-no-active-organization-session-semantics.md) D4). - -⛔ **Nothing ever defaults to the platform organization.** A writer that means the platform names it -explicitly (a `platformCtx()` sibling of `seedCtx()`); a writer that forgets gets a refusal, never a -shared row. Silently stamping the platform organization would rebuild the shared pile under a new -name — the one outcome this record exists to make impossible. - -The #13491 classification (`tenant-scoped` / `global` / `unclassified`) becomes structural: an object -with the column is in scope; an object without it is not; there is nothing left to be `unclassified` -about. The ledger `platform-object-tenancy.ts` is retired under D10 once D8 has run. - -### D8 — Migration: three fates, loud, per table, no reaping - -Existing NULL rows are resolved by **one generic migration family** driven by an inventory that the two -#13564 ledgers seed (59 platform objects + 28 example objects; the cloud supplement covers the -cloud-declared ones). Every object gets exactly one fate: - -1. **Column dropped** (D1 tenant-less objects — the read-side ledger's U-A plumbing group and every - object whose writer confirms "infrastructure rows, not tenant data"), through the - [ADR-0120](./0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md) D4 migration ceremony - and an [ADR-0087](./0087-metadata-protocol-upgrade-contract.md) registry entry. -2. **Platform organization** (D4 objects): `NULL → platform` in one statement per table. -3. **Real organization** (accidental rows): derived from a declared parent anchor - (`childKey` / `parentObject` / `parentOrgColumn`, the shape cloud's `org-id-backfill.ts` and this - repository's two backfills already share), per table, with the citation recorded in the inventory. - RBAC residue — pre-fix organization-less catalog rows that grants point at by id — is **adopted** - into per-organization copies with the grants remapped (cloud#1664 item 5's runbook made - first-class), superseding #10103's warn-not-reap posture now that the adopt path is defined. - -A row whose owner cannot be derived is **left NULL and reported**: the boot report lists, per table, -the count of unattributed rows and the remedy. ⛔ No row is deleted; no unattributed row is assigned to -the platform organization. The NOT NULL constraint (D1) is applied to a table **only when that table -reports zero NULL rows** — a table that still carries them stays red in the report and keeps its -nullable column until an operator resolves them. - -### D9 — Postures differ only in enforcement - -Ownership, seeding and constraints are identical across `single`, `group` and `isolated`. What differs -is whether the wall is enforced: under `single` Layer 0 contributes nothing (as today) and the driver -scopes to the Default Organization plus the shared set; under a wall both layers enforce D6's scope. -Degraded tenancy stays a refused boot ([ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md) D5). -`single` is no longer a reason for any row to be org-less. - -### D10 — Retirements (ADR-0049) - -When D8 reports zero NULL rows on every table of a deployment, the following are removed rather than -kept as dead compatibility: both `orWhereNull` arms in `applyTenantScope` and any sibling driver's -equivalent; the NULL readings in `resolve-authz-context.ts` (§4, §6, §6a, §6b), -`per-organization-catalog.ts` (`warnPreFixOrganizationLessRows`, residue detection), -`sharing-rule-service.ts::criteriaContext`, `settings-service.ts`'s global-rung special case, -`meta-write-org-scope.ts`'s NULL layer, `bootstrap-system-capabilities.ts`'s -`organization_id: null` identity predicate; the `__global__` sentinel and ADR-0120 D3's `COALESCE`; -the `platform-object-tenancy.ts` ledger and `isPlatformObjectOutOfTenantAuditScope`; #12699's -stand-down semantics (replaced by D5/D6). Each removal is an [ADR-0087](./0087-metadata-protocol-upgrade-contract.md) -entry where it retires an authorable shape. - -### D11 — Staging - -Phases that add — the platform organization (D2), the computed scope and declaration (D5/D6), the -seeders and writers (D3/D4/D7 in refusing mode behind the existing #13491 gate), the migration (D8) — -ship in **17.x**, with the driver's arms kept as the compatibility path. The NOT NULL constraint, the -every-posture refusal and the arm removal (D1/D7/D10) land at **protocol 18**, in the same staging -[ADR-0120](./0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md) D7 set for the -uniqueness work they compose with. Until D10 runs, ⛔ no card narrows or removes an arm. - -### D12 — The `group` posture: group-shared rows have an owner, and the org-axis red lines stand - -What this record already covers for `group` without further decision: - -- **The wall.** D6's `organizationIds` **is** `accessible_org_ids` - ([ADR-0105](./0105-group-tenancy-posture-and-first-class-org-scope.md) D2); the union predicate, - the fail-closed empty set and the active organization as write target are unchanged. -- **The catalog.** D3's per-organization seeding is what #10103 Option C already does under `group`, - and it is the only shape consistent with ADR-0105 D6 ①: a headquarters-authored position is **not** - inherited by a plant; a declared position reaches every organization from code, one copy each. -- **Cross-organization approvals** (ADR-0105 D9): the request row is owned by the plant; the `$root` - approver reads it through membership union or the system-context mirror. NOT NULL changes nothing. -- **The cut-line deployment** (ADR-0105 Appendix A: one organization, factories as business units): - the Default Organization owns everything under D3/D9. - -What this record decides for `group`: - -1. **D6's red lines are not amended by D5.** A sharing declaration names a fixed **owner set** for an - object; it is not an RLS policy, a sharing rule or a scope that reads `parent_organization_id` - per row, and it grants no permission — the reader still needs the object grant. Permission - inheritance along the org tree stays retired and lint-enforced - (`packages/lint/src/validate-org-axis-red-lines.ts`). This record adds **no** new sharing source: - `sharedOrganizationIds` holds the platform organization and nothing else. -2. **ADR-0105 D10 — layered master data (group template rows + per-org overrides) — is withdrawn.** - Maintainer ruling, 2026-09-04, verbatim and untranslated: 「不考虑集团级模板行,作废相关需求」. - No follow-up ADR will be drafted; group-level template rows are not a platform concept. This - record amends ADR-0105 by marking D10 withdrawn (the D10 paragraph carries the note). The - consequence for the `group` posture is that there is exactly **one** kind of cross-organization - visibility — membership union (ADR-0105 D2) — plus the runtime's own platform-organization rows on - declared-shared objects (D5). A group that wants headquarters-authored business data visible in a - plant expresses it the way the posture already provides: the readers are members of the - organization that owns the data. No tree-derived owner, no `group-root` source, no lint - allowlist. -3. **Migration.** Under this ruling a NULL business row on a `group` deployment is by definition not a - "group template": it takes D8 fate 3 (attributed to the organization its parent anchor names) or - fate 1 (column dropped where the object was never tenant data). Nothing is attributed to a group - root; nothing is attributed to the platform organization unless the object is a D4 owner. +Every object that carries `organization_id` carries it **NOT NULL** — at the DDL once D10 has cleared +the table, and at the engine from the start (D9). An object whose rows belong to no organization +declares `systemFields.tenant: false` (or `tenancy.enabled: false`, +[ADR-0066](./0066-unified-authorization-model.md) D2) and has **no column**. There is no nullable +tenant column anywhere. + +### D2 — Declared metadata is code and is never materialized as rows + +Everything a stack, package or the platform itself **declares** lives in the metadata registry and +only there: objects, fields, views, dashboards, reports, translations — and equally positions, +permission sets, the built-in identity roles (`platform_admin`, `org_owner`, `org_admin`, +`org_member`) and audience anchors (`everyone`, `guest`), capabilities, declared sharing rules, +declared and built-in email and notification templates, declared settings defaults. + +Declared items are cross-tenant by nature (every organization runs the same code), upgrade with the +code, and are immutable at runtime (the 2026-08-24 lock). **No seeder writes them into any table**, in +any posture, for any organization. Resolution reads the registry (D4). Setup and pickers read the +registry (D7). + +**The registry has a second provenance: environment metadata authored at runtime.** What a metadata +author creates in Studio, through the metadata door (ADR-0070), or through the cloud build agent — +objects, fields, views, flows, and equally positions, permission sets or templates created there — is +**saved in the database** (`sys_metadata`, the environment definition ledger) and **edited in the UI**. +It is hydrated into the same registry the code layer feeds (`loadMetaFromDb`), so everything below +that says "registry" means both provenances. The rule that separates them is provenance, verbatim: +「代码推送过来的元数据就只能在代码中修改;studio 界面上配置的元数据,保存在库中,可以在界面上修改」. +Environment metadata belongs to the deployment, not to an organization: its ledger carries **no +organization column** (D6/D7), and writing it requires the metadata-authoring capability +(`manage_metadata` / `studio.access`), which `organization_admin` deliberately does not hold. + +This retires: `bootstrapBuiltinRoles`, `bootstrapDeclaredPositions`, `bootstrapDeclaredPermissions`, +`bootstrapDeclaredSharingRules`, `bootstrapSystemCapabilities`, `bootstrapPlatformAdmin`'s +`defaultPermissionSets` materialization, `bootstrapDeclaredEmailTemplates`, `seedTemplates`, the +`sys_permission_set` projector and reconciler of [ADR-0094](./0094-sys-permission-set-pure-projection.md) +D2/D4 (D1 of that record stands and is generalized; its projection is no longer needed), and the whole +per-organization catalog machinery (`per-organization-catalog.ts`, #10103 Option C) — there is +nothing left to copy per organization. + +### D3 — A row exists only when an organization authored it + +The tables that hold catalog-shaped data (`sys_position`, `sys_permission_set`, +`sys_position_permission_set`, `sys_user_position`, `sys_user_permission_set`, `sys_capability`, +`sys_sharing_rule`, `sys_record_share`, `sys_email_template`, `sys_notification_template`, …) hold +**only what an organization wrote**: positions and sets an admin created (including clones of declared +ones), capabilities they defined, rules they authored, assignments and bindings they made, and — when +a door is open (D6) — overrides they saved. Every such row carries the authoring organization, NOT +NULL. + +Under `single` the authoring organization is the Default Organization +([ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md); `ensure-default-organization.ts`), which +becomes **load-bearing**: it exists before the first authenticated write (today it is created on +`kernel:ready` best-effort; a failure becomes a boot error). Because nothing is seeded, no boot-order +inversion is needed — the organization only has to exist before a person acts. + +An organization has no rows until it authors something. A fresh deployment with ten organizations +and no customization has an **empty** catalog table. + +### D4 — Declared items are referenced by name; resolution is registry-first + +[ADR-0129](./0129-object-name-is-the-canonical-id.md) made the object `name` the canonical id; this +record extends the principle to catalog items. A declared position or permission set has no row id, +so every reference to a catalog item — `sys_user_position`, `sys_position_permission_set`, sharing +recipients, grants — names it by its machine name, which is already unique per organization and, +for declared names, reserved across the deployment (an organization may not create an item whose +name a declaration holds; the uniqueness check spans registry and rows). + +Resolution is **registry first (both provenances — code-declared and environment-authored), then the +caller's organization's rows**. `resolve-authz-context.ts` already looks positions up by name +(`grants.positions`); the change is where it looks first. A name +that resolves nowhere — a declaration removed or renamed in code — **fails closed** for that reference +and is **reported at boot** per organization, by name. This is loud where today's behaviour is a +zombie: a seeded mirror of a removed declaration stays in the table and keeps granting. + +The registry and the organization's rows are **disjoint sets unioned by name**, not two definitions +of one item merged field by field. That distinction is what keeps the read-time-merge failures of the +overlay model (§5) out of this design. + +### D5 — Platform standing is configuration, or a Default-Organization row under `single` + +`PLATFORM_ADMIN` derives from `OS_PLATFORM_OWNER_EMAIL` (#13514 L4) — configuration, not a row. The +`single`-posture first-user promotion (Choice 4A), which today writes an `admin_full_access` grant row +with a NULL organization, writes it **owned by the Default Organization** instead; under a walled +posture no grant row is written (unchanged). `reportLegacyPlatformAdminGrant` and the legacy unscoped +anchor retire with D13. No NULL grant row is ever produced. + +### D6 — Editability follows provenance; the environment ledger has no organization column + +**Code-provenance metadata is edited in code.** No runtime door edits a declared item — not Setup, not +Studio, not the data API. The catalog's customization path is the 2026-08-24 one, **clone** (create an +org-owned row from a declaration, D3), and the environment's is **author** (create environment +metadata in Studio, below). + +**Environment-provenance metadata is edited in the UI.** What Studio or the cloud build agent wrote +into `sys_metadata` is theirs to change there, on every posture, by anyone holding the +metadata-authoring capability. This is [ADR-0005](./0005-metadata-customization-overlay.md)'s +environment layer, kept — and re-homed: **`sys_metadata` is the environment definition ledger and +carries no organization column** (D1/D7). It holds every type not `allowOrgOverride` and the +environment-level rows of the five overridable types. Per-organization overlays of those five +presentational types (`view`, `dashboard`, `report`, `translation`, `email_template`) are +organization-authored D3 rows and live in their **own** org-owned object, not beside environment rows +in a nullable column. ADR-0005 is amended accordingly; its "no overlay row = the registry" reading and +its overlay-wins resolution are unchanged. Under `single` the split is invisible in behaviour (one +environment, one organization); under a wall it is what makes environment metadata deployment-level +without an owner and organization overlays walled without a NULL arm. + +**Organization-level editing of declared templates stays closed.** Templates are the one class where +runtime editing by an *organization* is a stated requirement; the maintainer's startup posture is to +not open that door until an organization asks: `sendTemplate` resolves the registry (code-declared or +Studio-authored); no per-organization template row exists. If and when the door opens, it opens as +**copy-on-write**, never as a seed: the editor pre-fills the registry template; *Save* writes one row +owned by that organization; resolution becomes `(organization, name, locale)` with the org row winning +**whole** (no field-level merge); *Reset to default* deletes the row; a registry template removed +leaves an orphan override that the boot report names. The same shape serves any other class that later +earns an organization-level door. + +### D7 — Deployment-level state has no organization column; the UI never merges sources server-side + +Rows that belong to the deployment and not to an organization live in objects **without** the column +(D1): the environment metadata ledger and its family (`sys_metadata`, `sys_metadata_audit`, +`sys_metadata_commit`, `sys_metadata_history`, `sys_metadata_activation`, environment-level +`sys_view_definition` — D6), operational plumbing (`sys_job`, `sys_job_run`, `sys_job_queue`, +`sys_flow_dispatch`, `sys_http_delivery`, `sys_migration`, `sys_migration_journal`, and the +recipient-anchored notification/inbox family where `user_id` is the anchor), the audit ledger +(`sys_audit_log`, whose rows may concern deployment-level actions — the organization an audit row is +*about* becomes a plain attribution field under a name the tenant-field resolver does not claim, never +the tenancy anchor; cloud's `tenant_id` rule already reads it this way), and deployment-level runtime +settings — `sys_setting`'s global rung leaves the tenant-scoped table for configuration or a +tenant-less object. Such objects are governed by object permission, not by the wall. + +The #12699 deployment declaration becomes **total**: an object a deployment declares platform-global +gets **no organization column on that deployment** (the injected-columns plan reads the declaration), +so Layer 0 and the driver agree by having nothing to scope. This is how the cloud control plane +expresses that its own settings, metadata and jobs are deployment-level while the same objects are +per-organization on a tenant runtime. + +**Presentation** (the lesson of the maintainer's prior platform, where a merged list of in-memory and +database metadata forced search, sort and paging to be re-implemented in application code): no surface +promises **one merged, server-paged list** of declared and organization-authored items. Studio lists +declared items from the registry; Setup lists the organization's rows with the data API's native +search, sort and paging, plus a *Clone from package* action that reads the registry; pickers +(assignments, bindings, recipients) union the two **small** sets client-side. Declared catalogs are +tens of items; nothing about them needs a server. + +### D8 — One predicate, computed once + +The engine computes the tenant read scope from posture and context — `organization_id = :tenant` +under `isolated`, `IN accessible_org_ids` under `group` +([ADR-0105](./0105-group-tenancy-posture-and-first-class-org-scope.md) D2), nothing under `single` — +and threads the **same** value to Layer 0 (`computeTenantLayer0Filter`) and to every driver. No layer +has a NULL arm; no layer has a shared-owner set (there are no shared owners — D2/D7 removed the need). +#10103 cause 1 cannot recur: one input, one predicate. `check-tenant-chokepoint` refuses an +`IS NULL` arm on a tenant column. + +### D9 — A missing stamp is a refused write, in every posture + +`resolveSystemInsertOrganization` generalizes [ADR-0123](./0123-no-active-organization-session-semantics.md) +D3 to every writer: a write on a tenant-column object with no organization is **derived** under +`single` (the Default Organization is the only one) and **refused** otherwise, with a message naming +what is missing (ADR-0123 D4). ⛔ Nothing defaults to any owner other than the derivable one. The +#13491 classification (`tenant-scoped` / `global` / `unclassified`) becomes structural — column ⇒ in +scope — and its ledger retires (D13). + +### D10 — Migration: four fates, loud, per table; mirrors are the one sanctioned deletion + +Driven by the inventory the two censuses seed (59 platform + 28 example objects; cloud's supplement +for cloud-declared ones). Every object gets one fate; every row in it follows: + +1. **Column dropped** — D7 objects (through the [ADR-0120](./0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md) + D4 ceremony and an [ADR-0087](./0087-metadata-protocol-upgrade-contract.md) registry entry). +2. **Mirror deleted** — rows that are re-derivable from code and nothing else: seeded catalog copies + (per-organization and NULL residue alike), seeded templates, seeded capabilities. They are deleted + **after** every reference to them has been rewritten from row id to name (D4) and the rewrite has + been verified to resolve. ⚠️ This is the one deletion this record sanctions, and the reason it is + safe is the reason it is allowed: a mirror carries no information the code does not. +3. **Attributed** — organization-authored and accidental rows take their real organization from a + parent anchor (`childKey` / `parentObject` / `parentOrgColumn`, the shape + `backfill-sys-file-organizations.ts`, `plugin-approvals`' `backfill-platform-row-organizations.ts` + and cloud's `org-id-backfill.ts` share); under `single`, the Default Organization. +4. **Reported** — a row whose owner cannot be derived is left NULL and listed at boot, per table, with + counts and the remedy. ⛔ Never guessed, never deleted. The NOT NULL constraint lands on a table + **only when it reports zero NULL rows**. + +Template rows an admin has customized (`customized: true`) are the one population the maintainer must +rule on (§6 Q1): keep them readable as the Default Organization's overrides (half of the D6 door), or +accept the loss under the startup posture with a release note. + +### D11 — Postures differ only in enforcement + +Ownership rules, refusals and constraints are identical under `single`, `group` and `isolated`; only +whether the wall is enforced differs. Degraded tenancy stays a refused boot. `single` is never a +reason for a row to be org-less. + +### D12 — The `group` posture: no group-shared business rows; the org-axis red lines stand + +[ADR-0105](./0105-group-tenancy-posture-and-first-class-org-scope.md) D10 — layered master data +(group template rows shared down an organization tree) — is **withdrawn** by the maintainer (verbatim: +「不考虑集团级模板行,作废相关需求」「不考虑 分层主数据」); ADR-0105 carries the note. Under `group` there +is exactly one kind of cross-organization visibility, membership union (D2 of that record); D6's red +lines (no permission inheritance along `parent_organization_id`; business-unit trees stay +org-internal) are untouched, and this record adds no sharing source of any kind. The catalog needs no +per-organization copies under `group` either — D2 applies; a headquarters-authored position is an +ordinary org-owned row of the headquarters. + +### D13 — Retirements (ADR-0049) + +When D10 reports zero NULL rows on every table: both `orWhereNull` arms and any sibling driver's +equivalent; the seeders and projector of D2; `per-organization-catalog.ts` including +`warnPreFixOrganizationLessRows`; the NULL readings in `resolve-authz-context.ts` (§4, §6, §6a, §6b), +`sharing-rule-service.ts::criteriaContext`, `settings-service.ts`'s global rung, +`meta-write-org-scope.ts`'s NULL layer, `bootstrap-system-capabilities.ts`; the email-template seed +and `email-template-provenance.ts`; the `__global__` sentinel and ADR-0120 D3's `COALESCE`; +`platform-object-tenancy.ts` and `isPlatformObjectOutOfTenantAuditScope`; #12699's stand-down +semantics (replaced by D7's no-column). Each retirement of an authorable shape is an +[ADR-0087](./0087-metadata-protocol-upgrade-contract.md) entry. + +### D14 — Staging + +**17.x, additive**: the Default Organization made load-bearing (D3); registry-first resolution and +name-keyed references added beside the id columns (D4); refusals behind the existing #13491 gate (D9); +D7's no-column declarations; the D10 inventory and attribution migration; the template door closed +(D6). The driver arms stay as the compatibility path. **Protocol 18**: NOT NULL, every-posture +refusal, mirror deletion, arm removal, retirements (D1/D9/D10/D13), in the staging +[ADR-0120](./0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md) D7 set. Until D13 runs, +⛔ no card narrows or removes an arm. --- ## 3. Non-goals -- **Changing what tenants can see.** Platform rows that are shared today remain shared where the - deployment declares them so (D5); the record changes the *key* under which they are owned and the - *mechanism* by which sharing is expressed, not the visible result set. -- **Moving code-declared metadata into or out of the database.** Object/field/view metadata declared - in code stays in the in-memory registry (ADR-0005's base layer); the RBAC catalog's persistence - (Setup editability, provenance, grants by id — ADR-0078, ADR-0086 D5) is not revisited here. Whether - declared catalog rows *should* be persisted at all is a separate question the maintainer's premise - raises and this record does not answer. -- **better-auth-managed tables.** They have no tenant column (`managedBy: 'better-auth'` suppresses - injection) and are untouched. -- **The platform organization as a principal container.** No membership, no login, no SSO binding - (deferred, §6 Q2). -- **Reaping.** No data row is deleted by any part of this record (D8). -- **Group-level template rows.** ADR-0105 D10 is **withdrawn**, not deferred (D12); this record - adds no mechanism for headquarters-authored rows shared down an organization tree. -- **cloud's data model beyond ownership.** Scope rules anchored on `environment_id` / `invoice_id` - (cloud#1239, cloud#1255) stay; only the injected `organization_id`'s owner changes. +- **Changing what a tenant can see or do.** Declared items remain visible to every organization (they + are code); organizations keep creating their own items and cloning declared ones. Only the + mechanism moves. +- **Opening any editing door.** D6 closes template editing and adds no new customization surface; + copy-on-write is the *shape* a future door takes, not a commitment to build one. +- **A platform organization** in any form (§1.5, §5). +- **Group-level shared business rows** (D12; ADR-0105 D10 withdrawn, not deferred). +- **better-auth-managed tables** — no tenant column; untouched. +- **Reaping data.** The only deletion is D10 fate 2, mirrors re-derivable from code. +- **cloud's environment- and invoice-anchored scope rules** — they stay; only ownership changes. --- @@ -451,44 +434,46 @@ What this record decides for `group`: **What becomes true.** -- A forgotten stamp is a failed write (D7) and, once D1 lands, a constraint violation — it can no longer - land in a pile every tenant reads. The class cloud#1239 belongs to is closed structurally, not by - per-table repairs after each measurement. -- "Is this platform row visible to tenants?" has one answer in one place (D5), consumed by both walls - (D6). #10103's blank catalog and cloud#1239's leak are the same disagreement; D6 removes the second - implementation. -- The #13564 undetermined groups dissolve: an object is either tenant-less (no column), platform-owned, - or tenant-owned. The read-side ledger's 38 rows each acquire one of three fates in D8's inventory. -- ADR-0120 D3's NULL-safe uniqueness simplifies to plain per-organization uniqueness. -- #14547's class ("a service open-coded the strict equality and lost the seeded rows") cannot recur: - seeded units belong to an organization (D3/D7) and there is no NULL arm to forget. +- A forgotten stamp is a refused write (D9) and then a constraint violation (D1); the cloud#1239 + class is closed structurally. +- Nothing is seeded, per tenant or otherwise; a fresh deployment's catalog tables are empty; upgrades + need no reconciliation pass and leave no residue; a removed declaration fails loudly instead of + granting from a zombie mirror (D4). +- The two doctrines of §1.4 collapse into one (registry-authoritative, rows only when authored). +- One predicate in one place (D8); ADR-0120 D3's NULL-safe uniqueness becomes plain per-organization + uniqueness; #14547's class (a service open-coding the strict equality and losing seeded rows) has + nothing left to lose. +- #13564's three options are all moot: the arm is retired with its reason, not narrowed. **What it costs.** -- Boot ordering changes (D2): two bootstraps move ahead of every seeder; the plugin ordering is - declared ([ADR-0116](./0116-plugin-ordering-declared-contract.md)), not incidental. -- Every platform writer is touched once (D3/D4/D7) — the read-side ledger enumerates them; the - dominant shape (bare `SYSTEM_CTX`) does not need a tenant on reads, but every **write** site names - its owner. -- Tests that pin NULL semantics are rewritten to pin the new predicate (`deal_p1`, - `sql-driver-tenant-scope`, `memory-tenancy-guard`, `rule-criteria-org-scope`, - `per-organization-catalog`, cloud's `unscoped-control-plane-tenant-wall`). -- A live migration on customer databases (D8), staged per table and gated per table; unattributable - rows are an operator task, surfaced loudly, never resolved by the platform guessing. -- `sys_organization` gains a reserved row; any admin surface that lists *all* organizations (not - membership-derived) must mark or hide it (verified per surface; objectui card if one exists). -- Under `group`, the driver's `IN` list grows by one id (the platform organization) on declared-shared - objects; index shape is unchanged. Nothing else about the `group` posture moves (D12). +- **Reference columns move from id to name** (D4): `sys_user_position`, `sys_position_permission_set`, + sharing recipients, grants — a schema migration with an id→name rewrite over existing rows, and the + verified rewrite is the precondition of D10 fate 2. +- **Resolution changes at the measured sites** — the read-side ledger counted them: three objects with + tenant-threaded reads, plus the store helpers — from row lookup to registry-first lookup. +- **Setup, Studio and pickers change** (D7): Setup drops the declared rows it used to list, gains + *Clone from package*; pickers read two sources. No server-side merge is built. +- **Organization-level template editing is unavailable** until an organization asks (D6); existing + customized template rows need a ruling (§6 Q1). Studio-authored templates remain editable in Studio. +- **Deployment-level settings leave `sys_setting`** (D7): configuration or a tenant-less object. +- **The metadata ledger splits** (D6): `sys_metadata` loses its organization column and becomes the + environment definition ledger; per-organization overlays of the five presentational types move to + an org-owned object. This touches the layered read in `metadata-protocol` (`getMetaItem`), + `meta-write-org-scope.ts`, the ADR-0094 write-through, the ADR-0126 activation ledger, and + `sys-metadata-repository.ts` — the largest single item in the plan, and the reason C5 is its own card. +- Tests pinning NULL semantics and seeded rows are rewritten (`deal_p1`, `sql-driver-tenant-scope`, + `memory-tenancy-guard`, `rule-criteria-org-scope`, `per-organization-catalog`, + `bootstrap-declared-email-templates`, cloud's `unscoped-control-plane-tenant-wall`). **Risks.** -- A writer "helpfully" stamping the platform organization to make a refusal go away. D7 forbids it in - terms; review and the chokepoint gate look for it; the boot report of D8 would show the shared pile - reforming under the platform id. -- A declared-shared list that grows by convenience. D5 adopts cloud's written three-clause bar and - keeps the list a governed deployment decision. -- A `single` deployment whose Default Organization bootstrap fails (it is best-effort today). D2 makes - it load-bearing: a failure is a boot error, not a degraded warning. +- A writer defaulting a missing organization to *something* to silence a refusal. D9 forbids it; the + boot report shows the pile reforming. +- A name reference outliving its declaration. D4 fails closed and reports; the report is the control. +- The Default Organization bootstrap failing under `single`. D3 makes it a boot error. +- A surface quietly re-introducing a merged, paged list of two sources. D7 names the pattern and + refuses it; the objectui card carries the rule. --- @@ -496,50 +481,50 @@ What this record decides for `group`: | Alternative | Why not | |---|---| -| **(a) Keep the arm; add per-object scope rules above the driver** (cloud#1239's pattern; #13564's option a) | Keeps NULL's double meaning. Every new writer inherits fail-open; every repair is per table after a measurement. It is the status quo the maintainer rejected: 「不应该有允许 org_id 为空的状况」. | -| **(b) Narrow the arm to declared-global objects** | "Global" is per deployment, not per object (`sys_setting`), so the driver — constructed from connection config with no kernel access — cannot know it; and #12699's list would still ride the arm. It moves the fail-open behind a list without removing it. | -| **(c) Switch the arm on posture** | Keyed on effective posture it would have kept the arm on cloud#1239's degraded deployment (the leak); keyed on requested posture it blanks every #12699 object and revokes pre-fix RBAC grants silently. Either way NULL keeps a meaning under `single`. | -| **NOT NULL without a platform organization** (per-organization copies of everything) | Impossible for the shared-schema environment layer (ADR-0005: object/field overlays cannot be per-organization in a shared DB) and for the capability registry; it would also multiply every seeded template per tenant. | -| **A string sentinel (`__global__`) instead of a real organization row** | No FK to `sys_organization`, so lookups, RLS and admin surfaces treat it as a special case forever; ADR-0120 D3 already shows the COALESCE this costs. A real row lets every layer treat the platform like any other owner. | -| **Serve the declared catalog from the registry and persist nothing** | Answers the maintainer's first question differently and may be right — but it changes Setup editability, provenance and grants-by-id (ADR-0078, ADR-0086 D5) and is independent of NULL: persisted rows would still need an owner. Recorded as a follow-up question, not folded in (§6 Q3). | +| **Keep the arm; per-object scope rules above the driver** (#13564 option a) | Keeps NULL's double meaning; every new writer inherits fail-open; repairs are per table after a measurement. Rejected by the maintainer in terms. | +| **Narrow the arm to declared-global objects / switch on posture** (options b, c) | Keeps two predicates and NULL's meaning under `single`; blanks #12699 objects or revokes pre-fix grants silently; would have kept the arm on cloud#1239's degraded posture. | +| **A platform organization owning deployment-level rows** (this record's first draft) | Gives NULL a new name instead of removing it; keeps every mixed table mixed; adds an organization every enumerator special-cases; puts business editing behind platform standing. The maintainer's model removes the need. | +| **Per-organization seeded read-only mirrors of the declared catalog** (today under a wall; Salesforce-style managed components) | Copies immutable code into N tables to be read back; needs boot reconciliation, residue guards and provenance columns; is exactly the "既然不让改,又每个租户 seed 一遍" the maintainer rejected. | +| **Read-time overlay of code and database metadata for the same item** (the maintainer's prior platform; ADR-0005's mechanism) | Field-level precedence ambiguity, silent drift on upgrade, no way to delete a code item from the database, merged lists that re-implement search/sort/paging. D4 avoids it by keeping the sets disjoint and unioned by name; D7 avoids it by never merging lists server-side. | +| **A string sentinel (`__global__`) as owner** | Breaks the FK to `sys_organization`; every layer special-cases it forever (ADR-0120 D3's COALESCE is the running cost). | +| **Declare a legitimately org-less write per call** (#13636 option B, draft PR #14923) | Makes NULL a *declared* state instead of removing it; every future writer of a conditionally-scoped object must know to declare; the column stays nullable, so the constraint D1 wants can never land. Superseded by taking the column off the objects whose rows are legitimately org-less (§1.6). | --- ## 6. Open questions for the maintainer's merge decision -1. **Identity of the platform organization.** Slug `platform` and a fixed display name are proposed; - should it be visible (marked) or hidden in platform-admin organization lists? -2. **Platform standing as membership.** D2 keeps standing config-derived. Should a later record allow - "member of the platform organization" to *be* platform standing (replacing the legacy grant anchor)? - Deferred here. -3. **Persisting the declared catalog at all.** The premise 「理论上不需要写到数据库中」 is not decided - by this record (§3). Should a follow-up card measure what Setup, provenance and grants-by-id would - lose if declared roles/positions/permission sets were served from the registry and only - runtime-created ones persisted? -4. **cloud's recipient-anchored group** (`sys_inbox_message`, `sys_notification*`, - `sys_user_preference`): D1 (no column, anchored on `user_id`) or D4 (platform-owned, shared)? cloud's - own reading ("their inbox follows them, not their active workspace") points at D1. -5. **Staging confirmation.** D11 aligns with ADR-0120 D7 (17.x additive, protocol 18 constraint). - Confirm, or name a different boundary. +1. **Existing customized template rows** (D10): keep readable as the Default Organization's overrides + (builds the read half of the D6 door now), or accept the loss under the startup posture with a + release note? Depends on whether any deployment relies on the feature — this record cannot see that. +2. **`single`-posture first-user promotion** (D5): grant row owned by the Default Organization + (proposed), or configuration only (`OS_PLATFORM_OWNER_EMAIL` becomes mandatory for self-hosters)? +3. **Deployment-level settings** (D7): configuration file/environment, or a tenant-less + `sys_platform_setting` object? Proposed: configuration for infrastructure values, a tenant-less + object only for values an operator must change without a restart. +4. **Staging** (D14): confirm the 17.x / protocol 18 boundary aligned with ADR-0120 D7. +5. **The metadata ledger split** (D6): one object losing its column plus a new org-owned overlay + object is the shape this record proposes; the alternative — keep one `sys_metadata` and make the + five overridable types the only rows with an organization — cannot satisfy D1 (a nullable column). + Confirm the split, or rule that per-organization overlays are retired for now (startup posture), + which would make `sys_metadata` tenant-less with no second object. --- -## 7. Verification notes — where citations did not survive re-verification +## 7. Verification notes - The issue body of #13564 cites the arm at `sql-driver.ts:7320`; on `origin/main` `2514d49f3` the - arms are inside `applyTenantScope` (~line 12016–12066) and there are **two** of them. This record - cites the symbol, not the line. + arms are inside `applyTenantScope` (~12016–12066) and there are **two**. This record cites symbols. - `ensure-default-organization.ts` cites "ADR-0081 D1" for the Default Organization; in this - repository `docs/adr/0081` is the trusted React page tier. The framework record that describes the - bootstrap is [ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md) (§D6/D7 compose with it); - the "0081" is cloud's numbering. This record cites the module by path. -- The read-side ledger stated the arm's live tenant-facing surface is empty on a walled deployment - running `plugin-security`. Re-verified — with the addition in §1.2 item 4: the #12699 deployment - declaration is a tenant-facing dependent the ledger could not see, because it lives in cloud. -- `driver-memory` and `driver-mongodb` carry tenancy **guards** (`memory-tenancy-guard.ts`, - `mongodb-tenancy-guard.ts`) keyed on `tenancy.enabled === true`, not a NULL-armed predicate. D6 holds - them to the same computed scope; the audit of each driver's actual predicate is a card, not an - assumption recorded here. + repository `docs/adr/0081` is the trusted React page tier — the "0081" is cloud's numbering. The + framework record is [ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md). +- `resolve-authz-context.ts` already resolves positions **by name** (`{ name: { $in: grants.positions } }` + against `sys_position`); D4 changes where that lookup goes first, not what it is keyed on. +- `template-loader.ts` resolves by `(name, locale)`; a grep for `organization` / `tenant` in it returns + nothing — templates are deployment-wide today, which is what makes "close the door" behaviour-neutral + for every organization that never edited one. +- `driver-memory` and `driver-mongodb` carry tenancy **guards** keyed on `tenancy.enabled === true`, + not a NULL-armed predicate; D8 holds every driver to the one computed predicate, and each driver's + actual behaviour is measured by its card, not assumed here. --- @@ -549,18 +534,22 @@ One epic tracks the family. Phases 1–3 are additive (17.x); phase 4 is protoco | # | Card | Decisions | Blocked by | |---|---|---|---| -| C1 | Platform organization exists before the first seeder, in every posture | D2, D9 | ADR merge | -| C2 | One tenant read scope; `platformSharedObjects` threads to Layer 0 and every driver | D5, D6 | ADR merge | -| C3 | Catalog per-organization in every posture; registry, platform grants, authz reads on the platform organization | D3, D4 | C1, C2 | -| C4 | ADR-0005 environment layer owned by the platform organization | D4 | C1 | -| C5 | Every remaining platform writer names its owner — platform organization or no column; U-A/U-B adjudicated | D1, D4, D7 | C1 | -| C6 | Generic NULL-organization backfill: three fates, per-table boot report | D8 | C3, C4, C5 | -| C7 | NOT NULL; unstamped writes refused in every posture; both arms retired — protocol 18 | D1, D7, D10, D11 | C6 | -| C8 | cloud: control plane and objectos-ee adopt the record (cloud repository) | D1, D4, D5, D8 | C2, then C7 | -| C9 | Docs and card-family close-out (#13564, #11611, #10103 residue posture) | — | C7, C8 | - -⛔ Rules every card inherits: no card touches the driver's arms before C7; no writer ever defaults to -the platform organization; no row is reaped; `content/docs/releases/` is never edited in a code PR. +| C1 | Default Organization load-bearing under `single`; `resolveSystemInsertOrganization` derives it, refuses elsewhere | D3, D9, D11 | ADR merge | +| C2 | Registry-first catalog resolution; references by name (id→name columns + rewrite); dangling-name boot report; cross-source uniqueness | D2, D4 | ADR merge | +| C3 | Retire the seeders and the per-organization catalog machinery; built-ins and audience anchors as declared metadata; platform-admin grant row owned by the Default Organization | D2, D5, D13 | C1, C2 | +| C4 | Templates: `sendTemplate` resolves the registry; seed and provenance stamp retired; door closed; customized-rows ruling applied | D6, D10 | ADR merge (+ §6 Q1) | +| C5 | Metadata ledger split: `sys_metadata` family tenant-less (environment definitions, UI-editable by metadata authors); per-organization overlays of the five presentational types in an org-owned object; ADR-0005 amended | D6, D7 | C1 | +| C6 | Deployment-level state has no column: settings global rung leaves `sys_setting`; plumbing objects drop the column; #12699 declaration made total | D7 | ADR merge (+ §6 Q3) | +| C7 | Inventory + migration: four fates, id→name rewrite verified before mirror deletion, per-table boot report | D10 | C2, C3, C4, C5, C6 | +| C8 | One predicate; NOT NULL per cleared table; every-posture refusal; both arms and the ledger retired — protocol 18 | D1, D8, D9, D13, D14 | C7 | +| C9 | objectui: Setup lists org rows + *Clone from package*; Studio lists declared; pickers dual-source client-side; no merged server list | D7 | C2 | +| C10 | cloud: control plane and objectos-ee adopt the record (per-deployment no-column, backfill, tests) | D7, D8, D10 | C6, then C8 | +| C11 | Docs and card-family close-out (#13564, #11611, #10103 posture; #13636 superseded, PR #14923 closed unmerged) | — | C8, C10 | + +⛔ Rules every card inherits: no card touches the driver's arms before C8; no writer ever defaults a +missing organization to anything but the derivable Default Organization; no deletion except D10 +fate 2 after a verified rewrite; no surface builds a merged server-paged list of two sources; +`content/docs/releases/` is never edited in a code PR. --- @@ -574,36 +563,41 @@ the platform organization; no row is reaped; `content/docs/releases/` is never e [#14484](https://github.com/objectstack-ai/objectstack/issues/14484) · [#14547](https://github.com/objectstack-ai/objectstack/issues/14547) · [#11611](https://github.com/objectstack-ai/objectstack/issues/11611) · + [#13636](https://github.com/objectstack-ai/objectstack/issues/13636) (PR #14923) · + [#6190](https://github.com/objectstack-ai/objectstack/issues/6190) · [#8844](https://github.com/objectstack-ai/objectstack/issues/8844) · - [#13514](https://github.com/objectstack-ai/objectstack/issues/13514) · cloud#1020 · cloud#1232 · - cloud#1239 · cloud#1255 · cloud#1663 · cloud#1664. + [#13514](https://github.com/objectstack-ai/objectstack/issues/13514) · + [#4509](https://github.com/objectstack-ai/objectstack/issues/4509) · cloud#1020 · cloud#1232 · + cloud#1239 · cloud#1257 · cloud#1663 · cloud#1664. - Records: [ADR-0005](./0005-metadata-customization-overlay.md) · [ADR-0049](./0049-no-unenforced-security-properties.md) · [ADR-0066](./0066-unified-authorization-model.md) · - [ADR-0078](./0078-no-silently-inert-metadata.md) · [ADR-0086](./0086-authz-metadata-config-boundary-and-cross-package-composition.md) · - [ADR-0087](./0087-metadata-protocol-upgrade-contract.md) · [ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md) · + [ADR-0078](./0078-no-silently-inert-metadata.md) · [ADR-0087](./0087-metadata-protocol-upgrade-contract.md) · + [ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md) · [ADR-0094](./0094-sys-permission-set-pure-projection.md) · [ADR-0095](./0095-authz-kernel-tenant-layer-and-posture-ladder.md) · [ADR-0105](./0105-group-tenancy-posture-and-first-class-org-scope.md) · - [ADR-0116](./0116-plugin-ordering-declared-contract.md) · [ADR-0120](./0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md) · - [ADR-0123](./0123-no-active-organization-session-semantics.md). + [ADR-0123](./0123-no-active-organization-session-semantics.md) · [ADR-0129](./0129-object-name-is-the-canonical-id.md). - Code (framework, `origin/main` `2514d49f3`): `packages/drivers/driver-sql/src/sql-driver.ts` - (`applyTenantScope`, `injectTenantOnInsert`, `auditMissingTenant`); - `packages/objectql/src/engine.ts` (`buildDriverOptions`); + (`applyTenantScope`); `packages/objectql/src/engine.ts` (`buildDriverOptions`); `packages/objectql/src/tenancy/system-write-organization.ts`; `packages/objectql/src/tenancy/platform-object-tenancy.ts`; - `packages/plugins/plugin-security/src/tenant-layer.ts` (`computeTenantLayer0Filter`); + `packages/plugins/plugin-security/src/tenant-layer.ts`; `packages/plugins/plugin-security/src/per-organization-catalog.ts`; - `packages/plugins/plugin-security/src/bootstrap-platform-admin.ts`; + `packages/plugins/plugin-security/src/bootstrap-declared-permissions.ts`; + `packages/plugins/plugin-security/src/bootstrap-declared-positions.ts`; + `packages/plugins/plugin-security/src/bootstrap-builtin-positions.ts`; `packages/plugins/plugin-security/src/bootstrap-system-capabilities.ts`; - `packages/plugins/plugin-security/src/deployment-org-scoping-entitlement.ts`; + `packages/plugins/plugin-security/src/bootstrap-platform-admin.ts`; + `packages/plugins/plugin-security/src/permission-set-projection.ts`; + `packages/plugins/plugin-security/src/packaged-permission-set-lock.ts`; `packages/plugins/plugin-auth/src/ensure-default-organization.ts`; `packages/core/src/security/resolve-authz-context.ts`; + `packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts`; + `packages/plugins/plugin-email/src/email-template-provenance.ts`; + `packages/plugins/plugin-email/src/template-loader.ts`; `packages/metadata-core/src/meta-write-org-scope.ts`; - `packages/metadata-protocol/src/protocol.ts` (`getMetaItem`); `packages/spec/src/data/injected-system-columns.ts`; `scripts/check-tenant-chokepoint.mjs`. - Code (cloud, `main` `3856fbf7`): `packages/service-cloud/src/control-plane-platform-global.ts`; `packages/service-cloud/src/control-plane-org-scope-plugin.ts`; - `packages/service-cloud/src/control-plane-organizations.ts`; `packages/service-cloud/src/migrations/org-id-backfill.ts`; - `apps/cloud/test/unscoped-control-plane-tenant-wall.test.ts`; - `apps/objectos-ee/objectstack.config.ts`. + `apps/cloud/test/unscoped-control-plane-tenant-wall.test.ts`. From 6f767f6c03c5e1ffa93f5b9f1e22e73ce8b7ec1b Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Fri, 4 Sep 2026 01:07:31 +0800 Subject: [PATCH 05/24] =?UTF-8?q?docs(adr):=20ADR-0131=20D6=20=E2=80=94=20?= =?UTF-8?q?map=20ADR-0126's=20three=20customization=20regimes=20onto=20the?= =?UTF-8?q?=20two=20provenances;=20activation=20ledger=20has=20no=20reserv?= =?UTF-8?q?ed=20column?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maintainer 2026-09-04: 「代码中定义的元数据运行时不可修改,要修改就软件包重新发版本;不想用的可以停用;想改的可以复制 一个重新改」. Restates ADR-0126 O/C/E against code vs Studio provenance; amends ADR-0126 D3's 'org column reserved, written NULL' (a nullable column is the shape D1 forbids); adds §6 Q6 on a disable switch for views/dashboards. Refs #13564 Co-Authored-By: Claude Fable 5.1 --- ...ation-ownership-no-null-organization-id.md | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md index 7e05b0b092..b2e8853c66 100644 --- a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md +++ b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md @@ -29,8 +29,11 @@ generalizes this to the whole declared catalog and stops materializing the proje scope; the org-axis red lines; D10 withdrawn — D12), [ADR-0120](./0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md) D3/D4/D7 (`COALESCE(organization_id, '__global__')`, the migration ceremony, the 17.x → protocol 18 staging), [ADR-0123](./0123-no-active-organization-session-semantics.md) D2–D4 (no silent NULL -stamping — generalized by D9), [ADR-0129](./0129-object-name-is-the-canonical-id.md) D1 (the `name` -is the canonical id — extended by D4 to catalog items). +stamping — generalized by D9), [ADR-0126](./0126-packaged-metadata-customization-model.md) D1–D3 +(the three customization regimes for packaged metadata — overlay, disable + clone, extend — and the +activation ledger; D6 maps them onto this record's provenances and amends D3's reserved column), +[ADR-0129](./0129-object-name-is-the-canonical-id.md) D1 (the `name` is the canonical id — extended by +D4 to catalog items). **Evidence**: the two censuses on #13564 (2026-08-31, 2026-09-02) and the 2026-09-03 cloud-side supplement; [#10103](https://github.com/objectstack-ai/objectstack/issues/10103); [#13491](https://github.com/objectstack-ai/objectstack/issues/13491); @@ -293,6 +296,27 @@ its overlay-wins resolution are unchanged. Under `single` the split is invisible environment, one organization); under a wall it is what makes environment metadata deployment-level without an owner and organization overlays walled without a NULL arm. +**How code-declared metadata is customized without being edited** is not new: it is +[ADR-0126](./0126-packaged-metadata-customization-model.md)'s three regimes, first landed for packaged +flows, restated here against the two provenances so no card re-derives them. The maintainer's phrasing +of the requirement — 「代码中定义的元数据运行时不可修改,要修改就软件包重新发版本;不想用的可以停用;想改的可以复制一个 +重新改」 — is the model exactly: + +| Regime (ADR-0126 §3) | What the user does | What is written, and by whom | Under this record | +|:--|:--|:--|:--| +| **O — overlay** (`view`, `dashboard`, `report`, `translation`, `email_template`) | copy a shipped view or dashboard and change the copy | a **whole-item** overlay row keyed by the same name — organization-authored (Setup, org admin) or environment-authored (Studio, metadata author) | the org row is a D3 row in the overlay object; the environment row is Studio-provenance in the environment ledger (D6). Overlay wins whole; the code item is untouched | +| **C — disable + clone** (`flow`, `permission`, `action`; pre-charted `tool`, `skill`, `position`) | switch a shipped flow off; or clone it as a sibling under a **new name** and change the clone | disable = one `active` bit in the activation ledger, **no clone required**; clone = a whole-definition copy with no linkage to the base (ADR-0126 amendment ruling 2) | the ledger row is environment-level and the ledger carries **no organization column** (D7 — this amends ADR-0126 D3's "org column reserved, written NULL": a reserved nullable column is the shape D1 forbids; per-organization activation, when a customer asks, is a **separate org-owned object**, refused per trigger type exactly as ADR-0126 D3 pre-charts). The clone is Studio-provenance (environment ledger) or a D3 org row, by who authored it | +| **E — extend** (`object`, `app`) | add fields, validations, navigation | a package | unchanged; packages are code | +| **outside / unassigned** | — | — | unchanged (ADR-0126 §3 lists them so silence reads as decided) | + +Two consequences the table makes explicit. **A disable switch for a view or dashboard does not exist +today** — Regime O has overlay but no `active` bit; "I do not want this dashboard" is expressed by +overlaying it or by not binding it in navigation (Regime E). Admitting `view` / `dashboard` into the +disable half is a one-row change to ADR-0126 §3 that the generic ledger already supports; under the +startup posture it waits for a measured pull (§6 Q6). And **no regime edits the code item**: the base +stays what the package shipped, upgrades reach it untouched, and every customization is either a shadow +(O), a switch (C-disable), a sibling (C-clone) or a package (E). + **Organization-level editing of declared templates stays closed.** Templates are the one class where runtime editing by an *organization* is a stated requirement; the maintainer's startup posture is to not open that door until an organization asks: `sendTemplate` resolves the registry (code-declared or @@ -507,6 +531,10 @@ refusal, mirror deletion, arm removal, retirements (D1/D9/D10/D13), in the stagi five overridable types the only rows with an organization — cannot satisfy D1 (a nullable column). Confirm the split, or rule that per-organization overlays are retired for now (startup posture), which would make `sys_metadata` tenant-less with no second object. +6. **A disable switch for views and dashboards** (D6, ADR-0126 §3): Regime O has no `active` bit + today. Admit `view` / `dashboard` into the disable half now (one row in ADR-0126 §3; the ledger is + generic), or wait for a measured pull? Proposed: wait — overlay and navigation already express + "not wanted" without a new switch. --- @@ -538,7 +566,7 @@ One epic tracks the family. Phases 1–3 are additive (17.x); phase 4 is protoco | C2 | Registry-first catalog resolution; references by name (id→name columns + rewrite); dangling-name boot report; cross-source uniqueness | D2, D4 | ADR merge | | C3 | Retire the seeders and the per-organization catalog machinery; built-ins and audience anchors as declared metadata; platform-admin grant row owned by the Default Organization | D2, D5, D13 | C1, C2 | | C4 | Templates: `sendTemplate` resolves the registry; seed and provenance stamp retired; door closed; customized-rows ruling applied | D6, D10 | ADR merge (+ §6 Q1) | -| C5 | Metadata ledger split: `sys_metadata` family tenant-less (environment definitions, UI-editable by metadata authors); per-organization overlays of the five presentational types in an org-owned object; ADR-0005 amended | D6, D7 | C1 | +| C5 | Metadata ledger split: `sys_metadata` family tenant-less (environment definitions, UI-editable by metadata authors); per-organization overlays of the five presentational types in an org-owned object; `sys_metadata_activation` without the reserved column; ADR-0005 and ADR-0126 D3 amended | D6, D7 | C1 | | C6 | Deployment-level state has no column: settings global rung leaves `sys_setting`; plumbing objects drop the column; #12699 declaration made total | D7 | ADR merge (+ §6 Q3) | | C7 | Inventory + migration: four fates, id→name rewrite verified before mirror deletion, per-table boot report | D10 | C2, C3, C4, C5, C6 | | C8 | One predicate; NOT NULL per cleared table; every-posture refusal; both arms and the ledger retired — protocol 18 | D1, D8, D9, D13, D14 | C7 | @@ -576,7 +604,8 @@ fate 2 after a verified rewrite; no surface builds a merged server-paged list of [ADR-0095](./0095-authz-kernel-tenant-layer-and-posture-ladder.md) · [ADR-0105](./0105-group-tenancy-posture-and-first-class-org-scope.md) · [ADR-0120](./0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md) · - [ADR-0123](./0123-no-active-organization-session-semantics.md) · [ADR-0129](./0129-object-name-is-the-canonical-id.md). + [ADR-0123](./0123-no-active-organization-session-semantics.md) · + [ADR-0126](./0126-packaged-metadata-customization-model.md) · [ADR-0129](./0129-object-name-is-the-canonical-id.md). - Code (framework, `origin/main` `2514d49f3`): `packages/drivers/driver-sql/src/sql-driver.ts` (`applyTenantScope`); `packages/objectql/src/engine.ts` (`buildDriverOptions`); `packages/objectql/src/tenancy/system-write-organization.ts`; From 47db4e9f716323dca2a8a5c9591864661c45b52c Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Fri, 4 Sep 2026 01:16:42 +0800 Subject: [PATCH 06/24] =?UTF-8?q?docs(adr):=20ADR-0131=20D6=20=E2=80=94=20?= =?UTF-8?q?managed=20packages=20are=20sealed,=20template=20packages=20are?= =?UTF-8?q?=20copied=20in=20and=20editable,=20refused=20on=20shared-DB=20m?= =?UTF-8?q?ulti-tenant;=20ADR-0005=20overlay=20axis=20and=20ADR-0126=20O/C?= =?UTF-8?q?=20regimes=20paused?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maintainer 2026-09-04, verbatim: 「你这么说还不如先完全封死。flow 也先不让改。然后软件包应该有两种安装方式,有一种是 模版形式直接进库,那就是所有都可以修改。但是单库多租户禁止安装这种模版软件包;有一种是受管软件包,什么都以软件包中 的为准,就是不让改。」 sys_metadata needs no split (no per-organization metadata remains); C12 template install mode added; ADR-0126 and ADR-0005 carry amendment notes. Refs #13564 Co-Authored-By: Claude Fable 5.1 --- .../0005-metadata-customization-overlay.md | 2 +- ...6-packaged-metadata-customization-model.md | 2 + ...ation-ownership-no-null-organization-id.md | 181 ++++++++++-------- 3 files changed, 105 insertions(+), 80 deletions(-) diff --git a/docs/adr/0005-metadata-customization-overlay.md b/docs/adr/0005-metadata-customization-overlay.md index 77ac9fa7c3..9276a9dd6f 100644 --- a/docs/adr/0005-metadata-customization-overlay.md +++ b/docs/adr/0005-metadata-customization-overlay.md @@ -3,7 +3,7 @@ > **v5.0 update (2026):** Throughout this document, the term *project* has been renamed to *environment* (no aliases; CLI flags, URL paths, schemas, env vars all hard-renamed). See [ADR-0006 v4 — the v5.0 rename and its no-alias decision](./0006-project-environment-split.v4.md#the-v50-rename-and-its-no-alias-decision) for the rationale. The body below is preserved verbatim for historical context. -**Status**: Accepted (2026-05-16) · **Amended** (2026-05-22, see "Amendment: post-ADR-0006 v4 scope") · **Amended** (2026-04-13, branch concept removed — see [ADR-0008 §0](./0008-metadata-repository-and-change-log.md#0-2026-04-13-amendment--drop-project-and-branch-from-metaref)) · **Amended** (2026-08-09, #6825 — the Phase-1 overlay-index migration is deleted; see "Amendment (2026-08-09, #6825): overlay-index delivery after the Phase-1 migration was deleted") +**Status**: Accepted (2026-05-16) · **Amended** (2026-05-22, see "Amendment: post-ADR-0006 v4 scope") · **Amended** (2026-04-13, branch concept removed — see [ADR-0008 §0](./0008-metadata-repository-and-change-log.md#0-2026-04-13-amendment--drop-project-and-branch-from-metaref)) · **Amended** (2026-08-09, #6825 — the Phase-1 overlay-index migration is deleted; see "Amendment (2026-08-09, #6825): overlay-index delivery after the Phase-1 migration was deleted") · **Amended** (2026-09-04, [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D6 — the per-organization overlay axis is **retired for now**: `sys_metadata` carries no organization column, org-scoped writes of the five tier-A types are refused, and the environment layer is the whole ledger; the environment → code layered resolution and the "no overlay row = the registry" reading are unchanged) **Deciders**: ObjectStack Protocol Architects **Builds on**: [ADR-0003](./0003-package-as-first-class-citizen.md) (Package as first-class citizen), [ADR-0004](./0004-cloud-multi-kernel.md) (Cloud + per-project kernels) **Amended by**: [ADR-0006 v4](./0006-project-environment-split.v4.md) (drops `sys_project` entirely), [ADR-0008](./0008-metadata-repository-and-change-log.md) (re-expresses overlay as `LayeredRepository`; subsequently drops `project`/`branch` from `MetaRef`), [ADR-0029 D9](./0029-kernel-object-ownership-and-platform-objects-decomposition.md#amendment-2026-08-09-6853-a-tenant-overlay-of-an-object-is-its-own-contributor-layer-not-a-second-own) (for `object` only: the overlay is a registry contributor LAYER over the packaged owner, resolved as `base = overlay ?? own`, instead of a destructive in-place overwrite) diff --git a/docs/adr/0126-packaged-metadata-customization-model.md b/docs/adr/0126-packaged-metadata-customization-model.md index 9a593c8467..b488410c14 100644 --- a/docs/adr/0126-packaged-metadata-customization-model.md +++ b/docs/adr/0126-packaged-metadata-customization-model.md @@ -40,6 +40,8 @@ re-cited inline; the survey carries the instruments and positive controls. --- +> **Amended 2026-09-04 by [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D6.** Maintainer ruling, verbatim and untranslated: 「你这么说还不如先完全封死。flow 也先不让改。然后软件包应该有两种安装方式,有一种是模版形式直接进库,那就是所有都可以修改。但是单库多租户禁止安装这种模版软件包;有一种是受管软件包,什么都以软件包中的为准,就是不让改。」 Regimes **O** (overlay) and **C** (disable + clone) are **paused for managed content — packaged flows (§7) included**; customization is expressed by the **install mode** instead: *managed* (sealed, upgradeable; the only mode a shared-DB multi-tenant deployment accepts) or *template* (copied once into the environment ledger, fully editable, no upgrade channel, refused on `group` / `isolated`). D3's reserved `organization_id` on `sys_metadata_activation` is withdrawn (ADR-0131 D1: no nullable tenant column); the ledger has no writer while the door is sealed and is removed at protocol 18 unless a measured pull returns. Regime **E** stands. §7 remains the contract the regimes consume **if** they return. + ## TL;DR The platform ships packaged apps and refuses nearly every post-install change to them, while two diff --git a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md index b2e8853c66..eb0f3a1cca 100644 --- a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md +++ b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md @@ -13,7 +13,10 @@ principle 「或者说我们数据库中,不应该有允许 org_id 为空的 group-level template rows 「不考虑集团级模板行,作废相关需求」「不考虑 分层主数据」; the posture that breaks every remaining tie: 「我们现在创业阶段,应该定一套最稳定可靠的方案,而不是盲目追求功能。」; and the provenance rule for editing: 「可以先约定代码推送过来的元数据就只能在代码中修改。studio 界面上配置的元数据, -保存在库中,可以在界面上修改,包括 ../cloud build agent 构建的元数据,因为也是保存在库中,可以在界面上修改。」 +保存在库中,可以在界面上修改,包括 ../cloud build agent 构建的元数据,因为也是保存在库中,可以在界面上修改。」; +and the sealing ruling that fixes D6: 「你这么说还不如先完全封死。flow 也先不让改。然后软件包应该有两种安装方式, +有一种是模版形式直接进库,那就是所有都可以修改。但是单库多租户禁止安装这种模版软件包;有一种是受管软件包,什么都以 +软件包中的为准,就是不让改。」 **Builds on**: [ADR-0005](./0005-metadata-customization-overlay.md) (the metadata overlay — its environment layer is re-keyed by D6), [ADR-0049](./0049-no-unenforced-security-properties.md) (enforce or remove — the posture of D13), [ADR-0066](./0066-unified-authorization-model.md) D2 @@ -70,13 +73,15 @@ The obvious repair — give the platform's rows an owner — was drafted and rej "platform organization", a concept that is an organization for the database and not for anyone else. The maintainer's model is simpler and this record adopts it whole: -1. **Metadata is the registry, and the registry has two provenances.** Code-declared metadata — - objects, fields, views, and equally positions, permission sets, capabilities, declared sharing - rules, email and notification templates — is edited **in code only** and is **never materialized - as rows**; there is nothing to seed, per tenant or otherwise. Metadata authored at runtime in - Studio or by the cloud build agent is **saved in the database and edited in the UI**; it is the - environment's, not any organization's, and its ledger carries **no organization column**. Both - provenances are cross-tenant by nature: every organization runs the same schema. +1. **Metadata is the registry, and the registry has two provenances.** A **managed** package — + objects, fields, views, and equally positions, permission sets, capabilities, sharing rules, + templates, flows — is code: edited **in code only**, changed by publishing a new version, **never + materialized as rows**, and **sealed** at runtime (no overlay, no disable, no clone). Metadata + authored at runtime — in Studio, by the cloud build agent, or by installing a **template** package + (a one-time copy into the database) — is **saved in the database and edited in the UI**; it is the + environment's, not any organization's, and its ledger carries **no organization column**. Template + packages are **refused on shared-DB multi-tenant deployments**. Both provenances are cross-tenant + by nature: every organization runs the same schema. 2. **A row with an organization column exists only when an organization authored it** — a position an admin created in Setup, a set they cloned, an assignment they made. Every such row carries its organization, NOT NULL. Registry items are referenced **by name**, the way ADR-0129 already treats @@ -86,10 +91,10 @@ else. The maintainer's model is simpler and this record adopts it whole: Consequences: NULL has nothing left to mean (D1); the driver's NULL arm and Layer 0's strict equality stop disagreeing because there is one predicate (D8); a forgotten stamp is a refused write -(D9); the seeders, the per-organization catalog machinery and the #13491 ledger retire (D13); runtime -editing of declared templates stays **closed** until an organization asks, and opens — if ever — as -copy-on-write, never as a seed (D6). Startup posture throughout: the stable answer, not the -feature-complete one. +(D9); the seeders, the per-organization catalog machinery and the #13491 ledger retire (D13); +customization of managed content is **sealed** — ADR-0126's overlay and disable + clone regimes are +paused, and ADR-0005's per-organization overlay axis is retired, so `sys_metadata` needs no split (D6). +Startup posture throughout: the stable answer, not the feature-complete one. --- @@ -221,6 +226,10 @@ Environment metadata belongs to the deployment, not to an organization: its ledg organization column** (D6/D7), and writing it requires the metadata-authoring capability (`manage_metadata` / `studio.access`), which `organization_admin` deliberately does not hold. +A package reaches a deployment in one of **two install modes** (D6): **managed** — registered as code, +sealed, upgradeable; or **template** — copied once into the environment ledger as environment +metadata, fully editable, no upgrade channel, refused on shared-DB multi-tenant postures. + This retires: `bootstrapBuiltinRoles`, `bootstrapDeclaredPositions`, `bootstrapDeclaredPermissions`, `bootstrapDeclaredSharingRules`, `bootstrapSystemCapabilities`, `bootstrapPlatformAdmin`'s `defaultPermissionSets` materialization, `bootstrapDeclaredEmailTemplates`, `seedTemplates`, the @@ -276,56 +285,61 @@ with a NULL organization, writes it **owned by the Default Organization** instea posture no grant row is written (unchanged). `reportLegacyPlatformAdminGrant` and the legacy unscoped anchor retire with D13. No NULL grant row is ever produced. -### D6 — Editability follows provenance; the environment ledger has no organization column - -**Code-provenance metadata is edited in code.** No runtime door edits a declared item — not Setup, not -Studio, not the data API. The catalog's customization path is the 2026-08-24 one, **clone** (create an -org-owned row from a declaration, D3), and the environment's is **author** (create environment -metadata in Studio, below). - -**Environment-provenance metadata is edited in the UI.** What Studio or the cloud build agent wrote -into `sys_metadata` is theirs to change there, on every posture, by anyone holding the +### D6 — Managed is sealed; template is copied in and fully editable; the environment ledger has no organization column + +**Two install modes, and the mode is the whole customization story.** + +- **Managed** (default). The package is registered into the registry as code. Nothing in it is + editable at runtime — not in Setup, not in Studio, not through the data or metadata API — and + nothing in it can be switched off or cloned-with-linkage either: [ADR-0126](./0126-packaged-metadata-customization-model.md)'s + Regime O (overlay) and Regime C (disable + clone) are **paused for managed content, packaged flows + included** (verbatim: 「先完全封死。flow 也先不让改」). To change a managed item, the vendor publishes a + new version and upgrades flow to the base untouched. Regime E (extend) stands, because an extension + is itself a package. Managed is the **only** mode a shared-DB multi-tenant deployment (`group` / + `isolated`) accepts. +- **Template.** The package's metadata is copied **once** into the environment definition ledger as + environment-provenance items — from then on it is the environment's own metadata, editable in Studio + like anything authored there, with no upgrade channel (a later version is a new import, refused where + names collide; provenance is recorded for information only). ⛔ **Refused on `group` / `isolated`** + with a message naming the posture: a template import would hand every tenant an editable shared + schema. Under `single` it is how a customer takes an app and makes it theirs. + +Creating one's own item — a new permission set, position, view, flow — in Setup (organization-owned, +D3) or Studio (environment-owned) is ordinary authoring, not customization of a managed item; +pre-filling the form from a managed item is a UI convenience that records no linkage. + +**Code-provenance metadata is edited in code.** No runtime door edits a managed item. + +**Environment-provenance metadata is edited in the UI.** What Studio, the cloud build agent or a +template install wrote into `sys_metadata` is the environment's to change there, by anyone holding the metadata-authoring capability. This is [ADR-0005](./0005-metadata-customization-overlay.md)'s -environment layer, kept — and re-homed: **`sys_metadata` is the environment definition ledger and -carries no organization column** (D1/D7). It holds every type not `allowOrgOverride` and the -environment-level rows of the five overridable types. Per-organization overlays of those five -presentational types (`view`, `dashboard`, `report`, `translation`, `email_template`) are -organization-authored D3 rows and live in their **own** org-owned object, not beside environment rows -in a nullable column. ADR-0005 is amended accordingly; its "no overlay row = the registry" reading and -its overlay-wins resolution are unchanged. Under `single` the split is invisible in behaviour (one -environment, one organization); under a wall it is what makes environment metadata deployment-level -without an owner and organization overlays walled without a NULL arm. - -**How code-declared metadata is customized without being edited** is not new: it is -[ADR-0126](./0126-packaged-metadata-customization-model.md)'s three regimes, first landed for packaged -flows, restated here against the two provenances so no card re-derives them. The maintainer's phrasing -of the requirement — 「代码中定义的元数据运行时不可修改,要修改就软件包重新发版本;不想用的可以停用;想改的可以复制一个 -重新改」 — is the model exactly: - -| Regime (ADR-0126 §3) | What the user does | What is written, and by whom | Under this record | -|:--|:--|:--|:--| -| **O — overlay** (`view`, `dashboard`, `report`, `translation`, `email_template`) | copy a shipped view or dashboard and change the copy | a **whole-item** overlay row keyed by the same name — organization-authored (Setup, org admin) or environment-authored (Studio, metadata author) | the org row is a D3 row in the overlay object; the environment row is Studio-provenance in the environment ledger (D6). Overlay wins whole; the code item is untouched | -| **C — disable + clone** (`flow`, `permission`, `action`; pre-charted `tool`, `skill`, `position`) | switch a shipped flow off; or clone it as a sibling under a **new name** and change the clone | disable = one `active` bit in the activation ledger, **no clone required**; clone = a whole-definition copy with no linkage to the base (ADR-0126 amendment ruling 2) | the ledger row is environment-level and the ledger carries **no organization column** (D7 — this amends ADR-0126 D3's "org column reserved, written NULL": a reserved nullable column is the shape D1 forbids; per-organization activation, when a customer asks, is a **separate org-owned object**, refused per trigger type exactly as ADR-0126 D3 pre-charts). The clone is Studio-provenance (environment ledger) or a D3 org row, by who authored it | -| **E — extend** (`object`, `app`) | add fields, validations, navigation | a package | unchanged; packages are code | -| **outside / unassigned** | — | — | unchanged (ADR-0126 §3 lists them so silence reads as decided) | - -Two consequences the table makes explicit. **A disable switch for a view or dashboard does not exist -today** — Regime O has overlay but no `active` bit; "I do not want this dashboard" is expressed by -overlaying it or by not binding it in navigation (Regime E). Admitting `view` / `dashboard` into the -disable half is a one-row change to ADR-0126 §3 that the generic ledger already supports; under the -startup posture it waits for a measured pull (§6 Q6). And **no regime edits the code item**: the base -stays what the package shipped, upgrades reach it untouched, and every customization is either a shadow -(O), a switch (C-disable), a sibling (C-clone) or a package (E). - -**Organization-level editing of declared templates stays closed.** Templates are the one class where -runtime editing by an *organization* is a stated requirement; the maintainer's startup posture is to -not open that door until an organization asks: `sendTemplate` resolves the registry (code-declared or -Studio-authored); no per-organization template row exists. If and when the door opens, it opens as -**copy-on-write**, never as a seed: the editor pre-fills the registry template; *Save* writes one row -owned by that organization; resolution becomes `(organization, name, locale)` with the org row winning -**whole** (no field-level merge); *Reset to default* deletes the row; a registry template removed -leaves an orphan override that the boot report names. The same shape serves any other class that later -earns an organization-level door. +environment layer, kept — and made the **whole** ledger: **`sys_metadata` is the environment definition +ledger and carries no organization column** (D1/D7). + +**ADR-0005's per-organization overlay axis is retired for now.** The five `allowOrgOverride` types +(`view`, `dashboard`, `report`, `translation`, `email_template`) were the only reason `sys_metadata` +carried an organization. With managed content sealed and template content environment-owned there is +no per-organization metadata left to hold: under `single` the organization *is* the environment; under +a wall no metadata is editable at all. So there is **no split and no second object** — org-scoped +metadata writes are refused (the existing identity pin flips from "exactly five types accepted" to +"none"), and the environment layer is the ledger. ADR-0005 is amended (status note); its "no overlay +row = the registry" reading and its layered resolution (environment → code) are unchanged. If a +measured pull for per-organization presentational overlays returns, it comes back as an org-owned +overlay object holding D3 rows — never as a nullable column on the environment ledger. + +**What this does to ADR-0126.** Its three regimes were the answer to "how is packaged metadata +customized without being edited"; the maintainer's answer is now "it is not — pick the install mode". +Regime O and Regime C are paused for managed content; the activation ledger (`sys_metadata_activation`, +first consumer packaged flows, #11665) has no writer while the door is sealed and is removed at protocol +18 unless a measured pull returns (D13, [ADR-0049](./0049-no-unenforced-security-properties.md)); +ADR-0126 D3's "org column reserved, written NULL" on that ledger is withdrawn — a reserved nullable +tenant column is the shape D1 forbids. Regime E stands. ADR-0126 carries the amendment note. + +**Templates** follow the same two modes: a managed template renders from the registry and is immutable; +a Studio-authored or template-installed template is environment metadata, editable in Studio. There is +no per-organization template override (the door the maintainer chose not to open: 「我宁可先不让他编辑」). +If a measured organization-level pull ever returns, it opens as **copy-on-write** — one org-owned row, +whole-item, `(organization, name, locale)`, *Reset to default* deletes it — never as a seed. ### D7 — Deployment-level state has no organization column; the UI never merges sources server-side @@ -424,7 +438,9 @@ equivalent; the seeders and projector of D2; `per-organization-catalog.ts` inclu `meta-write-org-scope.ts`'s NULL layer, `bootstrap-system-capabilities.ts`; the email-template seed and `email-template-provenance.ts`; the `__global__` sentinel and ADR-0120 D3's `COALESCE`; `platform-object-tenancy.ts` and `isPlatformObjectOutOfTenantAuditScope`; #12699's stand-down -semantics (replaced by D7's no-column). Each retirement of an authorable shape is an +semantics (replaced by D7's no-column); ADR-0005's org-scoped write path for the five tier-A types; +the packaged-flow disable/clone door and `sys_metadata_activation` (unless a measured pull has +returned by then). Each retirement of an authorable shape is an [ADR-0087](./0087-metadata-protocol-upgrade-contract.md) entry. ### D14 — Staging @@ -478,14 +494,19 @@ refusal, mirror deletion, arm removal, retirements (D1/D9/D10/D13), in the stagi tenant-threaded reads, plus the store helpers — from row lookup to registry-first lookup. - **Setup, Studio and pickers change** (D7): Setup drops the declared rows it used to list, gains *Clone from package*; pickers read two sources. No server-side merge is built. -- **Organization-level template editing is unavailable** until an organization asks (D6); existing - customized template rows need a ruling (§6 Q1). Studio-authored templates remain editable in Studio. +- **Organization-level template editing is unavailable** (D6); existing customized template rows + need a ruling (§6 Q1). Studio-authored and template-installed templates remain editable in Studio. - **Deployment-level settings leave `sys_setting`** (D7): configuration or a tenant-less object. -- **The metadata ledger splits** (D6): `sys_metadata` loses its organization column and becomes the - environment definition ledger; per-organization overlays of the five presentational types move to - an org-owned object. This touches the layered read in `metadata-protocol` (`getMetaItem`), - `meta-write-org-scope.ts`, the ADR-0094 write-through, the ADR-0126 activation ledger, and - `sys-metadata-repository.ts` — the largest single item in the plan, and the reason C5 is its own card. +- **`sys_metadata` loses its organization column and ADR-0005's per-organization overlay axis is + retired** (D6): org-scoped writes of the five tier-A types are refused; the identity pin, the layered + read in `metadata-protocol` (`getMetaItem`), `meta-write-org-scope.ts`, the ADR-0094 write-through and + `sys-metadata-repository.ts` all simplify. A live feature is switched off; the maintainer confirms it + in §6 Q6. +- **Managed content is sealed** (D6): ADR-0126's disable/clone machinery for packaged flows (#11665) + goes dormant behind a refused door and the activation ledger is removed at protocol 18 unless a pull + returns; recently landed work is retired rather than kept inert. +- **The template install mode is new work** (D6): a manifest declaration of permitted modes, a + one-time importer into the environment ledger, and the posture refusal on `group` / `isolated`. - Tests pinning NULL semantics and seeded rows are rewritten (`deal_p1`, `sql-driver-tenant-scope`, `memory-tenancy-guard`, `rule-criteria-org-scope`, `per-organization-catalog`, `bootstrap-declared-email-templates`, cloud's `unscoped-control-plane-tenant-wall`). @@ -512,6 +533,7 @@ refusal, mirror deletion, arm removal, retirements (D1/D9/D10/D13), in the stagi | **Read-time overlay of code and database metadata for the same item** (the maintainer's prior platform; ADR-0005's mechanism) | Field-level precedence ambiguity, silent drift on upgrade, no way to delete a code item from the database, merged lists that re-implement search/sort/paging. D4 avoids it by keeping the sets disjoint and unioned by name; D7 avoids it by never merging lists server-side. | | **A string sentinel (`__global__`) as owner** | Breaks the FK to `sys_organization`; every layer special-cases it forever (ADR-0120 D3's COALESCE is the running cost). | | **Declare a legitimately org-less write per call** (#13636 option B, draft PR #14923) | Makes NULL a *declared* state instead of removing it; every future writer of a conditionally-scoped object must know to declare; the column stays nullable, so the constraint D1 wants can never land. Superseded by taking the column off the objects whose rows are legitimately org-less (§1.6). | +| **Keep ADR-0126's overlay and disable + clone regimes live for managed content** | Each regime is a per-type customization mechanism with its own ledger, walls and UI; under the startup posture the maintainer chose one switch (the install mode) over three mechanisms. Regime E stays because it costs nothing (a package). Paused, not rejected: the regimes return only on a measured pull, and never through a nullable tenant column. | --- @@ -526,15 +548,15 @@ refusal, mirror deletion, arm removal, retirements (D1/D9/D10/D13), in the stagi `sys_platform_setting` object? Proposed: configuration for infrastructure values, a tenant-less object only for values an operator must change without a restart. 4. **Staging** (D14): confirm the 17.x / protocol 18 boundary aligned with ADR-0120 D7. -5. **The metadata ledger split** (D6): one object losing its column plus a new org-owned overlay - object is the shape this record proposes; the alternative — keep one `sys_metadata` and make the - five overridable types the only rows with an organization — cannot satisfy D1 (a nullable column). - Confirm the split, or rule that per-organization overlays are retired for now (startup posture), - which would make `sys_metadata` tenant-less with no second object. -6. **A disable switch for views and dashboards** (D6, ADR-0126 §3): Regime O has no `active` bit - today. Admit `view` / `dashboard` into the disable half now (one row in ADR-0126 §3; the ledger is - generic), or wait for a measured pull? Proposed: wait — overlay and navigation already express - "not wanted" without a new switch. +5. **Who chooses the install mode** (D6): proposed — the package **declares** the modes it permits + (`installModes: ['managed'] | ['managed','template'] | ['template']`, default `['managed']`), the + installer picks one at install time, and a shared-DB multi-tenant deployment refuses `template` + whatever the package permits. Confirm, or name a different rule. +6. **Retiring ADR-0005's per-organization overlay axis** (D6): it follows from the sealing ruling and + removes the need to split `sys_metadata`, but it switches off a live feature (org-scoped writes of + the five tier-A types, pinned by identity). Confirm. +7. **The activation ledger and the flow disable/clone door** (#11665, landed): seal the door now and + remove the ledger at protocol 18 unless a pull returns (proposed), or remove both now? --- @@ -566,7 +588,8 @@ One epic tracks the family. Phases 1–3 are additive (17.x); phase 4 is protoco | C2 | Registry-first catalog resolution; references by name (id→name columns + rewrite); dangling-name boot report; cross-source uniqueness | D2, D4 | ADR merge | | C3 | Retire the seeders and the per-organization catalog machinery; built-ins and audience anchors as declared metadata; platform-admin grant row owned by the Default Organization | D2, D5, D13 | C1, C2 | | C4 | Templates: `sendTemplate` resolves the registry; seed and provenance stamp retired; door closed; customized-rows ruling applied | D6, D10 | ADR merge (+ §6 Q1) | -| C5 | Metadata ledger split: `sys_metadata` family tenant-less (environment definitions, UI-editable by metadata authors); per-organization overlays of the five presentational types in an org-owned object; `sys_metadata_activation` without the reserved column; ADR-0005 and ADR-0126 D3 amended | D6, D7 | C1 | +| C5 | `sys_metadata` family tenant-less (environment definitions, UI-editable by metadata authors); per-organization overlay axis retired (org-scoped writes of the five tier-A types refused); managed content sealed (ADR-0126 O/C doors refused, activation ledger dormant); ADR-0005 and ADR-0126 amended | D6, D7 | C1 | +| C12 | Template install mode: manifest `installModes`, one-time import into the environment ledger with provenance, refusal on `group` / `isolated`, CLI + marketplace surfaces | D6 | C5 | | C6 | Deployment-level state has no column: settings global rung leaves `sys_setting`; plumbing objects drop the column; #12699 declaration made total | D7 | ADR merge (+ §6 Q3) | | C7 | Inventory + migration: four fates, id→name rewrite verified before mirror deletion, per-table boot report | D10 | C2, C3, C4, C5, C6 | | C8 | One predicate; NOT NULL per cleared table; every-posture refusal; both arms and the ledger retired — protocol 18 | D1, D8, D9, D13, D14 | C7 | From 11c2dfa0d4fe07247864a841818749fc3c35033b Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Fri, 4 Sep 2026 01:23:04 +0800 Subject: [PATCH 07/24] =?UTF-8?q?docs(adr):=20ADR-0131=20D6=20=E2=80=94=20?= =?UTF-8?q?the=20unreleased=20ADR-0126=20flow=20disable/clone=20machinery?= =?UTF-8?q?=20is=20removed=20before=20the=20next=20release,=20not=20sealed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maintainer 2026-09-04: 「ADR-0126 已经开发完了吗?还没发版本呢」. Measured: #12158/#12296, #12156, #12419 landed on main from 2026-08-26; last release 17.2.0 tagged 2026-08-23 (npm serves 17.2.0). No compatibility obligation ⇒ remove (ADR-0049), §6 Q7 resolved, epic #12150 superseded. Refs #13564 Co-Authored-By: Claude Fable 5.1 --- ...6-packaged-metadata-customization-model.md | 2 +- ...ation-ownership-no-null-organization-id.md | 32 ++++++++++--------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/docs/adr/0126-packaged-metadata-customization-model.md b/docs/adr/0126-packaged-metadata-customization-model.md index b488410c14..22d25aa2b9 100644 --- a/docs/adr/0126-packaged-metadata-customization-model.md +++ b/docs/adr/0126-packaged-metadata-customization-model.md @@ -40,7 +40,7 @@ re-cited inline; the survey carries the instruments and positive controls. --- -> **Amended 2026-09-04 by [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D6.** Maintainer ruling, verbatim and untranslated: 「你这么说还不如先完全封死。flow 也先不让改。然后软件包应该有两种安装方式,有一种是模版形式直接进库,那就是所有都可以修改。但是单库多租户禁止安装这种模版软件包;有一种是受管软件包,什么都以软件包中的为准,就是不让改。」 Regimes **O** (overlay) and **C** (disable + clone) are **paused for managed content — packaged flows (§7) included**; customization is expressed by the **install mode** instead: *managed* (sealed, upgradeable; the only mode a shared-DB multi-tenant deployment accepts) or *template* (copied once into the environment ledger, fully editable, no upgrade channel, refused on `group` / `isolated`). D3's reserved `organization_id` on `sys_metadata_activation` is withdrawn (ADR-0131 D1: no nullable tenant column); the ledger has no writer while the door is sealed and is removed at protocol 18 unless a measured pull returns. Regime **E** stands. §7 remains the contract the regimes consume **if** they return. +> **Amended 2026-09-04 by [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D6.** Maintainer ruling, verbatim and untranslated: 「你这么说还不如先完全封死。flow 也先不让改。然后软件包应该有两种安装方式,有一种是模版形式直接进库,那就是所有都可以修改。但是单库多租户禁止安装这种模版软件包;有一种是受管软件包,什么都以软件包中的为准,就是不让改。」 Regimes **O** (overlay) and **C** (disable + clone) are **paused for managed content — packaged flows (§7) included**; customization is expressed by the **install mode** instead: *managed* (sealed, upgradeable; the only mode a shared-DB multi-tenant deployment accepts) or *template* (copied once into the environment ledger, fully editable, no upgrade channel, refused on `group` / `isolated`). D3's reserved `organization_id` on `sys_metadata_activation` is withdrawn (ADR-0131 D1: no nullable tenant column); the Regime C machinery landed for flows (#12158 / PR #12296, #12156, #12419) reached `main` after the last release (17.2.0, 2026-08-23) and is **removed before the next release** — nothing published depends on it; epic #12150 closes as superseded. Regime **E** stands. §7 remains the contract the regimes consume **if** they return. ## TL;DR diff --git a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md index eb0f3a1cca..5ea05fec21 100644 --- a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md +++ b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md @@ -329,11 +329,15 @@ overlay object holding D3 rows — never as a nullable column on the environment **What this does to ADR-0126.** Its three regimes were the answer to "how is packaged metadata customized without being edited"; the maintainer's answer is now "it is not — pick the install mode". -Regime O and Regime C are paused for managed content; the activation ledger (`sys_metadata_activation`, -first consumer packaged flows, #11665) has no writer while the door is sealed and is removed at protocol -18 unless a measured pull returns (D13, [ADR-0049](./0049-no-unenforced-security-properties.md)); -ADR-0126 D3's "org column reserved, written NULL" on that ledger is withdrawn — a reserved nullable -tenant column is the shape D1 forbids. Regime E stands. ADR-0126 carries the amendment note. +Regime O and Regime C are paused for managed content. The Regime C machinery that landed for packaged +flows — the activation ledger `sys_metadata_activation` and its `execute()`-time consult (#12158, PR +#12296), the flow clone action (#12156), the ledger convergence (#12419) — reached `main` on and after +2026-08-26, **after the last release** (17.2.0, tagged 2026-08-23; npm serves 17.2.0). Nothing published +depends on it, so it is **removed now, before the next release**, not sealed and carried +([ADR-0049](./0049-no-unenforced-security-properties.md): a shape nobody may use is not shipped +dormant). Clone without disable would in any case be worse than nothing — a cloned flow fires beside the +managed one it copied. Epic #12150 closes as superseded. ADR-0126 D3's "org column reserved, written +NULL" is withdrawn with the ledger. Regime E stands. ADR-0126 carries the amendment note. **Templates** follow the same two modes: a managed template renders from the registry and is immutable; a Studio-authored or template-installed template is environment metadata, editable in Studio. There is @@ -438,10 +442,10 @@ equivalent; the seeders and projector of D2; `per-organization-catalog.ts` inclu `meta-write-org-scope.ts`'s NULL layer, `bootstrap-system-capabilities.ts`; the email-template seed and `email-template-provenance.ts`; the `__global__` sentinel and ADR-0120 D3's `COALESCE`; `platform-object-tenancy.ts` and `isPlatformObjectOutOfTenantAuditScope`; #12699's stand-down -semantics (replaced by D7's no-column); ADR-0005's org-scoped write path for the five tier-A types; -the packaged-flow disable/clone door and `sys_metadata_activation` (unless a measured pull has -returned by then). Each retirement of an authorable shape is an -[ADR-0087](./0087-metadata-protocol-upgrade-contract.md) entry. +semantics (replaced by D7's no-column); ADR-0005's org-scoped write path for the five tier-A types. +The packaged-flow disable/clone door and `sys_metadata_activation` are **not** on this list because they +do not wait for protocol 18: unreleased, they are removed in C5 before the next release (D6). Each +retirement of an authorable shape is an [ADR-0087](./0087-metadata-protocol-upgrade-contract.md) entry. ### D14 — Staging @@ -502,9 +506,9 @@ refusal, mirror deletion, arm removal, retirements (D1/D9/D10/D13), in the stagi read in `metadata-protocol` (`getMetaItem`), `meta-write-org-scope.ts`, the ADR-0094 write-through and `sys-metadata-repository.ts` all simplify. A live feature is switched off; the maintainer confirms it in §6 Q6. -- **Managed content is sealed** (D6): ADR-0126's disable/clone machinery for packaged flows (#11665) - goes dormant behind a refused door and the activation ledger is removed at protocol 18 unless a pull - returns; recently landed work is retired rather than kept inert. +- **Managed content is sealed** (D6): ADR-0126's disable/clone machinery for packaged flows (#12158, + #12156, #12419) is removed before the next release — it never shipped (landed after the 17.2.0 tag), + so recently landed work is deleted rather than published dormant. - **The template install mode is new work** (D6): a manifest declaration of permitted modes, a one-time importer into the environment ledger, and the posture refusal on `group` / `isolated`. - Tests pinning NULL semantics and seeded rows are rewritten (`deal_p1`, `sql-driver-tenant-scope`, @@ -555,8 +559,6 @@ refusal, mirror deletion, arm removal, retirements (D1/D9/D10/D13), in the stagi 6. **Retiring ADR-0005's per-organization overlay axis** (D6): it follows from the sealing ruling and removes the need to split `sys_metadata`, but it switches off a live feature (org-scoped writes of the five tier-A types, pinned by identity). Confirm. -7. **The activation ledger and the flow disable/clone door** (#11665, landed): seal the door now and - remove the ledger at protocol 18 unless a pull returns (proposed), or remove both now? --- @@ -588,7 +590,7 @@ One epic tracks the family. Phases 1–3 are additive (17.x); phase 4 is protoco | C2 | Registry-first catalog resolution; references by name (id→name columns + rewrite); dangling-name boot report; cross-source uniqueness | D2, D4 | ADR merge | | C3 | Retire the seeders and the per-organization catalog machinery; built-ins and audience anchors as declared metadata; platform-admin grant row owned by the Default Organization | D2, D5, D13 | C1, C2 | | C4 | Templates: `sendTemplate` resolves the registry; seed and provenance stamp retired; door closed; customized-rows ruling applied | D6, D10 | ADR merge (+ §6 Q1) | -| C5 | `sys_metadata` family tenant-less (environment definitions, UI-editable by metadata authors); per-organization overlay axis retired (org-scoped writes of the five tier-A types refused); managed content sealed (ADR-0126 O/C doors refused, activation ledger dormant); ADR-0005 and ADR-0126 amended | D6, D7 | C1 | +| C5 | `sys_metadata` family tenant-less (environment definitions, UI-editable by metadata authors); per-organization overlay axis retired (org-scoped writes of the five tier-A types refused); managed content sealed — the unreleased packaged-flow disable/clone machinery and `sys_metadata_activation` **removed before the next release**; ADR-0005 and ADR-0126 amended | D6, D7 | C1 | | C12 | Template install mode: manifest `installModes`, one-time import into the environment ledger with provenance, refusal on `group` / `isolated`, CLI + marketplace surfaces | D6 | C5 | | C6 | Deployment-level state has no column: settings global rung leaves `sys_setting`; plumbing objects drop the column; #12699 declaration made total | D7 | ADR merge (+ §6 Q3) | | C7 | Inventory + migration: four fates, id→name rewrite verified before mirror deletion, per-table boot report | D10 | C2, C3, C4, C5, C6 | From 2746f3480e671954bdf56ff33ebd15c825d92c85 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Fri, 4 Sep 2026 01:39:44 +0800 Subject: [PATCH 08/24] =?UTF-8?q?docs(adr):=20ADR-0131=20D3=20=E2=80=94=20?= =?UTF-8?q?the=20catalog=20has=20one=20home=20(the=20environment=20registr?= =?UTF-8?q?y);=20assignments=20are=20organization=20rows;=20the=20four=20c?= =?UTF-8?q?atalog=20objects=20retire?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maintainer 2026-09-04: 「角色、岗位、权限集,Setup 里组织自建的是组织级。这个说的是单库单租户吧,单库多租户我可以禁止 他们创建。但是你要支持我绑定到人员。」 Single-tenant Setup creation = environment metadata write (ADR-0094 D3 redirect); shared-DB tenants assign but do not define; sys_user_position / sys_user_permission_set reference by name; sys_position, sys_permission_set, sys_position_permission_set, sys_capability retire (ADR-0094 completed). One source at selection. Refs #13564 Co-Authored-By: Claude Fable 5.1 --- ...ation-ownership-no-null-organization-id.md | 98 ++++++++++++------- 1 file changed, 65 insertions(+), 33 deletions(-) diff --git a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md index 5ea05fec21..2a178aab5e 100644 --- a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md +++ b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md @@ -14,9 +14,10 @@ group-level template rows 「不考虑集团级模板行,作废相关需求」 breaks every remaining tie: 「我们现在创业阶段,应该定一套最稳定可靠的方案,而不是盲目追求功能。」; and the provenance rule for editing: 「可以先约定代码推送过来的元数据就只能在代码中修改。studio 界面上配置的元数据, 保存在库中,可以在界面上修改,包括 ../cloud build agent 构建的元数据,因为也是保存在库中,可以在界面上修改。」; -and the sealing ruling that fixes D6: 「你这么说还不如先完全封死。flow 也先不让改。然后软件包应该有两种安装方式, +the sealing ruling that fixes D6: 「你这么说还不如先完全封死。flow 也先不让改。然后软件包应该有两种安装方式, 有一种是模版形式直接进库,那就是所有都可以修改。但是单库多租户禁止安装这种模版软件包;有一种是受管软件包,什么都以 -软件包中的为准,就是不让改。」 +软件包中的为准,就是不让改。」; and the catalog ruling that fixes D3: 「角色、岗位、权限集,Setup 里组织自建的是组织级。 +这个说的是单库单租户吧,单库多租户我可以禁止他们创建。但是你要支持我绑定到人员。」 **Builds on**: [ADR-0005](./0005-metadata-customization-overlay.md) (the metadata overlay — its environment layer is re-keyed by D6), [ADR-0049](./0049-no-unenforced-security-properties.md) (enforce or remove — the posture of D13), [ADR-0066](./0066-unified-authorization-model.md) D2 @@ -82,10 +83,13 @@ else. The maintainer's model is simpler and this record adopts it whole: environment's, not any organization's, and its ledger carries **no organization column**. Template packages are **refused on shared-DB multi-tenant deployments**. Both provenances are cross-tenant by nature: every organization runs the same schema. -2. **A row with an organization column exists only when an organization authored it** — a position an - admin created in Setup, a set they cloned, an assignment they made. Every such row carries its - organization, NOT NULL. Registry items are referenced **by name**, the way ADR-0129 already treats - objects. +2. **A row with an organization column exists only when an organization authored it** — an assignment + (this user holds this position, this user holds this permission set), a business unit, a sharing + rule, a record share. Every such row carries its organization, NOT NULL, and references registry + items **by name**, the way ADR-0129 already treats objects. **Catalog definitions — positions, + permission sets, capabilities — are never organization rows**: they live only in the environment + registry. In a single-tenant deployment the organization *is* the environment, so its admins author + them there; in a shared-DB multi-tenant deployment tenants **assign but do not define**. 3. **A table with no organization column is deployment-level or code-level**, governed by permission, not by a wall. There is no third shape. @@ -238,15 +242,33 @@ D2/D4 (D1 of that record stands and is generalized; its projection is no longer per-organization catalog machinery (`per-organization-catalog.ts`, #10103 Option C) — there is nothing left to copy per organization. -### D3 — A row exists only when an organization authored it - -The tables that hold catalog-shaped data (`sys_position`, `sys_permission_set`, -`sys_position_permission_set`, `sys_user_position`, `sys_user_permission_set`, `sys_capability`, -`sys_sharing_rule`, `sys_record_share`, `sys_email_template`, `sys_notification_template`, …) hold -**only what an organization wrote**: positions and sets an admin created (including clones of declared -ones), capabilities they defined, rules they authored, assignments and bindings they made, and — when -a door is open (D6) — overrides they saved. Every such row carries the authoring organization, NOT -NULL. +### D3 — A row exists only when an organization authored it; the catalog is environment-level, assignments are organization-level + +**The catalog has one home.** Positions, permission sets and capabilities are **definitions**, and a +definition lives only in the environment registry — code-declared (managed, sealed) or +environment-authored (Studio, template package; editable by metadata-authoring capability holders). +The position → permission-set binding is part of the position's definition, declared in code or +authored in Studio. There is **no organization-level catalog**: the objects `sys_position`, +`sys_permission_set`, `sys_position_permission_set` and `sys_capability` retire (D13), completing +[ADR-0094](./0094-sys-permission-set-pure-projection.md) D1 — the metadata layer was already the sole +authoritative store; the projected row no longer exists either. Consequences by posture, verbatim from +the ruling 「单库多租户我可以禁止他们创建。但是你要支持我绑定到人员」: + +- **Single-tenant** (`single`): the organization is the environment. An admin who creates a position + or permission set in Setup performs an environment metadata write — the redirect ADR-0094 D3 + already makes today — gated by the metadata-authoring capability, which the deployment's owner holds + and grants as they see fit. +- **Shared-DB multi-tenant** (`group` / `isolated`): tenant admins **cannot create or edit catalog + items**; the operator defines the catalog for every tenant (managed packages, Studio). Creation + through Setup or the API is refused with a message naming the posture and the capability. + +**Assignments are organization rows, in every posture.** `sys_user_position` (this user holds this +position) and `sys_user_permission_set` (this user holds this set) are owned by the organization the +assignment is made in, NOT NULL, reference the catalog item **by name** (D4), and are created and +removed by that organization's admins for its own members — walled by Layer 0 like any other +organization data. So are the other things an organization writes: business units, sharing rules and +record shares it authors, approvals, files, and every business row. Every such row carries the +authoring organization, NOT NULL. Under `single` the authoring organization is the Default Organization ([ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md); `ensure-default-organization.ts`), which @@ -255,7 +277,7 @@ becomes **load-bearing**: it exists before the first authenticated write (today inversion is needed — the organization only has to exist before a person acts. An organization has no rows until it authors something. A fresh deployment with ten organizations -and no customization has an **empty** catalog table. +has **no catalog tables at all** and empty assignment tables until an admin assigns someone. ### D4 — Declared items are referenced by name; resolution is registry-first @@ -304,9 +326,10 @@ anchor retire with D13. No NULL grant row is ever produced. with a message naming the posture: a template import would hand every tenant an editable shared schema. Under `single` it is how a customer takes an app and makes it theirs. -Creating one's own item — a new permission set, position, view, flow — in Setup (organization-owned, -D3) or Studio (environment-owned) is ordinary authoring, not customization of a managed item; -pre-filling the form from a managed item is a UI convenience that records no linkage. +Creating one's own item — a new permission set, position, view, flow — is environment authoring +(Studio, or Setup acting as a metadata editor under `single`), gated by the metadata-authoring +capability and refused to tenants of a shared-DB deployment (D3). It is not customization of a +managed item; pre-filling the form from a managed item is a UI convenience that records no linkage. **Code-provenance metadata is edited in code.** No runtime door edits a managed item. @@ -367,11 +390,11 @@ per-organization on a tenant runtime. **Presentation** (the lesson of the maintainer's prior platform, where a merged list of in-memory and database metadata forced search, sort and paging to be re-implemented in application code): no surface -promises **one merged, server-paged list** of declared and organization-authored items. Studio lists -declared items from the registry; Setup lists the organization's rows with the data API's native -search, sort and paging, plus a *Clone from package* action that reads the registry; pickers -(assignments, bindings, recipients) union the two **small** sets client-side. Declared catalogs are -tens of items; nothing about them needs a server. +merges sources. The catalog has one source, the registry (D3): Studio lists and edits it; Setup's +position and permission-set pages read the same registry — as an editor under `single` for capability +holders, read-only under a wall — and the assignment pages list the organization's own rows with the +data API's native search, sort and paging. Pickers (assign a user to a position, choose a recipient) +list the registry. Catalogs are tens of items; nothing about them needs a server-side merge. ### D8 — One predicate, computed once @@ -442,7 +465,9 @@ equivalent; the seeders and projector of D2; `per-organization-catalog.ts` inclu `meta-write-org-scope.ts`'s NULL layer, `bootstrap-system-capabilities.ts`; the email-template seed and `email-template-provenance.ts`; the `__global__` sentinel and ADR-0120 D3's `COALESCE`; `platform-object-tenancy.ts` and `isPlatformObjectOutOfTenantAuditScope`; #12699's stand-down -semantics (replaced by D7's no-column); ADR-0005's org-scoped write path for the five tier-A types. +semantics (replaced by D7's no-column); ADR-0005's org-scoped write path for the five tier-A types; +the catalog objects `sys_position`, `sys_permission_set`, `sys_position_permission_set` and +`sys_capability` (D3 — definitions live in the registry; ADR-0094's projection is no longer needed). The packaged-flow disable/clone door and `sys_metadata_activation` are **not** on this list because they do not wait for protocol 18: unreleased, they are removed in C5 before the next release (D6). Each retirement of an authorable shape is an [ADR-0087](./0087-metadata-protocol-upgrade-contract.md) entry. @@ -491,13 +516,19 @@ refusal, mirror deletion, arm removal, retirements (D1/D9/D10/D13), in the stagi **What it costs.** -- **Reference columns move from id to name** (D4): `sys_user_position`, `sys_position_permission_set`, +- **Reference columns move from id to name** (D4): `sys_user_position`, `sys_user_permission_set`, sharing recipients, grants — a schema migration with an id→name rewrite over existing rows, and the verified rewrite is the precondition of D10 fate 2. -- **Resolution changes at the measured sites** — the read-side ledger counted them: three objects with - tenant-threaded reads, plus the store helpers — from row lookup to registry-first lookup. -- **Setup, Studio and pickers change** (D7): Setup drops the declared rows it used to list, gains - *Clone from package*; pickers read two sources. No server-side merge is built. +- **Four catalog objects retire** (D3/D13): `sys_position`, `sys_permission_set`, + `sys_position_permission_set`, `sys_capability`. Every reader of those tables — authorization + resolution, hierarchy security, delegated admin, sharing recipients, the Setup pages — moves to the + registry. The read-side ledger counted the tenant-threaded readers; the bare-context readers are the + larger population and are enumerated by C2. +- **Setup changes shape** (D7): its catalog pages become registry views (editors under `single`, + read-only under a wall); its assignment pages stay data pages. No server-side merge is built. +- **Tenants of a shared-DB deployment lose catalog authoring** they nominally have today (creating a + position or set in Setup). They keep assignment. A tenant that needs its own catalog gets its own + environment. - **Organization-level template editing is unavailable** (D6); existing customized template rows need a ruling (§6 Q1). Studio-authored and template-installed templates remain editable in Studio. - **Deployment-level settings leave `sys_setting`** (D7): configuration or a tenant-less object. @@ -537,6 +568,7 @@ refusal, mirror deletion, arm removal, retirements (D1/D9/D10/D13), in the stagi | **Read-time overlay of code and database metadata for the same item** (the maintainer's prior platform; ADR-0005's mechanism) | Field-level precedence ambiguity, silent drift on upgrade, no way to delete a code item from the database, merged lists that re-implement search/sort/paging. D4 avoids it by keeping the sets disjoint and unioned by name; D7 avoids it by never merging lists server-side. | | **A string sentinel (`__global__`) as owner** | Breaks the FK to `sys_organization`; every layer special-cases it forever (ADR-0120 D3's COALESCE is the running cost). | | **Declare a legitimately org-less write per call** (#13636 option B, draft PR #14923) | Makes NULL a *declared* state instead of removing it; every future writer of a conditionally-scoped object must know to declare; the column stays nullable, so the constraint D1 wants can never land. Superseded by taking the column off the objects whose rows are legitimately org-less (§1.6). | +| **An organization-level catalog beside the environment one** (this record's second draft: tenant-created positions and sets as org-owned rows, pickers unioning registry and rows) | Two sources at selection time, a uniqueness check spanning both, a resolution order — three costs paid so that a tenant of a shared-DB deployment can define its own roles, a need no customer has stated. Deferred: if it arrives, it returns as an org-owned catalog object (the same shape), never as rows in a nullable-column table. | | **Keep ADR-0126's overlay and disable + clone regimes live for managed content** | Each regime is a per-type customization mechanism with its own ledger, walls and UI; under the startup posture the maintainer chose one switch (the install mode) over three mechanisms. Regime E stays because it costs nothing (a package). Paused, not rejected: the regimes return only on a measured pull, and never through a nullable tenant column. | --- @@ -587,15 +619,15 @@ One epic tracks the family. Phases 1–3 are additive (17.x); phase 4 is protoco | # | Card | Decisions | Blocked by | |---|---|---|---| | C1 | Default Organization load-bearing under `single`; `resolveSystemInsertOrganization` derives it, refuses elsewhere | D3, D9, D11 | ADR merge | -| C2 | Registry-first catalog resolution; references by name (id→name columns + rewrite); dangling-name boot report; cross-source uniqueness | D2, D4 | ADR merge | -| C3 | Retire the seeders and the per-organization catalog machinery; built-ins and audience anchors as declared metadata; platform-admin grant row owned by the Default Organization | D2, D5, D13 | C1, C2 | +| C2 | Catalog resolution reads the registry; assignment tables reference by name (id→name columns + rewrite); dangling-name boot report; every reader of the four catalog tables enumerated and converted | D2, D3, D4 | ADR merge | +| C3 | Retire the seeders, the per-organization catalog machinery and the four catalog objects; built-ins and audience anchors as declared metadata; Setup catalog creation = environment metadata write under `single`, refused under a wall; platform-admin grant row owned by the Default Organization | D2, D3, D5, D13 | C1, C2 | | C4 | Templates: `sendTemplate` resolves the registry; seed and provenance stamp retired; door closed; customized-rows ruling applied | D6, D10 | ADR merge (+ §6 Q1) | | C5 | `sys_metadata` family tenant-less (environment definitions, UI-editable by metadata authors); per-organization overlay axis retired (org-scoped writes of the five tier-A types refused); managed content sealed — the unreleased packaged-flow disable/clone machinery and `sys_metadata_activation` **removed before the next release**; ADR-0005 and ADR-0126 amended | D6, D7 | C1 | | C12 | Template install mode: manifest `installModes`, one-time import into the environment ledger with provenance, refusal on `group` / `isolated`, CLI + marketplace surfaces | D6 | C5 | | C6 | Deployment-level state has no column: settings global rung leaves `sys_setting`; plumbing objects drop the column; #12699 declaration made total | D7 | ADR merge (+ §6 Q3) | | C7 | Inventory + migration: four fates, id→name rewrite verified before mirror deletion, per-table boot report | D10 | C2, C3, C4, C5, C6 | | C8 | One predicate; NOT NULL per cleared table; every-posture refusal; both arms and the ledger retired — protocol 18 | D1, D8, D9, D13, D14 | C7 | -| C9 | objectui: Setup lists org rows + *Clone from package*; Studio lists declared; pickers dual-source client-side; no merged server list | D7 | C2 | +| C9 | objectui: Setup catalog pages read the registry (editor under `single`, read-only under a wall); assignment pages stay data pages; pickers list the registry; Studio lists and edits environment metadata | D3, D7 | C2 | | C10 | cloud: control plane and objectos-ee adopt the record (per-deployment no-column, backfill, tests) | D7, D8, D10 | C6, then C8 | | C11 | Docs and card-family close-out (#13564, #11611, #10103 posture; #13636 superseded, PR #14923 closed unmerged) | — | C8, C10 | From de84c708bd029744dc8a410a89d600260ae2e647 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Fri, 4 Sep 2026 01:41:17 +0800 Subject: [PATCH 09/24] =?UTF-8?q?docs(adr):=20ADR-0131=20D4=20=E2=80=94=20?= =?UTF-8?q?resolution=20reads=20the=20registry=20only;=20no=20second=20sou?= =?UTF-8?q?rce,=20no=20cross-source=20uniqueness?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follows D3 (one catalog home). Refs #13564 Co-Authored-By: Claude Fable 5.1 --- ...ation-ownership-no-null-organization-id.md | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md index 2a178aab5e..72964a0602 100644 --- a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md +++ b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md @@ -279,25 +279,25 @@ inversion is needed — the organization only has to exist before a person acts. An organization has no rows until it authors something. A fresh deployment with ten organizations has **no catalog tables at all** and empty assignment tables until an admin assigns someone. -### D4 — Declared items are referenced by name; resolution is registry-first +### D4 — Catalog items are referenced by name; resolution reads the registry [ADR-0129](./0129-object-name-is-the-canonical-id.md) made the object `name` the canonical id; this -record extends the principle to catalog items. A declared position or permission set has no row id, -so every reference to a catalog item — `sys_user_position`, `sys_position_permission_set`, sharing -recipients, grants — names it by its machine name, which is already unique per organization and, -for declared names, reserved across the deployment (an organization may not create an item whose -name a declaration holds; the uniqueness check spans registry and rows). - -Resolution is **registry first (both provenances — code-declared and environment-authored), then the -caller's organization's rows**. `resolve-authz-context.ts` already looks positions up by name -(`grants.positions`); the change is where it looks first. A name -that resolves nowhere — a declaration removed or renamed in code — **fails closed** for that reference -and is **reported at boot** per organization, by name. This is loud where today's behaviour is a -zombie: a seeded mirror of a removed declaration stays in the table and keeps granting. - -The registry and the organization's rows are **disjoint sets unioned by name**, not two definitions -of one item merged field by field. That distinction is what keeps the read-time-merge failures of the -overlay model (§5) out of this design. +record extends the principle to catalog items. A position or permission set has no row id, so every +reference to one — `sys_user_position`, `sys_user_permission_set`, sharing-rule recipients, grants — +names it by its machine name. The namespace is the environment registry's: code-declared and +environment-authored items share it, and Studio refuses a name a managed package holds. + +Resolution reads the **registry** — one source, both provenances. `resolve-authz-context.ts` already +looks positions up by name (`grants.positions`); the change is that the lookup is a registry read +instead of a table read, and the position → permission-set binding is read from the position's +definition instead of a junction table. A name that resolves nowhere — a declaration removed or renamed +in code — **fails closed** for that reference and is **reported at boot** per organization, by name. +This is loud where today's behaviour is a zombie: a seeded mirror of a removed declaration stays in the +table and keeps granting. + +Because the catalog has one home (D3), nothing is merged at selection time: a picker lists the +registry, an assignment names one of its entries. The read-time-merge failures of the overlay model +(§5) cannot arise. ### D5 — Platform standing is configuration, or a Default-Organization row under `single` From 5d55c80100c26cad978dc425aada63ac1c5c2780 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Fri, 4 Sep 2026 01:43:50 +0800 Subject: [PATCH 10/24] docs(adr): ADR-0126 superseded by ADR-0131; staging = one pre-17.3 revert, everything else on the v18 line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maintainer 2026-09-04: 「ADR-0126 可以先作废,我发 17.3,然后后续这么大的改动应该放到 v18吧?」 D14 rewritten; §1.7 keeps ADR-0126's survey findings; C0 (revert #12296/#12419/#12156 before 17.3) added; §6 staging question resolved. Refs #13564 Co-Authored-By: Claude Fable 5.1 --- ...6-packaged-metadata-customization-model.md | 5 +- ...ation-ownership-no-null-organization-id.md | 52 ++++++++++++++----- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/docs/adr/0126-packaged-metadata-customization-model.md b/docs/adr/0126-packaged-metadata-customization-model.md index 22d25aa2b9..7ab6a702ce 100644 --- a/docs/adr/0126-packaged-metadata-customization-model.md +++ b/docs/adr/0126-packaged-metadata-customization-model.md @@ -1,6 +1,7 @@ # ADR-0126: The platform customization model for packaged metadata — three regimes, one activation ledger -**Status**: Proposed (2026-08-25) — awaiting the maintainer's hand-merge, which is itself the +**Status**: **Superseded** (2026-09-04) by [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D6 — maintainer, verbatim: 「ADR-0126 可以先作废」. The regimes below are replaced by the install mode (managed = sealed, template = copied in and editable); the activation ledger and the packaged-flow disable/clone machinery are reverted before 17.3 (never released). Kept as the record of the survey (ADR-0131 §1.7 summarizes it). Original status line follows. +**Original status**: Proposed (2026-08-25) — awaiting the maintainer's hand-merge, which is itself the acceptance act for a governed surface (Prime Directive #14) **and the ruling that settles the tentative flow-instance directions in §7** (chartered on [#12049](https://github.com/objectstack-ai/objectstack/issues/12049); ⛔ none of §7 is settled @@ -40,7 +41,7 @@ re-cited inline; the survey carries the instruments and positive controls. --- -> **Amended 2026-09-04 by [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D6.** Maintainer ruling, verbatim and untranslated: 「你这么说还不如先完全封死。flow 也先不让改。然后软件包应该有两种安装方式,有一种是模版形式直接进库,那就是所有都可以修改。但是单库多租户禁止安装这种模版软件包;有一种是受管软件包,什么都以软件包中的为准,就是不让改。」 Regimes **O** (overlay) and **C** (disable + clone) are **paused for managed content — packaged flows (§7) included**; customization is expressed by the **install mode** instead: *managed* (sealed, upgradeable; the only mode a shared-DB multi-tenant deployment accepts) or *template* (copied once into the environment ledger, fully editable, no upgrade channel, refused on `group` / `isolated`). D3's reserved `organization_id` on `sys_metadata_activation` is withdrawn (ADR-0131 D1: no nullable tenant column); the Regime C machinery landed for flows (#12158 / PR #12296, #12156, #12419) reached `main` after the last release (17.2.0, 2026-08-23) and is **removed before the next release** — nothing published depends on it; epic #12150 closes as superseded. Regime **E** stands. §7 remains the contract the regimes consume **if** they return. +> **Superseded 2026-09-04 by [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D6.** Maintainer ruling, verbatim and untranslated: 「你这么说还不如先完全封死。flow 也先不让改。然后软件包应该有两种安装方式,有一种是模版形式直接进库,那就是所有都可以修改。但是单库多租户禁止安装这种模版软件包;有一种是受管软件包,什么都以软件包中的为准,就是不让改。」 Regimes **O** (overlay) and **C** (disable + clone) are **paused for managed content — packaged flows (§7) included**; customization is expressed by the **install mode** instead: *managed* (sealed, upgradeable; the only mode a shared-DB multi-tenant deployment accepts) or *template* (copied once into the environment ledger, fully editable, no upgrade channel, refused on `group` / `isolated`). D3's reserved `organization_id` on `sys_metadata_activation` is withdrawn (ADR-0131 D1: no nullable tenant column); the Regime C machinery landed for flows (#12158 / PR #12296, #12156, #12419) reached `main` after the last release (17.2.0, 2026-08-23) and is **removed before the next release** — nothing published depends on it; epic #12150 closes as superseded. Regime **E** stands. §7 remains the contract the regimes consume **if** they return. ## TL;DR diff --git a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md index 72964a0602..f744954af8 100644 --- a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md +++ b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md @@ -3,6 +3,7 @@ **Status**: Proposed (2026-09-04) — awaiting the maintainer's hand-merge, which is itself the acceptance act for a governed surface (Prime Directive #14). ⛔ Nothing below is settled until this record merges; the implementation cards are cut **from** the merged ADR, never ahead of it. +**Supersedes**: [ADR-0126](./0126-packaged-metadata-customization-model.md) (maintainer, 2026-09-04: 「ADR-0126 可以先作废」— its regimes are replaced by the install mode of D6, its activation ledger is removed before 17.3; §1.7 keeps what its survey found so the superseded record need not be read). **Deciders**: ObjectStack maintainer, 2026-09-03/04, live chat on [#13564](https://github.com/objectstack-ai/objectstack/issues/13564), verbatim and untranslated, in the order the model was built: the premise 「我理解只有代码定义的元数据是跨租户的,对象、字段、视图等 @@ -194,6 +195,21 @@ plain data field, not the tenancy anchor (D7). Once the column is gone there is a declared-NULL mechanism would keep alive exactly the state D1 removes. #14923 therefore does not merge; #13636 closes as superseded when this record merges (§8, C11). +### 1.7 What ADR-0126 found, kept here so the superseded record need not be read + +ADR-0126 (2026-08-25) surveyed every metadata type for post-install customization pull and found +three mechanisms already in the tree, each invented per type with its own ledger: an **organization +overlay** for exactly five presentational types (ADR-0005 tier A), **clone-to-customize** for permission +sets (the 2026-08-24 lock-the-base ruling), and **package-grain extend** for objects and app navigation +(`objectExtensions`, ADR-0029 D7). It recorded that two shipped documentation pages promised "install +with one click, then customize in Studio" while the platform refused nearly every post-install change; +that a tenant had once switched a shipped flow off environment-wide through an unscoped in-process map +(#10243); and that behavioral types must never gain an org overlay (the #6190 wall). Its answer was a +per-type regime table plus a generic activation ledger. This record keeps the findings and replaces the +answer: the extend mechanism stands because it is a package; the #10243 class is closed by sealing +managed content rather than by a durable switch; and the documentation promise is rewritten as "install +managed, or install as a template" (C11). + --- ## 2. Decision @@ -356,7 +372,7 @@ Regime O and Regime C are paused for managed content. The Regime C machinery tha flows — the activation ledger `sys_metadata_activation` and its `execute()`-time consult (#12158, PR #12296), the flow clone action (#12156), the ledger convergence (#12419) — reached `main` on and after 2026-08-26, **after the last release** (17.2.0, tagged 2026-08-23; npm serves 17.2.0). Nothing published -depends on it, so it is **removed now, before the next release**, not sealed and carried +depends on it, so it is **removed before 17.3 is cut** — a revert of #12296, #12419 and the clone action (#12156) — not sealed and carried ([ADR-0049](./0049-no-unenforced-security-properties.md): a shape nobody may use is not shipped dormant). Clone without disable would in any case be worse than nothing — a cloned flow fires beside the managed one it copied. Epic #12150 closes as superseded. ADR-0126 D3's "org column reserved, written @@ -472,15 +488,25 @@ The packaged-flow disable/clone door and `sys_metadata_activation` are **not** o do not wait for protocol 18: unreleased, they are removed in C5 before the next release (D6). Each retirement of an authorable shape is an [ADR-0087](./0087-metadata-protocol-upgrade-contract.md) entry. -### D14 — Staging +### D14 — Staging: one pre-17.3 removal, everything else on the v18 line + +Maintainer, 2026-09-04: 「我发 17.3,然后后续这么大的改动应该放到 v18」. Two consequences: + +- **Before 17.3 is cut**, the unreleased ADR-0126 flow machinery is reverted (D6; C0). Nothing else of + this record ships in 17.x. The #13491 tenant-audit ledger (#13635, also unreleased) **may** ship in + 17.3: it is internal protection with no authorable surface, refuses org-less system writes on walled + postures, and retires in v18 as machinery, not as a contract. +- **Everything else is the v18 line** — one major, one migration: D1's constraint, D2/D3's retirements, + D4's name references, D6's sealing and template mode, D7's column drops, D8's single predicate, D9's + refusals, D10's four-fate migration, D13's retirements. Within the line the order of §8 still holds + (C1–C6 before C7 before C8), and the migration keeps its per-table gate (D10) — the customer database + is never asked to satisfy a constraint its report has not cleared. The driver arms are removed in the + same major, after C7. ⛔ No 17.x card narrows or removes an arm, adds a name column beside an id + column, or ships a half of this record. -**17.x, additive**: the Default Organization made load-bearing (D3); registry-first resolution and -name-keyed references added beside the id columns (D4); refusals behind the existing #13491 gate (D9); -D7's no-column declarations; the D10 inventory and attribution migration; the template door closed -(D6). The driver arms stay as the compatibility path. **Protocol 18**: NOT NULL, every-posture -refusal, mirror deletion, arm removal, retirements (D1/D9/D10/D13), in the staging -[ADR-0120](./0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md) D7 set. Until D13 runs, -⛔ no card narrows or removes an arm. +Doing it in one major rather than additively across 17.x avoids carrying dual id/name columns, a +registry-first-then-rows resolution, and a sealed-but-present flow ledger through a public release — +compatibility shims for a shape nobody has used yet. --- @@ -583,12 +609,11 @@ refusal, mirror deletion, arm removal, retirements (D1/D9/D10/D13), in the stagi 3. **Deployment-level settings** (D7): configuration file/environment, or a tenant-less `sys_platform_setting` object? Proposed: configuration for infrastructure values, a tenant-less object only for values an operator must change without a restart. -4. **Staging** (D14): confirm the 17.x / protocol 18 boundary aligned with ADR-0120 D7. -5. **Who chooses the install mode** (D6): proposed — the package **declares** the modes it permits +4. **Who chooses the install mode** (D6): proposed — the package **declares** the modes it permits (`installModes: ['managed'] | ['managed','template'] | ['template']`, default `['managed']`), the installer picks one at install time, and a shared-DB multi-tenant deployment refuses `template` whatever the package permits. Confirm, or name a different rule. -6. **Retiring ADR-0005's per-organization overlay axis** (D6): it follows from the sealing ruling and +5. **Retiring ADR-0005's per-organization overlay axis** (D6): it follows from the sealing ruling and removes the need to split `sys_metadata`, but it switches off a live feature (org-scoped writes of the five tier-A types, pinned by identity). Confirm. @@ -614,10 +639,11 @@ refusal, mirror deletion, arm removal, retirements (D1/D9/D10/D13), in the stagi ## 8. Execution plan (cards are cut from the merged record) -One epic tracks the family. Phases 1–3 are additive (17.x); phase 4 is protocol 18. +One epic tracks the family. C0 lands before 17.3; every other card is on the **v18** line (D14), in this order. | # | Card | Decisions | Blocked by | |---|---|---|---| +| C0 | Revert the unreleased ADR-0126 flow machinery (#12296, #12419, #12156) so 17.3 does not publish it | D6, D14 | ADR merge; **before 17.3** | | C1 | Default Organization load-bearing under `single`; `resolveSystemInsertOrganization` derives it, refuses elsewhere | D3, D9, D11 | ADR merge | | C2 | Catalog resolution reads the registry; assignment tables reference by name (id→name columns + rewrite); dangling-name boot report; every reader of the four catalog tables enumerated and converted | D2, D3, D4 | ADR merge | | C3 | Retire the seeders, the per-organization catalog machinery and the four catalog objects; built-ins and audience anchors as declared metadata; Setup catalog creation = environment metadata write under `single`, refused under a wall; platform-admin grant row owned by the Default Organization | D2, D3, D5, D13 | C1, C2 | From c40b6e382ede94845c4453e092bd2917d38a44db Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Fri, 4 Sep 2026 01:50:05 +0800 Subject: [PATCH 11/24] =?UTF-8?q?docs(adr):=20ADR-0131=20D7=20=E2=80=94=20?= =?UTF-8?q?the=20no-column=20list=20is=20decided=20by=20the=20writer,=20no?= =?UTF-8?q?t=20the=20name;=20sys=5Fhttp=5Fdelivery=20and=20sys=5Femail=20a?= =?UTF-8?q?re=20tenant=20data=20(#13565,=20#11741);=20sys=5Fmetadata=5Fact?= =?UTF-8?q?ivation=20is=20reverted=20before=2017.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #13564 Co-Authored-By: Claude Fable 5.1 --- ...zation-ownership-no-null-organization-id.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md index f744954af8..70baa30465 100644 --- a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md +++ b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md @@ -388,16 +388,26 @@ whole-item, `(organization, name, locale)`, *Reset to default* deletes it — ne Rows that belong to the deployment and not to an organization live in objects **without** the column (D1): the environment metadata ledger and its family (`sys_metadata`, `sys_metadata_audit`, -`sys_metadata_commit`, `sys_metadata_history`, `sys_metadata_activation`, environment-level -`sys_view_definition` — D6), operational plumbing (`sys_job`, `sys_job_run`, `sys_job_queue`, -`sys_flow_dispatch`, `sys_http_delivery`, `sys_migration`, `sys_migration_journal`, and the -recipient-anchored notification/inbox family where `user_id` is the anchor), the audit ledger +`sys_metadata_commit`, `sys_metadata_history`, environment-level `sys_view_definition` — D6; +`sys_metadata_activation` is reverted before 17.3 and does not return), operational plumbing whose +rows no writer attributes to an organization (`sys_job`, `sys_job_run`, `sys_job_queue`, +`sys_flow_dispatch`, `sys_migration`, `sys_migration_journal`), the audit ledger (`sys_audit_log`, whose rows may concern deployment-level actions — the organization an audit row is *about* becomes a plain attribution field under a name the tenant-field resolver does not claim, never the tenancy anchor; cloud's `tenant_id` rule already reads it this way), and deployment-level runtime settings — `sys_setting`'s global rung leaves the tenant-scoped table for configuration or a tenant-less object. Such objects are governed by object permission, not by the wall. +⚠️ **Membership in this list is decided by the writer, not by the object's name.** The read-side ledger +grouped `sys_http_delivery`, `sys_inbox_message`, `sys_notification*` and `sys_email` as "plumbing"; the +tree has since decided otherwise for each — #13565 stamps `sys_http_delivery` from the webhook's +organization *because* `redeliver()` walls by tenant, #11741 widened `SendEmailInput` so `sys_email` is +stamped at its producers (#11303 Decision 2), and the notification family is recipient-anchored in +cloud's reading. An object whose writer attributes rows to an organization is tenant data and keeps the +column (NOT NULL, D3); an object no writer attributes is deployment-level and loses it. The C7 +inventory records the verdict per object with the writer fact as its citation — this record names only +the objects whose verdict the tree already states. + The #12699 deployment declaration becomes **total**: an object a deployment declares platform-global gets **no organization column on that deployment** (the injected-columns plan reads the declaration), so Layer 0 and the driver agree by having nothing to scope. This is how the cloud control plane From 24821ea548d3199369b1a913e646bc8272f27c80 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Fri, 4 Sep 2026 01:52:06 +0800 Subject: [PATCH 12/24] =?UTF-8?q?docs(adr):=20ADR-0131=20D3=20=E2=80=94=20?= =?UTF-8?q?application=20seed=20datasets=20need=20the=20Default=20Organiza?= =?UTF-8?q?tion=20first;=20the=20seed=20loader's=20sys=5F=20exemption=20is?= =?UTF-8?q?=20withdrawn=20(#14547=20root)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #13564 Co-Authored-By: Claude Fable 5.1 --- ...anization-ownership-no-null-organization-id.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md index 70baa30465..b176446ee8 100644 --- a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md +++ b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md @@ -288,9 +288,16 @@ authoring organization, NOT NULL. Under `single` the authoring organization is the Default Organization ([ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md); `ensure-default-organization.ts`), which -becomes **load-bearing**: it exists before the first authenticated write (today it is created on -`kernel:ready` best-effort; a failure becomes a boot error). Because nothing is seeded, no boot-order -inversion is needed — the organization only has to exist before a person acts. +becomes **load-bearing**: it exists before the first write that needs an owner (today it is created on +`kernel:ready` best-effort; a failure becomes a boot error). The catalog needs no ordering because it is +never seeded — but **application seed data** (`SeedSchema` datasets: a showcase's accounts, business +units, sample records) still is, and today lands `organization_id = NULL` on a first boot because the +organization does not exist yet (`seed-loader.ts::resolveSoleOrganizationId` correctly finds none; the +`seed-tenancy-backfill` migration exists for exactly that residue). Two rules close it: the Default +Organization is created **before** application seed datasets load under `single`, and the seed loader's +exemption of `sys_` / `cloud_` / `ai_` seeds from stamping ("intentionally global") is withdrawn — there +are no platform-global seeds left; a seeded `sys_business_unit` is the organization's business unit +(D9 derives the owner; #14547 is this defect seen from the sharing side). An organization has no rows until it authors something. A fresh deployment with ten organizations has **no catalog tables at all** and empty assignment tables until an admin assigns someone. @@ -654,7 +661,7 @@ One epic tracks the family. C0 lands before 17.3; every other card is on the **v | # | Card | Decisions | Blocked by | |---|---|---|---| | C0 | Revert the unreleased ADR-0126 flow machinery (#12296, #12419, #12156) so 17.3 does not publish it | D6, D14 | ADR merge; **before 17.3** | -| C1 | Default Organization load-bearing under `single`; `resolveSystemInsertOrganization` derives it, refuses elsewhere | D3, D9, D11 | ADR merge | +| C1 | Default Organization load-bearing under `single` and created before application seed datasets load; the seed loader stamps every seed (the `sys_` exemption withdrawn); `resolveSystemInsertOrganization` derives it, refuses elsewhere | D3, D9, D11 | ADR merge | | C2 | Catalog resolution reads the registry; assignment tables reference by name (id→name columns + rewrite); dangling-name boot report; every reader of the four catalog tables enumerated and converted | D2, D3, D4 | ADR merge | | C3 | Retire the seeders, the per-organization catalog machinery and the four catalog objects; built-ins and audience anchors as declared metadata; Setup catalog creation = environment metadata write under `single`, refused under a wall; platform-admin grant row owned by the Default Organization | D2, D3, D5, D13 | C1, C2 | | C4 | Templates: `sendTemplate` resolves the registry; seed and provenance stamp retired; door closed; customized-rows ruling applied | D6, D10 | ADR merge (+ §6 Q1) | From 0be9039d4ac5f43284c25019c4c00dcd367c0fc6 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Fri, 4 Sep 2026 01:56:25 +0800 Subject: [PATCH 13/24] =?UTF-8?q?docs(adr):=20ADR-0131=20=E2=80=94=20recor?= =?UTF-8?q?d=20the=202026-09-04=20acceptances=20(install-mode=20declaratio?= =?UTF-8?q?n,=20settings,=20grant=20row,=20overlay=20axis)=20and=20the=20p?= =?UTF-8?q?ost-17.2=20audit=20verdicts;=20one=20open=20question=20remains?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maintainer: 「接受你的建议」. Refs #13564 Co-Authored-By: Claude Fable 5.1 --- ...ation-ownership-no-null-organization-id.md | 53 ++++++++++++------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md index b176446ee8..4bb9710d6a 100644 --- a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md +++ b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md @@ -18,7 +18,8 @@ provenance rule for editing: 「可以先约定代码推送过来的元数据就 the sealing ruling that fixes D6: 「你这么说还不如先完全封死。flow 也先不让改。然后软件包应该有两种安装方式, 有一种是模版形式直接进库,那就是所有都可以修改。但是单库多租户禁止安装这种模版软件包;有一种是受管软件包,什么都以 软件包中的为准,就是不让改。」; and the catalog ruling that fixes D3: 「角色、岗位、权限集,Setup 里组织自建的是组织级。 -这个说的是单库单租户吧,单库多租户我可以禁止他们创建。但是你要支持我绑定到人员。」 +这个说的是单库单租户吧,单库多租户我可以禁止他们创建。但是你要支持我绑定到人员。」; and, on the proposals this record +carried as open questions and on the post-17.2 audit (§7), 「接受你的建议」(2026-09-04) — recorded per item below. **Builds on**: [ADR-0005](./0005-metadata-customization-overlay.md) (the metadata overlay — its environment layer is re-keyed by D6), [ADR-0049](./0049-no-unenforced-security-properties.md) (enforce or remove — the posture of D13), [ADR-0066](./0066-unified-authorization-model.md) D2 @@ -334,6 +335,10 @@ anchor retire with D13. No NULL grant row is ever produced. **Two install modes, and the mode is the whole customization story.** +The package **declares** the modes it permits (`installModes`, default `['managed']`); the installer picks one at +install time; a shared-DB multi-tenant deployment refuses `template` whatever the package permits (ruled +2026-09-04). + - **Managed** (default). The package is registered into the registry as code. Nothing in it is editable at runtime — not in Setup, not in Studio, not through the data or metadata API — and nothing in it can be switched off or cloned-with-linkage either: [ADR-0126](./0126-packaged-metadata-customization-model.md)'s @@ -402,8 +407,9 @@ rows no writer attributes to an organization (`sys_job`, `sys_job_run`, `sys_job (`sys_audit_log`, whose rows may concern deployment-level actions — the organization an audit row is *about* becomes a plain attribution field under a name the tenant-field resolver does not claim, never the tenancy anchor; cloud's `tenant_id` rule already reads it this way), and deployment-level runtime -settings — `sys_setting`'s global rung leaves the tenant-scoped table for configuration or a -tenant-less object. Such objects are governed by object permission, not by the wall. +settings — `sys_setting`'s global rung leaves the tenant-scoped table: infrastructure values go to +configuration; a tenant-less object holds only values an operator must change without a restart (ruled +2026-09-04). Such objects are governed by object permission, not by the wall. ⚠️ **Membership in this list is decided by the writer, not by the object's name.** The read-side ledger grouped `sys_http_delivery`, `sys_inbox_message`, `sys_notification*` and `sys_email` as "plumbing"; the @@ -618,26 +624,35 @@ compatibility shims for a shape nobody has used yet. ## 6. Open questions for the maintainer's merge decision -1. **Existing customized template rows** (D10): keep readable as the Default Organization's overrides - (builds the read half of the D6 door now), or accept the loss under the startup posture with a - release note? Depends on whether any deployment relies on the feature — this record cannot see that. -2. **`single`-posture first-user promotion** (D5): grant row owned by the Default Organization - (proposed), or configuration only (`OS_PLATFORM_OWNER_EMAIL` becomes mandatory for self-hosters)? -3. **Deployment-level settings** (D7): configuration file/environment, or a tenant-less - `sys_platform_setting` object? Proposed: configuration for infrastructure values, a tenant-less - object only for values an operator must change without a restart. -4. **Who chooses the install mode** (D6): proposed — the package **declares** the modes it permits - (`installModes: ['managed'] | ['managed','template'] | ['template']`, default `['managed']`), the - installer picks one at install time, and a shared-DB multi-tenant deployment refuses `template` - whatever the package permits. Confirm, or name a different rule. -5. **Retiring ADR-0005's per-organization overlay axis** (D6): it follows from the sealing ruling and - removes the need to split `sys_metadata`, but it switches off a live feature (org-scoped writes of - the five tier-A types, pinned by identity). Confirm. +Four of the five questions this draft carried were **accepted as proposed** on 2026-09-04 (「接受你的建议」) +and are now part of the decisions: the `single`-posture first-user grant row is owned by the Default +Organization (D5); the package **declares** the install modes it permits (`installModes`, default +`['managed']`), the installer picks one, and a shared-DB multi-tenant deployment refuses `template` +whatever the package permits (D6); deployment-level settings go to configuration, with a tenant-less +object only for values an operator must change without a restart (D7); ADR-0005's per-organization +overlay axis is retired (D6). One remains: ---- +1. **Existing customized template rows** (D10): keep readable as the Default Organization's overrides + (builds the read half of a copy-on-write door now), or accept the loss under the startup posture + with a release note? Depends on whether any deployment relies on the feature — this record cannot + see that; the maintainer rules it when the C4 card is cut. ## 7. Verification notes +**The post-17.2 audit (2026-09-04, maintainer: 「查 17.2 之后的修改就可以」).** Every merged change between the +17.2.0 tag (2026-08-23) and `origin/main` touching organization ownership was read, with a mechanical +scan of the unreleased diff for newly added NULL-tolerant reads (`organization_id: null`, `IS NULL`, +`orWhereNull`, `$or … null`); the scan's positive control is that it found the ADR-0126 machinery. +Verdicts: **revert before 17.3** — the ADR-0126 flow/action disable + clone machinery (#12185, #12190, +#12296, #12348, #12419, framework half of #12491; card #15024) and the NULL-inclusive business-unit +screen added by #14949 (its strict member screen stays; card #15030); **ship in 17.3, retire in v18 as +machinery** — the #13491 ledger (#13635, #13584), per-organization catalog resolution (#13818), +seed-ownership batching (#14718, #14687), ADR-0120 D3's COALESCE shadow (#13016), the capability lookup +batching (#11537), the deployment platform-global declaration (#12704); **consistent, keep** — every +stamp-and-backfill repair (#12929, #13180, #13527, #13572, #13565, #14726) and #14635, #13685, #14129; +**closed unmerged** — PR #14923. Pre-17.2 behaviour (Choice 4A's NULL grant row, the legacy grant anchor, +#10103 Option C, ADR-0005 overlays) is out of the audit's scope and moves in v18. + - The issue body of #13564 cites the arm at `sql-driver.ts:7320`; on `origin/main` `2514d49f3` the arms are inside `applyTenantScope` (~12016–12066) and there are **two**. This record cites symbols. - `ensure-default-organization.ts` cites "ADR-0081 D1" for the Default Organization; in this From 1fed6b7e58d2ec74f1fb57c95905aba28499471d Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Fri, 4 Sep 2026 09:25:41 +0800 Subject: [PATCH 14/24] =?UTF-8?q?docs(adr):=20ADR-0131=20D12=20=E2=80=94?= =?UTF-8?q?=20the=20group=20posture=20rule=20by=20rule:=20what=20ADR-0105?= =?UTF-8?q?=20keeps,=20what=20this=20record=20decides,=20the=20costs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maintainer 2026-09-04: 「你目前新的 ADR 有重新考虑集团版的规则吗?帮我具体列一下」. Refs #13564 Co-Authored-By: Claude Fable 5.1 --- ...ation-ownership-no-null-organization-id.md | 79 ++++++++++++++++--- 1 file changed, 69 insertions(+), 10 deletions(-) diff --git a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md index 4bb9710d6a..f8fb43f73b 100644 --- a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md +++ b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md @@ -484,16 +484,75 @@ Ownership rules, refusals and constraints are identical under `single`, `group` whether the wall is enforced differs. Degraded tenancy stays a refused boot. `single` is never a reason for a row to be org-less. -### D12 — The `group` posture: no group-shared business rows; the org-axis red lines stand - -[ADR-0105](./0105-group-tenancy-posture-and-first-class-org-scope.md) D10 — layered master data -(group template rows shared down an organization tree) — is **withdrawn** by the maintainer (verbatim: -「不考虑集团级模板行,作废相关需求」「不考虑 分层主数据」); ADR-0105 carries the note. Under `group` there -is exactly one kind of cross-organization visibility, membership union (D2 of that record); D6's red -lines (no permission inheritance along `parent_organization_id`; business-unit trees stay -org-internal) are untouched, and this record adds no sharing source of any kind. The catalog needs no -per-organization copies under `group` either — D2 applies; a headquarters-authored position is an -ordinary org-owned row of the headquarters. +### D12 — The `group` posture, rule by rule + +[ADR-0105](./0105-group-tenancy-posture-and-first-class-org-scope.md) defines `group` as "organizations = +membership boundaries over one shared dataset". This record does not redefine the posture; it states, +for each of its own decisions, what a group deployment gets. The maintainer asked for the list +(2026-09-04: 「你目前新的 ADR 有重新考虑集团版的规则吗?帮我具体列一下」). + +**Unchanged from ADR-0105, restated so nothing is inferred:** + +1. **The wall is membership union.** Reads are bounded by `organization_id IN accessible_org_ids` + (ADR-0105 D2), resolved from the caller's valid memberships; an empty set fails closed. A + headquarters analyst sees every plant they are a member of, on one screen. This is D8's one + predicate under `group`, threaded identically to Layer 0 and to every driver — and with **no NULL + arm**, so a plant's unstamped row can no longer be seen by the whole group. +2. **The active organization is the write target** (ADR-0105 D2/D5): a row written in plant A is owned + by plant A, NOT NULL (D1/D3). +3. **The org-axis red lines stand** (ADR-0105 D6): no permission inheritance along + `parent_organization_id`; business-unit trees stay inside one organization. This record adds no + sharing source and no tree walk (D12, above). +4. **Cross-organization approvals** (ADR-0105 D9): the request row belongs to the plant; a + headquarters approver reaches it through membership union or the system-context mirror; `$root` + resolution keeps its single chain walk. Unaffected. +5. **Delegated administration and scoped invitations** (ADR-0105 D8): unaffected. +6. **Layered master data** (ADR-0105 D10): **withdrawn** — 「不考虑集团级模板行,作废相关需求」「不考虑 + 分层主数据」. ADR-0105 carries the note. There are no group-shared business rows; a headquarters that + wants a plant to read its data makes the plant's users members of the organization that owns it. + +**What this record decides for `group`:** + +7. **One environment, one schema, one catalog.** A group deployment is one environment. Its managed + packages are sealed (D6); its Studio-authored metadata — objects, views, flows, positions, permission + sets, templates — is environment-wide and belongs to the deployment, not to any plant (D2/D6). The + metadata-authoring capability (`manage_metadata` / `studio.access`) is what headquarters IT holds and + plant admins do not. This is 集团统管 for roles by construction: the group defines positions and + permission sets once, every plant assigns from the same list (D3/D4). +8. **Plants assign; plants do not define.** A plant admin assigns its members to positions and + permission sets (org-owned rows, NOT NULL, by name — D3/D4), authors its business units, sharing + rules and record shares, and manages its data. It cannot create a position, a permission set, a + capability, a template or any metadata (D3, verbatim ruling 「单库多租户我可以禁止他们创建。但是你要支持我 + 绑定到人员」). A plant that needs its own catalog is given its own environment. +9. **No per-plant catalog copies.** #10103 Option C's one-pass-per-organization seeding under `group` + retires with the seeders (D2/D13); the catalog tables retire (D3). There is nothing to copy. +10. **Template packages are refused; managed only.** A shared-DB deployment accepts only the managed + install mode (D6): a template import would hand every plant an editable shared schema. +11. **No per-plant overlays and no per-plant template overrides.** ADR-0005's per-organization overlay + axis is retired (D6); a plant cannot re-word an email template or re-lay a view for itself. + Presentation customization per plant, if it is ever pulled for, returns as an org-owned overlay + object (D3 shape), never as a nullable column. +12. **Seeds under `group` must name their organization.** D9 derives an owner only under `single`. + Application seed datasets on a group deployment carry an explicit `organizationId` (the plant they + populate) or the load is refused; a system writer without an organization is refused, never + defaulted (D9). Boot seeds no longer exist (D2), so nothing runs before the organizations do. +13. **Platform standing is configuration.** Under `group` no grant row is written by first-user + promotion (D5); `OS_PLATFORM_OWNER_EMAIL` and the platform-exclusive capabilities decide who crosses + the wall (ADR-0095 D3, #12974 for the verified owner). Unchanged, restated. +14. **Deployment-level state has no organization column** (D7) — the environment metadata ledger, the + audit ledger, operational plumbing, deployment settings. Recipient-anchored objects (a person who is + a member of several plants has one inbox) are decided by their writer facts in the C7 inventory, + with cloud's reading — anchor on the person, not on the active organization — as the evidence on + file. +15. **Migration on a group database.** Existing NULL rows take D10's fates: mirrors deleted, business + rows attributed to the plant their parent anchor names, unattributable rows reported per table and + left NULL until an operator resolves them; the NOT NULL constraint lands per table only when that + table reports zero (D10). Nothing is attributed to a "group root" or to any default plant. + +**Costs specific to `group`, stated once:** a plant cannot define its own roles, templates or views +(items 8, 11); a group that wants HQ-authored business data visible in plants expresses it through +membership, not through a shared row (item 6). Both are the startup posture applied to the shared-DB +shape; the escape hatch is an environment per plant. ### D13 — Retirements (ADR-0049) From 3197adb7f6b2b2db8652c651f6b2dbb8e5b06fe8 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Fri, 4 Sep 2026 10:13:38 +0800 Subject: [PATCH 15/24] =?UTF-8?q?docs(adr):=20ADR-0131=20D13=20=E2=80=94?= =?UTF-8?q?=20sys=5Fview=5Fdefinition=20retired=20as=20inert=20(verified:?= =?UTF-8?q?=20no=20writer/reader=20in=20framework,=20never=20referenced=20?= =?UTF-8?q?by=20objectui);=20ADR-0017=20=C2=A73.4/=C2=A73.6=20amended?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maintainer 2026-09-04: 「现在就核实」. Refs #13564 Co-Authored-By: Claude Fable 5.1 --- docs/adr/0017-object-has-many-view.md | 2 +- ...ation-ownership-no-null-organization-id.md | 26 ++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/docs/adr/0017-object-has-many-view.md b/docs/adr/0017-object-has-many-view.md index 88e96df83a..9846cac62a 100644 --- a/docs/adr/0017-object-has-many-view.md +++ b/docs/adr/0017-object-has-many-view.md @@ -1,6 +1,6 @@ # ADR-0017: Object has-many View (Independent View Entities) -**Status**: Accepted · Implemented Phases 1–4 (2026-05-30) · Phase 5 (Studio designer) deferred +**Status**: Accepted · Implemented Phases 1–4 (2026-05-30) · Phase 5 (Studio designer) deferred · **Amended** (2026-09-04, [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D13 — §3.4's `sys_view_definition` store and §3.6's objectui repointing are **retired as inert**: verified on 2026-09-04 that no framework writer or reader of its rows exists, that `objectui` never referenced the object in its history (its runtime-view CRUD writes the ADR-0005 `view` overlay via `client.meta.saveItem`), and that a per-user view scope is a parked v18 direction. Runtime-authored views are environment metadata; §3.5's `GET /meta/view?object=` filter stands) **Deciders**: ObjectStack Protocol Architects **Builds on**: [ADR-0003](./0003-package-as-first-class-citizen.md) (package as first-class citizen), [ADR-0005](./0005-metadata-customization-overlay.md) (one Zod source per type, org overlay), [ADR-0010](./0010-metadata-protection-model.md) (L1/L2/L3 protection) **Consumers**: `@objectstack/spec`, `@objectstack/metadata`, `@objectstack/objectql`, `@objectstack/rest`, `@objectstack/platform-objects`, `../objectui` (runtime switcher + Studio designer) diff --git a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md index f8fb43f73b..4061a363fe 100644 --- a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md +++ b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md @@ -400,8 +400,8 @@ whole-item, `(organization, name, locale)`, *Reset to default* deletes it — ne Rows that belong to the deployment and not to an organization live in objects **without** the column (D1): the environment metadata ledger and its family (`sys_metadata`, `sys_metadata_audit`, -`sys_metadata_commit`, `sys_metadata_history`, environment-level `sys_view_definition` — D6; -`sys_metadata_activation` is reverted before 17.3 and does not return), operational plumbing whose +`sys_metadata_commit`, `sys_metadata_history` — D6; `sys_metadata_activation` is reverted before 17.3 and +does not return; `sys_view_definition` is not in this family — see D13), operational plumbing whose rows no writer attributes to an organization (`sys_job`, `sys_job_run`, `sys_job_queue`, `sys_flow_dispatch`, `sys_migration`, `sys_migration_journal`), the audit ledger (`sys_audit_log`, whose rows may concern deployment-level actions — the organization an audit row is @@ -565,7 +565,15 @@ and `email-template-provenance.ts`; the `__global__` sentinel and ADR-0120 D3's `platform-object-tenancy.ts` and `isPlatformObjectOutOfTenantAuditScope`; #12699's stand-down semantics (replaced by D7's no-column); ADR-0005's org-scoped write path for the five tier-A types; the catalog objects `sys_position`, `sys_permission_set`, `sys_position_permission_set` and -`sys_capability` (D3 — definitions live in the registry; ADR-0094's projection is no longer needed). +`sys_capability` (D3 — definitions live in the registry; ADR-0094's projection is no longer needed); +`sys_view_definition` with its two runtime index migrations (`view-definition-active-index.ts`, the +`runtime-index-preflight` row) and the `overlay-views-to-sys-view-definition` runbook — **verified inert +on 2026-09-04**: no writer and no reader exists in the framework, `objectui` has never referenced the +object in its history (its `createView` / `updateView` / `listViews` write ADR-0005 `view` overlays through +`client.meta.saveItem`), and cloud lists it only in permission tables. ADR-0017 §3.4/§3.6's runtime-view +store never acquired its consumer; runtime-authored views are environment metadata (D6) and a per-user +"personal view" scope stays the parked v18 direction objectui records — if pulled, it returns as a +user-owned D3 object, not as this table. ADR-0017 carries the amendment note. The packaged-flow disable/clone door and `sys_metadata_activation` are **not** on this list because they do not wait for protocol 18: unreleased, they are removed in C5 before the next release (D6). Each retirement of an authorable shape is an [ADR-0087](./0087-metadata-protocol-upgrade-contract.md) entry. @@ -698,6 +706,16 @@ overlay axis is retired (D6). One remains: ## 7. Verification notes +**`sys_view_definition` (2026-09-04, maintainer: 「现在就核实」).** Read across three repositories: the +framework holds the declaration, two runtime index migrations, a CLI migration allowlist entry, the +registry name and a runbook for migrating overlay views *into* it — and **no writer or reader of its rows**; +`objectui` `origin/main` has **zero** references and `git log -S sys_view_definition` is empty across its +history (its runtime-view CRUD writes the ADR-0005 `view` overlay via `client.meta.saveItem`; per-user +scope is recorded there as "a parked platform-side v18 direction"); cloud references it only in +`control-plane-permissions.ts` and the platform-global list. Verdict: not a projection — an ADR-0017 store +whose consumer never landed; retired under D13. The read-side ledger's "no direct read call site" was the +same fact seen from one repository. + **The post-17.2 audit (2026-09-04, maintainer: 「查 17.2 之后的修改就可以」).** Every merged change between the 17.2.0 tag (2026-08-23) and `origin/main` touching organization ownership was read, with a mechanical scan of the unreleased diff for newly added NULL-tolerant reads (`organization_id: null`, `IS NULL`, @@ -739,7 +757,7 @@ One epic tracks the family. C0 lands before 17.3; every other card is on the **v | C2 | Catalog resolution reads the registry; assignment tables reference by name (id→name columns + rewrite); dangling-name boot report; every reader of the four catalog tables enumerated and converted | D2, D3, D4 | ADR merge | | C3 | Retire the seeders, the per-organization catalog machinery and the four catalog objects; built-ins and audience anchors as declared metadata; Setup catalog creation = environment metadata write under `single`, refused under a wall; platform-admin grant row owned by the Default Organization | D2, D3, D5, D13 | C1, C2 | | C4 | Templates: `sendTemplate` resolves the registry; seed and provenance stamp retired; door closed; customized-rows ruling applied | D6, D10 | ADR merge (+ §6 Q1) | -| C5 | `sys_metadata` family tenant-less (environment definitions, UI-editable by metadata authors); per-organization overlay axis retired (org-scoped writes of the five tier-A types refused); managed content sealed — the unreleased packaged-flow disable/clone machinery and `sys_metadata_activation` **removed before the next release**; ADR-0005 and ADR-0126 amended | D6, D7 | C1 | +| C5 | `sys_metadata` family tenant-less (environment definitions, UI-editable by metadata authors); per-organization overlay axis retired (org-scoped writes of the five tier-A types refused); managed content sealed; `sys_view_definition` and its two runtime index migrations retired as inert (ADR-0087 entry; ADR-0017 amended); ADR-0005 amended | D6, D7, D13 | C1 | | C12 | Template install mode: manifest `installModes`, one-time import into the environment ledger with provenance, refusal on `group` / `isolated`, CLI + marketplace surfaces | D6 | C5 | | C6 | Deployment-level state has no column: settings global rung leaves `sys_setting`; plumbing objects drop the column; #12699 declaration made total | D7 | ADR merge (+ §6 Q3) | | C7 | Inventory + migration: four fates, id→name rewrite verified before mirror deletion, per-table boot report | D10 | C2, C3, C4, C5, C6 | From 5e175b075ff19d70b6d9a1b0a32e6862ffb31fc7 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Fri, 4 Sep 2026 10:26:01 +0800 Subject: [PATCH 16/24] =?UTF-8?q?docs(adr):=20ADR-0131=20D3=20=E2=80=94=20?= =?UTF-8?q?the=20position=E2=86=92permission-set=20binding=20has=20no=20de?= =?UTF-8?q?clared=20vocabulary=20today;=20add=20PositionSchema.permissionS?= =?UTF-8?q?ets,=20migrate=20junction=20rows=20into=20definitions,=20=C2=A7?= =?UTF-8?q?6=20Q2=20records=20the=20alternative=20reading?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified 2026-09-04: no binding key in PositionSchema/PermissionSetSchema; only isDefault→everyone (ADR-0090 D5); packages suggest, never own (D9); bindings exist only as sys_position_permission_set rows. Refs #13564 Co-Authored-By: Claude Fable 5.1 --- ...ation-ownership-no-null-organization-id.md | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md index 4061a363fe..0c9dbd3964 100644 --- a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md +++ b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md @@ -264,8 +264,18 @@ nothing left to copy per organization. **The catalog has one home.** Positions, permission sets and capabilities are **definitions**, and a definition lives only in the environment registry — code-declared (managed, sealed) or environment-authored (Studio, template package; editable by metadata-authoring capability holders). -The position → permission-set binding is part of the position's definition, declared in code or -authored in Studio. There is **no organization-level catalog**: the objects `sys_position`, +**The position → permission-set binding is part of the position's definition** — and today it is not +declared anywhere: `PositionSchema` and `PermissionSetSchema` carry no binding key (verified 2026-09-04; +the only declared relation is `isDefault`, which ADR-0090 D5 binds to the `everyone` position), packages +may only *suggest* a binding (ADR-0090 D9), and the binding exists solely as `sys_position_permission_set` +rows an admin accepted or created in Setup. Which sets a position carries is what the position **means**, +so under this record it is a definition, not an assignment: the spec gains `PositionSchema.permissionSets` +(the one new authoring key this record adds — a contract change, `needs:contract-review`), a code-declared +position names its sets there, a Studio-authored position names them in its definition, the `everyone` +baseline stays derived from `isDefault`, and an ADR-0090 D9 suggestion, when accepted, edits the position's +definition instead of inserting a row. Under `single` an admin binding a set to a position in Setup is an +environment metadata write; under a wall a plant admin cannot rebind a position (§6 Q2 records the +alternative reading). There is **no organization-level catalog**: the objects `sys_position`, `sys_permission_set`, `sys_position_permission_set` and `sys_capability` retire (D13), completing [ADR-0094](./0094-sys-permission-set-pure-projection.md) D1 — the metadata layer was already the sole authoritative store; the projected row no longer exists either. Consequences by posture, verbatim from @@ -314,7 +324,7 @@ environment-authored items share it, and Studio refuses a name a managed package Resolution reads the **registry** — one source, both provenances. `resolve-authz-context.ts` already looks positions up by name (`grants.positions`); the change is that the lookup is a registry read instead of a table read, and the position → permission-set binding is read from the position's -definition instead of a junction table. A name that resolves nowhere — a declaration removed or renamed +definition (`permissionSets`, D3) instead of a junction table. A name that resolves nowhere — a declaration removed or renamed in code — **fails closed** for that reference and is **reported at boot** per organization, by name. This is loud where today's behaviour is a zombie: a seeded mirror of a removed declaration stays in the table and keeps granting. @@ -635,6 +645,9 @@ compatibility shims for a shape nobody has used yet. - **Reference columns move from id to name** (D4): `sys_user_position`, `sys_user_permission_set`, sharing recipients, grants — a schema migration with an id→name rewrite over existing rows, and the verified rewrite is the precondition of D10 fate 2. +- **One new authoring key** (D3): `PositionSchema.permissionSets` — the binding had no declared vocabulary; + every existing `sys_position_permission_set` row is migrated into the position definitions it binds + (environment metadata under `single`; under a wall the residue is reported per organization, D10). - **Four catalog objects retire** (D3/D13): `sys_position`, `sys_permission_set`, `sys_position_permission_set`, `sys_capability`. Every reader of those tables — authorization resolution, hierarchy security, delegated admin, sharing recipients, the Setup pages — moves to the @@ -703,6 +716,12 @@ overlay axis is retired (D6). One remains: (builds the read half of a copy-on-write door now), or accept the loss under the startup posture with a release note? Depends on whether any deployment relies on the feature — this record cannot see that; the maintainer rules it when the C4 card is cut. +2. **Position → permission-set binding: definition or assignment?** (D3) This record reads "which sets + a position carries" as part of the position's definition — environment-level, `PositionSchema.permissionSets`, + plants cannot rebind — because the maintainer's rule is 「单库多租户禁止创建,但要支持绑定到人员」 and + rebinding changes what a role grants. The alternative reading keeps `sys_position_permission_set` as an + org-owned assignment table (position name, set name, organization NOT NULL): a plant composes its roles + from the environment's sets. That is per-plant role composition by another door; proposed: definition. ## 7. Verification notes @@ -755,7 +774,7 @@ One epic tracks the family. C0 lands before 17.3; every other card is on the **v | C0 | Revert the unreleased ADR-0126 flow machinery (#12296, #12419, #12156) so 17.3 does not publish it | D6, D14 | ADR merge; **before 17.3** | | C1 | Default Organization load-bearing under `single` and created before application seed datasets load; the seed loader stamps every seed (the `sys_` exemption withdrawn); `resolveSystemInsertOrganization` derives it, refuses elsewhere | D3, D9, D11 | ADR merge | | C2 | Catalog resolution reads the registry; assignment tables reference by name (id→name columns + rewrite); dangling-name boot report; every reader of the four catalog tables enumerated and converted | D2, D3, D4 | ADR merge | -| C3 | Retire the seeders, the per-organization catalog machinery and the four catalog objects; built-ins and audience anchors as declared metadata; Setup catalog creation = environment metadata write under `single`, refused under a wall; platform-admin grant row owned by the Default Organization | D2, D3, D5, D13 | C1, C2 | +| C3 | Retire the seeders, the per-organization catalog machinery and the four catalog objects; `PositionSchema.permissionSets` added and `sys_position_permission_set` rows migrated into definitions; built-ins and audience anchors as declared metadata; Setup catalog creation = environment metadata write under `single`, refused under a wall; platform-admin grant row owned by the Default Organization | D2, D3, D5, D13 | C1, C2 | | C4 | Templates: `sendTemplate` resolves the registry; seed and provenance stamp retired; door closed; customized-rows ruling applied | D6, D10 | ADR merge (+ §6 Q1) | | C5 | `sys_metadata` family tenant-less (environment definitions, UI-editable by metadata authors); per-organization overlay axis retired (org-scoped writes of the five tier-A types refused); managed content sealed; `sys_view_definition` and its two runtime index migrations retired as inert (ADR-0087 entry; ADR-0017 amended); ADR-0005 amended | D6, D7, D13 | C1 | | C12 | Template install mode: manifest `installModes`, one-time import into the environment ledger with provenance, refusal on `group` / `isolated`, CLI + marketplace surfaces | D6 | C5 | From 6566e912f7574c0555198a04244b830b49ed844b Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Fri, 4 Sep 2026 10:34:12 +0800 Subject: [PATCH 17/24] =?UTF-8?q?docs(adr):=20ADR-0131=20D3=20=E2=80=94=20?= =?UTF-8?q?position=E2=86=92permission-set=20binding=20is=20definition,=20?= =?UTF-8?q?ruled;=20the=20org-level=20binding=20table=20recorded=20as=20a?= =?UTF-8?q?=20rejected=20alternative?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maintainer 2026-09-04: 「ok」. Refs #13564 Co-Authored-By: Claude Fable 5.1 --- ...nization-ownership-no-null-organization-id.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md index 0c9dbd3964..d95f294c1a 100644 --- a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md +++ b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md @@ -274,8 +274,12 @@ so under this record it is a definition, not an assignment: the spec gains `Posi position names its sets there, a Studio-authored position names them in its definition, the `everyone` baseline stays derived from `isDefault`, and an ADR-0090 D9 suggestion, when accepted, edits the position's definition instead of inserting a row. Under `single` an admin binding a set to a position in Setup is an -environment metadata write; under a wall a plant admin cannot rebind a position (§6 Q2 records the -alternative reading). There is **no organization-level catalog**: the objects `sys_position`, +environment metadata write; under a wall a plant admin cannot rebind a position. **Ruled 2026-09-04** +(「ok」 to the reading that a position's sets are its definition): the alternative — an org-owned binding +table letting each plant compose roles from the environment's sets — is recorded and rejected in §5; the +escape valves are direct user ↔ permission-set assignment (`sys_user_permission_set`, organization-level), +a plant-specific position authored centrally, or a plant's own environment. There is **no +organization-level catalog**: the objects `sys_position`, `sys_permission_set`, `sys_position_permission_set` and `sys_capability` retire (D13), completing [ADR-0094](./0094-sys-permission-set-pure-projection.md) D1 — the metadata layer was already the sole authoritative store; the projected row no longer exists either. Consequences by posture, verbatim from @@ -698,6 +702,7 @@ compatibility shims for a shape nobody has used yet. | **A string sentinel (`__global__`) as owner** | Breaks the FK to `sys_organization`; every layer special-cases it forever (ADR-0120 D3's COALESCE is the running cost). | | **Declare a legitimately org-less write per call** (#13636 option B, draft PR #14923) | Makes NULL a *declared* state instead of removing it; every future writer of a conditionally-scoped object must know to declare; the column stays nullable, so the constraint D1 wants can never land. Superseded by taking the column off the objects whose rows are legitimately org-less (§1.6). | | **An organization-level catalog beside the environment one** (this record's second draft: tenant-created positions and sets as org-owned rows, pickers unioning registry and rows) | Two sources at selection time, a uniqueness check spanning both, a resolution order — three costs paid so that a tenant of a shared-DB deployment can define its own roles, a need no customer has stated. Deferred: if it arrives, it returns as an org-owned catalog object (the same shape), never as rows in a nullable-column table. | +| **Position → permission-set binding as an organization-level assignment table** (each plant composes roles from the environment's sets) | A position name is the public vocabulary approvals, sharing rules, reports and audits reference; per-plant rebinding gives one name N meanings, lets a plant admin silently widen a shared role for every holder in the plant, and puts a managed package's shipped bindings in conflict with the plant's. SAP master/derived roles, Salesforce permission-set groups (org = environment) and 集团统管 all keep role content central. Exceptions are expressed by assigning a set to a person, not by redefining the role. Rejected 2026-09-04. | | **Keep ADR-0126's overlay and disable + clone regimes live for managed content** | Each regime is a per-type customization mechanism with its own ledger, walls and UI; under the startup posture the maintainer chose one switch (the install mode) over three mechanisms. Regime E stays because it costs nothing (a package). Paused, not rejected: the regimes return only on a measured pull, and never through a nullable tenant column. | --- @@ -716,12 +721,7 @@ overlay axis is retired (D6). One remains: (builds the read half of a copy-on-write door now), or accept the loss under the startup posture with a release note? Depends on whether any deployment relies on the feature — this record cannot see that; the maintainer rules it when the C4 card is cut. -2. **Position → permission-set binding: definition or assignment?** (D3) This record reads "which sets - a position carries" as part of the position's definition — environment-level, `PositionSchema.permissionSets`, - plants cannot rebind — because the maintainer's rule is 「单库多租户禁止创建,但要支持绑定到人员」 and - rebinding changes what a role grants. The alternative reading keeps `sys_position_permission_set` as an - org-owned assignment table (position name, set name, organization NOT NULL): a plant composes its roles - from the environment's sets. That is per-plant role composition by another door; proposed: definition. + ## 7. Verification notes From 83cbeca9b54f014134eda84d6dec94f4fc18ba30 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Fri, 4 Sep 2026 10:42:56 +0800 Subject: [PATCH 18/24] =?UTF-8?q?docs(adr):=20ADR-0131=20D2/D3=20=E2=80=94?= =?UTF-8?q?=20two=20authoring=20doors:=20Studio=20(metadata=20capability)?= =?UTF-8?q?=20and=20Setup=20(security-admin=20authority,=20open=20under=20?= =?UTF-8?q?single,=20refused=20under=20a=20wall);=20Setup=20editor=20parit?= =?UTF-8?q?y=20is=20a=20C9=20acceptance=20criterion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #13564 Co-Authored-By: Claude Fable 5.1 --- ...ation-ownership-no-null-organization-id.md | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md index d95f294c1a..3bd8673546 100644 --- a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md +++ b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md @@ -244,8 +244,14 @@ It is hydrated into the same registry the code layer feeds (`loadMetaFromDb`), s that says "registry" means both provenances. The rule that separates them is provenance, verbatim: 「代码推送过来的元数据就只能在代码中修改;studio 界面上配置的元数据,保存在库中,可以在界面上修改」. Environment metadata belongs to the deployment, not to an organization: its ledger carries **no -organization column** (D6/D7), and writing it requires the metadata-authoring capability -(`manage_metadata` / `studio.access`), which `organization_admin` deliberately does not hold. +organization column** (D6/D7). It has **two authoring doors** with different gates. **Studio** — every +type — requires the metadata-authoring capability (`manage_metadata` / `studio.access`), which +`organization_admin` deliberately does not hold. **Setup** — the security catalog only (positions, +permission sets, capabilities) — keeps the security-administration authority an organization admin holds +today (the object grants on the catalog), and is **open under `single`** (the organization is the +environment) and **refused under a wall** (a plant is not the environment). This is what keeps a +single-tenant admin's Setup experience unchanged: the same page, the same editor, the write landing in the +environment ledger exactly as ADR-0094 D3 already redirects it today. A package reaches a deployment in one of **two install modes** (D6): **managed** — registered as code, sealed, upgradeable; or **template** — copied once into the environment ledger as environment @@ -285,10 +291,11 @@ organization-level catalog**: the objects `sys_position`, authoritative store; the projected row no longer exists either. Consequences by posture, verbatim from the ruling 「单库多租户我可以禁止他们创建。但是你要支持我绑定到人员」: -- **Single-tenant** (`single`): the organization is the environment. An admin who creates a position - or permission set in Setup performs an environment metadata write — the redirect ADR-0094 D3 - already makes today — gated by the metadata-authoring capability, which the deployment's owner holds - and grants as they see fit. +- **Single-tenant** (`single`): the organization is the environment. An admin who creates or edits a + position or permission set in Setup performs an environment metadata write — the redirect ADR-0094 D3 + already makes today — under the security-administration authority `organization_admin` already holds; + no Studio capability is required. Managed-package items stay locked as they have been since + 2026-08-24 (clone to customize). - **Shared-DB multi-tenant** (`group` / `isolated`): tenant admins **cannot create or edit catalog items**; the operator defines the catalog for every tenant (managed packages, Studio). Creation through Setup or the API is refused with a message naming the posture and the capability. @@ -659,6 +666,10 @@ compatibility shims for a shape nobody has used yet. larger population and are enumerated by C2. - **Setup changes shape** (D7): its catalog pages become registry views (editors under `single`, read-only under a wall); its assignment pages stay data pages. No server-side merge is built. + ⚠️ **Parity is an acceptance criterion, not a hope**: the permission-set page keeps its list views, + filters, the object × CRUD / field-level matrix editor and the active/inactive switch (the `active` + row state moves into the environment-authored definition's lifecycle, since there is no row). A JSON + textarea in place of the matrix editor fails C9. - **Tenants of a shared-DB deployment lose catalog authoring** they nominally have today (creating a position or set in Setup). They keep assignment. A tenant that needs its own catalog gets its own environment. From b6da63a63096dc022b99d9f6745f6c252fa27657 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Fri, 4 Sep 2026 10:50:29 +0800 Subject: [PATCH 19/24] =?UTF-8?q?chore:=20drop=20the=20empty=20changeset?= =?UTF-8?q?=20=E2=80=94=20docs-only=20PRs=20carry=20the=20skip-changeset?= =?UTF-8?q?=20label=20instead=20(#4898)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- .changeset/adr-0131-total-organization-ownership.md | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .changeset/adr-0131-total-organization-ownership.md diff --git a/.changeset/adr-0131-total-organization-ownership.md b/.changeset/adr-0131-total-organization-ownership.md deleted file mode 100644 index 658ce4c268..0000000000 --- a/.changeset/adr-0131-total-organization-ownership.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Docs-only: adds ADR-0131 (organization ownership is total — no NULL `organization_id`; declared -metadata stays in code and is never seeded; a row exists only when an organization authored it; -references by name; deployment-level state has no organization column) and annotates ADR-0105 D10 -as withdrawn. Releases nothing — no package changes. The ADR is Proposed; implementation cards are -cut from it after the maintainer's hand-merge (Prime Directive #14), never ahead of it. Refs #13564. From 02a6f991a613f68a68fb7c2efcd4e16f20ffc090 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Fri, 4 Sep 2026 10:51:12 +0800 Subject: [PATCH 20/24] =?UTF-8?q?docs(adr):=20ADR-0131=20D6=20=E2=80=94=20?= =?UTF-8?q?how=20far=20clone-to-customize=20reaches,=20by=20type;=20behavi?= =?UTF-8?q?oural=20items=20are=20not=20customizable=20under=20managed;=20t?= =?UTF-8?q?he=20supersede=20shape=20recorded=20for=20a=20future=20pull?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #13564 Co-Authored-By: Claude Fable 5.1 --- ...zation-ownership-no-null-organization-id.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md index 3bd8673546..c58e5bc89e 100644 --- a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md +++ b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md @@ -380,6 +380,24 @@ Creating one's own item — a new permission set, position, view, flow — is en capability and refused to tenants of a shared-DB deployment (D3). It is not customization of a managed item; pre-filling the form from a managed item is a UI convenience that records no linkage. +**How far "lock the base, clone to customize" reaches, by type** (maintainer, 2026-09-04: 「是不是可以复制到 +各种元数据类型,包括流程?克隆再定制之后,之前软件包中的元数据还需要停用吗?」). Locking the base reaches every +type — that is D6. Cloning reaches only the types whose items are **inert until referenced**: + +| Type family | Clone-to-customize under managed | Does the base need switching off? | +|:--|:--|:--| +| Referenced by name — permission sets, positions, capabilities, templates | **Yes.** Create your own, assign or reference the clone. | **No.** An unassigned set or an unreferenced template does nothing. | +| Presentational — views, dashboards, reports, pages | **Partly.** The clone can be used, but the platform lists every view bound to an object (the switcher), so the base stays visible beside the clone. | Hiding it needs an overlay (retired) or a switch (sealed) — so under managed the base remains; a template install is the clean answer. | +| Self-triggering behaviour — flows, workflows, validation rules, scheduled jobs, object-bound actions | **No.** A clone fires **beside** the base: two flows on one record change, two validations on one save. | It would — and that is the disable half this record seals. Under managed such an item is **not customizable**: the vendor exposes a setting in the package, the customer installs as a template, or asks for a version. | +| Structural — objects, fields, app navigation | Extend (Regime E). | n/a — an extension adds, it does not replace. | + +The third row is why ADR-0126 paired clone with disable for flows, and why sealing removed both together +rather than one. If a measured pull for customizing managed behaviour returns, the smallest reopening is +**supersede declared on the environment clone** — `supersedes: ` in the clone's own definition, +the registry treating the managed item as shadowed whole, environment-level, by a metadata author. That is +a declaration in the environment ledger, not an activation ledger and not a per-field merge. Recorded here +so the shape is known; not decided. + **Code-provenance metadata is edited in code.** No runtime door edits a managed item. **Environment-provenance metadata is edited in the UI.** What Studio, the cloud build agent or a From dc254c08db95cb4924896f0cbad0752048f0307e Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Fri, 4 Sep 2026 11:09:31 +0800 Subject: [PATCH 21/24] =?UTF-8?q?docs(adr):=20ADR-0131=20D6=20=E2=80=94=20?= =?UTF-8?q?managed=20definitions=20sealed,=20ADR-0126's=20regimes=20stand?= =?UTF-8?q?=20at=20environment=20scope=20(amended,=20not=20superseded);=20?= =?UTF-8?q?the=20standard-package=20scenario;=20C0=20re-scoped=20to=20the?= =?UTF-8?q?=20column=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maintainer 2026-09-04: 「所以 ADR-0126 不能简单的关掉」「hotcrm 是标准的软件包,客户安装之后能根据业务需求实现具体的 定制」「同意」. Refs #13564 Co-Authored-By: Claude Fable 5.1 --- ...6-packaged-metadata-customization-model.md | 4 +- ...ation-ownership-no-null-organization-id.md | 242 +++++++++--------- 2 files changed, 128 insertions(+), 118 deletions(-) diff --git a/docs/adr/0126-packaged-metadata-customization-model.md b/docs/adr/0126-packaged-metadata-customization-model.md index 7ab6a702ce..8529d0063a 100644 --- a/docs/adr/0126-packaged-metadata-customization-model.md +++ b/docs/adr/0126-packaged-metadata-customization-model.md @@ -1,6 +1,6 @@ # ADR-0126: The platform customization model for packaged metadata — three regimes, one activation ledger -**Status**: **Superseded** (2026-09-04) by [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D6 — maintainer, verbatim: 「ADR-0126 可以先作废」. The regimes below are replaced by the install mode (managed = sealed, template = copied in and editable); the activation ledger and the packaged-flow disable/clone machinery are reverted before 17.3 (never released). Kept as the record of the survey (ADR-0131 §1.7 summarizes it). Original status line follows. +**Status**: **Amended** (2026-09-04) by [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D6 — maintainer, verbatim: 「所以 ADR-0126 不能简单的关掉」「hotcrm 是标准的软件包,客户安装之后能根据业务需求实现具体的定制」「同意」. The three regimes stand at **environment scope only**: O's per-organization reading is retired (ADR-0005 amended), C's activation ledger stays as tenant-less deployment state (D3's reserved `organization_id`, written NULL, is withdrawn — ADR-0131 D1 forbids a nullable tenant column), E stands; the unreleased implementation (#12185, #12190, #12296, #12348, #12419, framework half of #12491) ships in 17.3 with that column dropped. The install mode ADR-0131 D6 adds (managed = sealed definitions + these regimes; template = copied once, fully editable, refused on shared-DB multi-tenant) is the frame the regimes now sit in. Original status line follows. **Original status**: Proposed (2026-08-25) — awaiting the maintainer's hand-merge, which is itself the acceptance act for a governed surface (Prime Directive #14) **and the ruling that settles the tentative flow-instance directions in §7** (chartered on @@ -41,7 +41,7 @@ re-cited inline; the survey carries the instruments and positive controls. --- -> **Superseded 2026-09-04 by [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D6.** Maintainer ruling, verbatim and untranslated: 「你这么说还不如先完全封死。flow 也先不让改。然后软件包应该有两种安装方式,有一种是模版形式直接进库,那就是所有都可以修改。但是单库多租户禁止安装这种模版软件包;有一种是受管软件包,什么都以软件包中的为准,就是不让改。」 Regimes **O** (overlay) and **C** (disable + clone) are **paused for managed content — packaged flows (§7) included**; customization is expressed by the **install mode** instead: *managed* (sealed, upgradeable; the only mode a shared-DB multi-tenant deployment accepts) or *template* (copied once into the environment ledger, fully editable, no upgrade channel, refused on `group` / `isolated`). D3's reserved `organization_id` on `sys_metadata_activation` is withdrawn (ADR-0131 D1: no nullable tenant column); the Regime C machinery landed for flows (#12158 / PR #12296, #12156, #12419) reached `main` after the last release (17.2.0, 2026-08-23) and is **removed before the next release** — nothing published depends on it; epic #12150 closes as superseded. Regime **E** stands. §7 remains the contract the regimes consume **if** they return. +> **Amended 2026-09-04 by [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D6** (see the Status line): regimes O, C and E stand at environment scope; the per-organization axis and D3's reserved `organization_id` are withdrawn; the activation ledger is tenant-less deployment state; §7 remains the contract the flow machinery implements. A third draft of ADR-0131 marked this record Superseded; the standard-package scenario (「hotcrm 是标准的软件包,客户安装之后能根据业务需求实现具体的定制」) reversed that the same day. ## TL;DR diff --git a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md index c58e5bc89e..b2559aa620 100644 --- a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md +++ b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md @@ -3,7 +3,6 @@ **Status**: Proposed (2026-09-04) — awaiting the maintainer's hand-merge, which is itself the acceptance act for a governed surface (Prime Directive #14). ⛔ Nothing below is settled until this record merges; the implementation cards are cut **from** the merged ADR, never ahead of it. -**Supersedes**: [ADR-0126](./0126-packaged-metadata-customization-model.md) (maintainer, 2026-09-04: 「ADR-0126 可以先作废」— its regimes are replaced by the install mode of D6, its activation ledger is removed before 17.3; §1.7 keeps what its survey found so the superseded record need not be read). **Deciders**: ObjectStack maintainer, 2026-09-03/04, live chat on [#13564](https://github.com/objectstack-ai/objectstack/issues/13564), verbatim and untranslated, in the order the model was built: the premise 「我理解只有代码定义的元数据是跨租户的,对象、字段、视图等 @@ -19,7 +18,10 @@ the sealing ruling that fixes D6: 「你这么说还不如先完全封死。flow 有一种是模版形式直接进库,那就是所有都可以修改。但是单库多租户禁止安装这种模版软件包;有一种是受管软件包,什么都以 软件包中的为准,就是不让改。」; and the catalog ruling that fixes D3: 「角色、岗位、权限集,Setup 里组织自建的是组织级。 这个说的是单库单租户吧,单库多租户我可以禁止他们创建。但是你要支持我绑定到人员。」; and, on the proposals this record -carried as open questions and on the post-17.2 audit (§7), 「接受你的建议」(2026-09-04) — recorded per item below. +carried as open questions and on the post-17.2 audit (§7), 「接受你的建议」(2026-09-04) — recorded per item below; +finally the reversal of the sealing draft once the standard-package scenario was put on the table: +「所以 ADR-0126 不能简单的关掉」「hotcrm 是标准的软件包,客户安装之后能根据业务需求实现具体的定制」「同意」 +(2026-09-04) — D6 as it now stands. **Builds on**: [ADR-0005](./0005-metadata-customization-overlay.md) (the metadata overlay — its environment layer is re-keyed by D6), [ADR-0049](./0049-no-unenforced-security-properties.md) (enforce or remove — the posture of D13), [ADR-0066](./0066-unified-authorization-model.md) D2 @@ -79,7 +81,10 @@ else. The maintainer's model is simpler and this record adopts it whole: 1. **Metadata is the registry, and the registry has two provenances.** A **managed** package — objects, fields, views, and equally positions, permission sets, capabilities, sharing rules, templates, flows — is code: edited **in code only**, changed by publishing a new version, **never - materialized as rows**, and **sealed** at runtime (no overlay, no disable, no clone). Metadata + materialized as rows**, and its **definitions are sealed** at runtime. What a customer changes is + environment-level state beside them — an overlay of a presentational item, an on/off switch on a + behavioural item, a clone, an extension (D6) — so a standard package stays customizable **and** + upgradeable. Metadata authored at runtime — in Studio, by the cloud build agent, or by installing a **template** package (a one-time copy into the database) — is **saved in the database and edited in the UI**; it is the environment's, not any organization's, and its ledger carries **no organization column**. Template @@ -98,8 +103,8 @@ else. The maintainer's model is simpler and this record adopts it whole: Consequences: NULL has nothing left to mean (D1); the driver's NULL arm and Layer 0's strict equality stop disagreeing because there is one predicate (D8); a forgotten stamp is a refused write (D9); the seeders, the per-organization catalog machinery and the #13491 ledger retire (D13); -customization of managed content is **sealed** — ADR-0126's overlay and disable + clone regimes are -paused, and ADR-0005's per-organization overlay axis is retired, so `sys_metadata` needs no split (D6). +managed definitions are sealed while ADR-0126's three customization regimes stand at environment +scope; ADR-0005's per-organization overlay axis is retired, so `sys_metadata` needs no split (D6). Startup posture throughout: the stable answer, not the feature-complete one. --- @@ -196,7 +201,7 @@ plain data field, not the tenancy anchor (D7). Once the column is gone there is a declared-NULL mechanism would keep alive exactly the state D1 removes. #14923 therefore does not merge; #13636 closes as superseded when this record merges (§8, C11). -### 1.7 What ADR-0126 found, kept here so the superseded record need not be read +### 1.7 What ADR-0126 found, and what this record keeps of it ADR-0126 (2026-08-25) surveyed every metadata type for post-install customization pull and found three mechanisms already in the tree, each invented per type with its own ledger: an **organization @@ -206,10 +211,11 @@ sets (the 2026-08-24 lock-the-base ruling), and **package-grain extend** for obj with one click, then customize in Studio" while the platform refused nearly every post-install change; that a tenant had once switched a shipped flow off environment-wide through an unscoped in-process map (#10243); and that behavioral types must never gain an org overlay (the #6190 wall). Its answer was a -per-type regime table plus a generic activation ledger. This record keeps the findings and replaces the -answer: the extend mechanism stands because it is a package; the #10243 class is closed by sealing -managed content rather than by a durable switch; and the documentation promise is rewritten as "install -managed, or install as a template" (C11). +per-type regime table plus a generic activation ledger. This record keeps both, at **environment scope +only**: the regimes stand (D6), the ledger stays as deployment-level state without its reserved +organization column (D7), and the documentation promise becomes "install managed and customize at the +environment level, or install as a template" (C11). A third draft of this record sealed managed content +completely; the standard-package scenario reversed it (§5). --- @@ -352,88 +358,90 @@ with a NULL organization, writes it **owned by the Default Organization** instea posture no grant row is written (unchanged). `reportLegacyPlatformAdminGrant` and the legacy unscoped anchor retire with D13. No NULL grant row is ever produced. -### D6 — Managed is sealed; template is copied in and fully editable; the environment ledger has no organization column - -**Two install modes, and the mode is the whole customization story.** - -The package **declares** the modes it permits (`installModes`, default `['managed']`); the installer picks one at -install time; a shared-DB multi-tenant deployment refuses `template` whatever the package permits (ruled -2026-09-04). - -- **Managed** (default). The package is registered into the registry as code. Nothing in it is - editable at runtime — not in Setup, not in Studio, not through the data or metadata API — and - nothing in it can be switched off or cloned-with-linkage either: [ADR-0126](./0126-packaged-metadata-customization-model.md)'s - Regime O (overlay) and Regime C (disable + clone) are **paused for managed content, packaged flows - included** (verbatim: 「先完全封死。flow 也先不让改」). To change a managed item, the vendor publishes a - new version and upgrades flow to the base untouched. Regime E (extend) stands, because an extension - is itself a package. Managed is the **only** mode a shared-DB multi-tenant deployment (`group` / - `isolated`) accepts. +### D6 — Managed definitions are sealed; customization is environment-level state beside them, never an edit and never per organization + +**Two install modes.** The package **declares** the modes it permits (`installModes`, default +`['managed']`); the installer picks one at install time; a shared-DB multi-tenant deployment refuses +`template` whatever the package permits (ruled 2026-09-04). + +- **Managed** (default). The package is registered into the registry as code. **No door edits a managed + definition** — not Setup, not Studio, not the data or metadata API; to change one, the vendor publishes a + version and upgrades reach it untouched. What the customer changes lives **beside** the managed item, as + environment-level state that upgrades never touch — the three regimes of + [ADR-0126](./0126-packaged-metadata-customization-model.md), kept and re-homed at environment scope: + - **O — overlay** (the five presentational types: `view`, `dashboard`, `report`, `translation`, + `email_template`): an environment row of the **same name** replaces the managed item **whole** — + ADR-0005's environment layer, exactly as today, with the per-organization axis retired. The customer + re-lays the lead list or re-words the welcome mail; the vendor's later change to that one item is + shadowed until the customer drops the overlay (Salesforce's custom layout has the same cost). + - **C — disable + clone** (behavioural types: `flow`, `action`; pre-charted `workflow`, validation, + scheduled job): **disable** is one `active` bit of deployment-level state in the tenant-less activation + ledger `sys_metadata_activation` (ADR-0126 D2, without D3's reserved organization column — D7); + **clone** is a whole-definition sibling under a **new name** with no linkage to the base (ADR-0126 §7.1, + amendment ruling 2). The two are independent primitives: switch a managed automation off; or clone it, + change the clone, and switch the base off. Operator-gated under a wall (ADR-0126 D3). + - **E — extend** (`object`, `app`): fields, validations, navigation arrive as a package. + Managed is the **only** mode a shared-DB multi-tenant deployment (`group` / `isolated`) accepts, and every + regime there is environment-wide and operator-gated — a plant admin neither overlays, switches nor clones. - **Template.** The package's metadata is copied **once** into the environment definition ledger as - environment-provenance items — from then on it is the environment's own metadata, editable in Studio - like anything authored there, with no upgrade channel (a later version is a new import, refused where - names collide; provenance is recorded for information only). ⛔ **Refused on `group` / `isolated`** - with a message naming the posture: a template import would hand every tenant an editable shared - schema. Under `single` it is how a customer takes an app and makes it theirs. + environment-provenance items — from then on it is the environment's own metadata, editable in Studio like + anything authored there, with no upgrade channel (a later version is a new import, refused where names + collide; provenance recorded for information only). ⛔ **Refused on `group` / `isolated`** with a message + naming the posture: a template import would hand every tenant an editable shared schema. Under `single` + it is how a customer takes an app and makes it theirs wholesale — Salesforce's unmanaged package. Creating one's own item — a new permission set, position, view, flow — is environment authoring -(Studio, or Setup acting as a metadata editor under `single`), gated by the metadata-authoring -capability and refused to tenants of a shared-DB deployment (D3). It is not customization of a -managed item; pre-filling the form from a managed item is a UI convenience that records no linkage. - -**How far "lock the base, clone to customize" reaches, by type** (maintainer, 2026-09-04: 「是不是可以复制到 -各种元数据类型,包括流程?克隆再定制之后,之前软件包中的元数据还需要停用吗?」). Locking the base reaches every -type — that is D6. Cloning reaches only the types whose items are **inert until referenced**: - -| Type family | Clone-to-customize under managed | Does the base need switching off? | -|:--|:--|:--| -| Referenced by name — permission sets, positions, capabilities, templates | **Yes.** Create your own, assign or reference the clone. | **No.** An unassigned set or an unreferenced template does nothing. | -| Presentational — views, dashboards, reports, pages | **Partly.** The clone can be used, but the platform lists every view bound to an object (the switcher), so the base stays visible beside the clone. | Hiding it needs an overlay (retired) or a switch (sealed) — so under managed the base remains; a template install is the clean answer. | -| Self-triggering behaviour — flows, workflows, validation rules, scheduled jobs, object-bound actions | **No.** A clone fires **beside** the base: two flows on one record change, two validations on one save. | It would — and that is the disable half this record seals. Under managed such an item is **not customizable**: the vendor exposes a setting in the package, the customer installs as a template, or asks for a version. | -| Structural — objects, fields, app navigation | Extend (Regime E). | n/a — an extension adds, it does not replace. | - -The third row is why ADR-0126 paired clone with disable for flows, and why sealing removed both together -rather than one. If a measured pull for customizing managed behaviour returns, the smallest reopening is -**supersede declared on the environment clone** — `supersedes: ` in the clone's own definition, -the registry treating the managed item as shadowed whole, environment-level, by a metadata author. That is -a declaration in the environment ledger, not an activation ledger and not a per-field merge. Recorded here -so the shape is known; not decided. - -**Code-provenance metadata is edited in code.** No runtime door edits a managed item. +(Studio, or Setup acting as a metadata editor under `single`), gated as D2/D3 describe and refused to +tenants of a shared-DB deployment. It is not customization of a managed item; pre-filling the form from a +managed item is a UI convenience that records no linkage. + +**What each customer need maps to** (the standard-package scenario, maintainer 2026-09-04: 「hotcrm 是标准的 +软件包,客户安装之后能根据业务需求实现具体的定制」): + +| The customer wants to… | Mechanism | The managed item | Upgrades | +|:--|:--|:--|:--| +| add a field, a validation, a menu entry | E — extension package | untouched | flow | +| change the lead list's columns and filters | O — environment overlay, same name | shadowed, untouched | flow; that view's vendor changes are shadowed | +| re-word the welcome e-mail | O — `email_template` overlay | shadowed | flow | +| stop "send SMS on lead conversion" | C — disable | off, untouched | flow; stays off | +| run their own approval flow instead of the vendor's | C — clone under a new name + disable the base | off | flow to the base; the customer re-clones when they want the new logic | +| define their own roles | Setup / Studio authoring (D3) | managed sets read-only, clonable | flow | +| flip a business option the vendor exposed | a setting in the package | n/a | flow | +| delete or rename a vendor object or field | **not possible** — hide through views and permissions | untouched | flow | + +Every row changes **which item is in effect** in this environment, never the managed definition. That is +the managed-package contract customers already know. + +**Templates.** A managed e-mail or notification template renders from the registry and is immutable; the +environment may overlay it (O) or author its own; there is **no per-organization template override** (the +door the maintainer chose not to open: 「我宁可先不让他编辑」) — if a measured organization-level pull ever +returns, it opens as copy-on-write, one org-owned row, never as a seed. **Environment-provenance metadata is edited in the UI.** What Studio, the cloud build agent or a template install wrote into `sys_metadata` is the environment's to change there, by anyone holding the -metadata-authoring capability. This is [ADR-0005](./0005-metadata-customization-overlay.md)'s +authoring authority the two doors of D2 describe. This is [ADR-0005](./0005-metadata-customization-overlay.md)'s environment layer, kept — and made the **whole** ledger: **`sys_metadata` is the environment definition ledger and carries no organization column** (D1/D7). -**ADR-0005's per-organization overlay axis is retired for now.** The five `allowOrgOverride` types -(`view`, `dashboard`, `report`, `translation`, `email_template`) were the only reason `sys_metadata` -carried an organization. With managed content sealed and template content environment-owned there is -no per-organization metadata left to hold: under `single` the organization *is* the environment; under -a wall no metadata is editable at all. So there is **no split and no second object** — org-scoped -metadata writes are refused (the existing identity pin flips from "exactly five types accepted" to -"none"), and the environment layer is the ledger. ADR-0005 is amended (status note); its "no overlay -row = the registry" reading and its layered resolution (environment → code) are unchanged. If a -measured pull for per-organization presentational overlays returns, it comes back as an org-owned -overlay object holding D3 rows — never as a nullable column on the environment ledger. - -**What this does to ADR-0126.** Its three regimes were the answer to "how is packaged metadata -customized without being edited"; the maintainer's answer is now "it is not — pick the install mode". -Regime O and Regime C are paused for managed content. The Regime C machinery that landed for packaged -flows — the activation ledger `sys_metadata_activation` and its `execute()`-time consult (#12158, PR -#12296), the flow clone action (#12156), the ledger convergence (#12419) — reached `main` on and after -2026-08-26, **after the last release** (17.2.0, tagged 2026-08-23; npm serves 17.2.0). Nothing published -depends on it, so it is **removed before 17.3 is cut** — a revert of #12296, #12419 and the clone action (#12156) — not sealed and carried -([ADR-0049](./0049-no-unenforced-security-properties.md): a shape nobody may use is not shipped -dormant). Clone without disable would in any case be worse than nothing — a cloned flow fires beside the -managed one it copied. Epic #12150 closes as superseded. ADR-0126 D3's "org column reserved, written -NULL" is withdrawn with the ledger. Regime E stands. ADR-0126 carries the amendment note. - -**Templates** follow the same two modes: a managed template renders from the registry and is immutable; -a Studio-authored or template-installed template is environment metadata, editable in Studio. There is -no per-organization template override (the door the maintainer chose not to open: 「我宁可先不让他编辑」). -If a measured organization-level pull ever returns, it opens as **copy-on-write** — one org-owned row, -whole-item, `(organization, name, locale)`, *Reset to default* deletes it — never as a seed. +**ADR-0005's per-organization overlay axis is retired for now.** The five `allowOrgOverride` types were the +only reason `sys_metadata` carried an organization. With customization environment-level and template +content environment-owned there is no per-organization metadata left to hold: under `single` the +organization *is* the environment; under a wall no plant customizes. So there is **no split and no second +object** — org-scoped metadata writes are refused (the existing identity pin flips from "exactly five types +accepted" to "none"), and the environment layer is the ledger. ADR-0005 is amended (status note); its "no +overlay row = the registry" reading and its layered resolution (environment → code) are unchanged. If a +measured pull for per-organization presentational overlays returns, it comes back as an org-owned overlay +object holding D3 rows — never as a nullable column on the environment ledger. + +**What this does to ADR-0126.** It is **amended, not superseded**: its three regimes stand, at environment +scope only; its per-type assignment (§3) stands, with the O row's per-organization reading gone; its +activation ledger stands as tenant-less deployment state (D7), D3's "org column reserved, written NULL" +withdrawn — a reserved nullable tenant column is the shape D1 forbids; its D3 operator gate under a wall +stands and is the same rule as D12 item 7. The unreleased machinery (#12185, #12190, #12296, #12348, +#12419, #12491) **ships in 17.3** once the reserved column is dropped (C0). ADR-0126 carries the amendment +note. The lesson of the maintainer's prior platform is honoured where it applies: an overlay replaces an +item **whole**, never field by field; there is no per-organization second layer; no list merges two +sources server-side. ### D7 — Deployment-level state has no organization column; the UI never merges sources server-side @@ -553,8 +561,9 @@ for each of its own decisions, what a group deployment gets. The maintainer aske **What this record decides for `group`:** 7. **One environment, one schema, one catalog.** A group deployment is one environment. Its managed - packages are sealed (D6); its Studio-authored metadata — objects, views, flows, positions, permission - sets, templates — is environment-wide and belongs to the deployment, not to any plant (D2/D6). The + packages' definitions are sealed and every customization — overlay, switch, clone, extension — is + environment-wide and operator-gated (D6); its Studio-authored metadata — objects, views, flows, + positions, permission sets, templates — belongs to the deployment, not to any plant (D2/D6). The metadata-authoring capability (`manage_metadata` / `studio.access`) is what headquarters IT holds and plant admins do not. This is 集团统管 for roles by construction: the group defines positions and permission sets once, every plant assigns from the same list (D3/D4). @@ -567,8 +576,9 @@ for each of its own decisions, what a group deployment gets. The maintainer aske retires with the seeders (D2/D13); the catalog tables retire (D3). There is nothing to copy. 10. **Template packages are refused; managed only.** A shared-DB deployment accepts only the managed install mode (D6): a template import would hand every plant an editable shared schema. -11. **No per-plant overlays and no per-plant template overrides.** ADR-0005's per-organization overlay - axis is retired (D6); a plant cannot re-word an email template or re-lay a view for itself. +11. **No per-plant overlays, switches or template overrides.** ADR-0005's per-organization overlay + axis is retired (D6) and the activation ledger is environment-level; a plant cannot re-word an email + template, re-lay a view or switch off a flow for itself — headquarters does it for the environment. Presentation customization per plant, if it is ever pulled for, returns as an org-owned overlay object (D3 shape), never as a nullable column. 12. **Seeds under `group` must name their organization.** D9 derives an owner only under `single`. @@ -613,29 +623,29 @@ object in its history (its `createView` / `updateView` / `listViews` write ADR-0 store never acquired its consumer; runtime-authored views are environment metadata (D6) and a per-user "personal view" scope stays the parked v18 direction objectui records — if pulled, it returns as a user-owned D3 object, not as this table. ADR-0017 carries the amendment note. -The packaged-flow disable/clone door and `sys_metadata_activation` are **not** on this list because they -do not wait for protocol 18: unreleased, they are removed in C5 before the next release (D6). Each -retirement of an authorable shape is an [ADR-0087](./0087-metadata-protocol-upgrade-contract.md) entry. +`sys_metadata_activation` is **not** on this list: it stays as the tenant-less activation ledger of +D6/D7; only its reserved organization column goes (C0, before 17.3). Each retirement of an authorable +shape is an [ADR-0087](./0087-metadata-protocol-upgrade-contract.md) entry. -### D14 — Staging: one pre-17.3 removal, everything else on the v18 line +### D14 — Staging: one pre-17.3 column fix, everything else on the v18 line Maintainer, 2026-09-04: 「我发 17.3,然后后续这么大的改动应该放到 v18」. Two consequences: -- **Before 17.3 is cut**, the unreleased ADR-0126 flow machinery is reverted (D6; C0). Nothing else of - this record ships in 17.x. The #13491 tenant-audit ledger (#13635, also unreleased) **may** ship in - 17.3: it is internal protection with no authorable surface, refuses org-less system writes on walled - postures, and retires in v18 as machinery, not as a contract. +- **Before 17.3 is cut**, `sys_metadata_activation` drops its reserved `organization_id` + (`systemFields.tenant: false`; C0) so the unreleased ADR-0126 machinery ships tenant-less, and the + NULL-inclusive business-unit screen of #14949 is reverted (#15030). Nothing else of this record ships in + 17.x. The #13491 tenant-audit ledger (#13635, also unreleased) **may** ship in 17.3: internal protection + with no authorable surface, retired in v18 as machinery. - **Everything else is the v18 line** — one major, one migration: D1's constraint, D2/D3's retirements, - D4's name references, D6's sealing and template mode, D7's column drops, D8's single predicate, D9's - refusals, D10's four-fate migration, D13's retirements. Within the line the order of §8 still holds - (C1–C6 before C7 before C8), and the migration keeps its per-table gate (D10) — the customer database - is never asked to satisfy a constraint its report has not cleared. The driver arms are removed in the - same major, after C7. ⛔ No 17.x card narrows or removes an arm, adds a name column beside an id - column, or ships a half of this record. + D4's name references, D6's template mode and the retirement of the per-organization overlay axis, D7's + column drops, D8's single predicate, D9's refusals, D10's four-fate migration, D13's retirements. Within + the line the order of §8 holds (C1–C6 before C7 before C8), and the migration keeps its per-table gate + (D10). The driver arms are removed in the same major, after C7. ⛔ No 17.x card narrows or removes an + arm, adds a name column beside an id column, or ships a half of this record. -Doing it in one major rather than additively across 17.x avoids carrying dual id/name columns, a -registry-first-then-rows resolution, and a sealed-but-present flow ledger through a public release — -compatibility shims for a shape nobody has used yet. +Doing it in one major rather than additively across 17.x avoids carrying dual id/name columns and a +registry-first-then-rows resolution through a public release — compatibility shims for a shape nobody +has used yet. --- @@ -644,8 +654,8 @@ compatibility shims for a shape nobody has used yet. - **Changing what a tenant can see or do.** Declared items remain visible to every organization (they are code); organizations keep creating their own items and cloning declared ones. Only the mechanism moves. -- **Opening any editing door.** D6 closes template editing and adds no new customization surface; - copy-on-write is the *shape* a future door takes, not a commitment to build one. +- **Editing a managed definition.** No mechanism in this record edits managed content; customization is + environment-level state beside it (D6). Per-organization customization of any kind stays closed. - **A platform organization** in any form (§1.5, §5). - **Group-level shared business rows** (D12; ADR-0105 D10 withdrawn, not deferred). - **better-auth-managed tables** — no tenant column; untouched. @@ -699,9 +709,8 @@ compatibility shims for a shape nobody has used yet. read in `metadata-protocol` (`getMetaItem`), `meta-write-org-scope.ts`, the ADR-0094 write-through and `sys-metadata-repository.ts` all simplify. A live feature is switched off; the maintainer confirms it in §6 Q6. -- **Managed content is sealed** (D6): ADR-0126's disable/clone machinery for packaged flows (#12158, - #12156, #12419) is removed before the next release — it never shipped (landed after the 17.2.0 tag), - so recently landed work is deleted rather than published dormant. +- **Managed definitions are sealed; ADR-0126's regimes stand at environment scope** (D6): the + unreleased disable/clone machinery ships in 17.3 with its reserved organization column dropped (C0). - **The template install mode is new work** (D6): a manifest declaration of permitted modes, a one-time importer into the environment ledger, and the posture refusal on `group` / `isolated`. - Tests pinning NULL semantics and seeded rows are rewritten (`deal_p1`, `sql-driver-tenant-scope`, @@ -732,7 +741,7 @@ compatibility shims for a shape nobody has used yet. | **Declare a legitimately org-less write per call** (#13636 option B, draft PR #14923) | Makes NULL a *declared* state instead of removing it; every future writer of a conditionally-scoped object must know to declare; the column stays nullable, so the constraint D1 wants can never land. Superseded by taking the column off the objects whose rows are legitimately org-less (§1.6). | | **An organization-level catalog beside the environment one** (this record's second draft: tenant-created positions and sets as org-owned rows, pickers unioning registry and rows) | Two sources at selection time, a uniqueness check spanning both, a resolution order — three costs paid so that a tenant of a shared-DB deployment can define its own roles, a need no customer has stated. Deferred: if it arrives, it returns as an org-owned catalog object (the same shape), never as rows in a nullable-column table. | | **Position → permission-set binding as an organization-level assignment table** (each plant composes roles from the environment's sets) | A position name is the public vocabulary approvals, sharing rules, reports and audits reference; per-plant rebinding gives one name N meanings, lets a plant admin silently widen a shared role for every holder in the plant, and puts a managed package's shipped bindings in conflict with the plant's. SAP master/derived roles, Salesforce permission-set groups (org = environment) and 集团统管 all keep role content central. Exceptions are expressed by assigning a set to a person, not by redefining the role. Rejected 2026-09-04. | -| **Keep ADR-0126's overlay and disable + clone regimes live for managed content** | Each regime is a per-type customization mechanism with its own ledger, walls and UI; under the startup posture the maintainer chose one switch (the install mode) over three mechanisms. Regime E stays because it costs nothing (a package). Paused, not rejected: the regimes return only on a measured pull, and never through a nullable tenant column. | +| **Seal managed content completely — no overlay, no disable, no clone** (this record's third draft, 「先完全封死」) | A standard package's customer could then change nothing without a vendor release or a template install that forfeits upgrades — not even switch off an unwanted automation. The standard-package scenario (install HotCRM, customize to the business, keep upgrading) is the product; sealing *definitions* while keeping environment-level *choices* — which view, which switch, which clone — is what managed packages mean everywhere. Rejected 2026-09-04 (「所以 ADR-0126 不能简单的关掉」「同意」). | --- @@ -768,9 +777,10 @@ same fact seen from one repository. 17.2.0 tag (2026-08-23) and `origin/main` touching organization ownership was read, with a mechanical scan of the unreleased diff for newly added NULL-tolerant reads (`organization_id: null`, `IS NULL`, `orWhereNull`, `$or … null`); the scan's positive control is that it found the ADR-0126 machinery. -Verdicts: **revert before 17.3** — the ADR-0126 flow/action disable + clone machinery (#12185, #12190, -#12296, #12348, #12419, framework half of #12491; card #15024) and the NULL-inclusive business-unit -screen added by #14949 (its strict member screen stays; card #15030); **ship in 17.3, retire in v18 as +Verdicts: **fix before 17.3** — the ADR-0126 machinery (#12185, #12190, #12296, #12348, #12419, framework +half of #12491) ships with its reserved organization column dropped (card #15024, re-scoped from a revert +after the standard-package reversal); **revert before 17.3** — the NULL-inclusive business-unit screen added +by #14949 (its strict member screen stays; card #15030); **ship in 17.3, retire in v18 as machinery** — the #13491 ledger (#13635, #13584), per-organization catalog resolution (#13818), seed-ownership batching (#14718, #14687), ADR-0120 D3's COALESCE shadow (#13016), the capability lookup batching (#11537), the deployment platform-global declaration (#12704); **consistent, keep** — every @@ -796,16 +806,16 @@ stamp-and-backfill repair (#12929, #13180, #13527, #13572, #13565, #14726) and # ## 8. Execution plan (cards are cut from the merged record) -One epic tracks the family. C0 lands before 17.3; every other card is on the **v18** line (D14), in this order. +One epic tracks the family. C0 and #15030 land before 17.3; every other card is on the **v18** line (D14), in this order. | # | Card | Decisions | Blocked by | |---|---|---|---| -| C0 | Revert the unreleased ADR-0126 flow machinery (#12296, #12419, #12156) so 17.3 does not publish it | D6, D14 | ADR merge; **before 17.3** | +| C0 | Drop the reserved `organization_id` from `sys_metadata_activation` (`systemFields.tenant: false`) so 17.3 ships the activation ledger tenant-less | D6, D7, D14 | ADR merge; **before 17.3** | | C1 | Default Organization load-bearing under `single` and created before application seed datasets load; the seed loader stamps every seed (the `sys_` exemption withdrawn); `resolveSystemInsertOrganization` derives it, refuses elsewhere | D3, D9, D11 | ADR merge | | C2 | Catalog resolution reads the registry; assignment tables reference by name (id→name columns + rewrite); dangling-name boot report; every reader of the four catalog tables enumerated and converted | D2, D3, D4 | ADR merge | | C3 | Retire the seeders, the per-organization catalog machinery and the four catalog objects; `PositionSchema.permissionSets` added and `sys_position_permission_set` rows migrated into definitions; built-ins and audience anchors as declared metadata; Setup catalog creation = environment metadata write under `single`, refused under a wall; platform-admin grant row owned by the Default Organization | D2, D3, D5, D13 | C1, C2 | | C4 | Templates: `sendTemplate` resolves the registry; seed and provenance stamp retired; door closed; customized-rows ruling applied | D6, D10 | ADR merge (+ §6 Q1) | -| C5 | `sys_metadata` family tenant-less (environment definitions, UI-editable by metadata authors); per-organization overlay axis retired (org-scoped writes of the five tier-A types refused); managed content sealed; `sys_view_definition` and its two runtime index migrations retired as inert (ADR-0087 entry; ADR-0017 amended); ADR-0005 amended | D6, D7, D13 | C1 | +| C5 | `sys_metadata` family tenant-less (environment definitions, UI-editable by metadata authors); per-organization overlay axis retired (org-scoped writes of the five tier-A types refused); `sys_view_definition` and its two runtime index migrations retired as inert (ADR-0087 entry; ADR-0017 amended); ADR-0005 amended | D6, D7, D13 | C1 | | C12 | Template install mode: manifest `installModes`, one-time import into the environment ledger with provenance, refusal on `group` / `isolated`, CLI + marketplace surfaces | D6 | C5 | | C6 | Deployment-level state has no column: settings global rung leaves `sys_setting`; plumbing objects drop the column; #12699 declaration made total | D7 | ADR merge (+ §6 Q3) | | C7 | Inventory + migration: four fates, id→name rewrite verified before mirror deletion, per-table boot report | D10 | C2, C3, C4, C5, C6 | From a2882a90acaac4a92adda211f37799369b8bd013 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Fri, 4 Sep 2026 11:28:13 +0800 Subject: [PATCH 22/24] =?UTF-8?q?docs(adr):=20ADR-0131=20D10/D14=20?= =?UTF-8?q?=E2=80=94=20the=20v18=20migration=20is=20a=20manual=20operator?= =?UTF-8?q?=20ceremony=20(plan=20/=20backup=20/=20apply=20/=20post-check)?= =?UTF-8?q?=20with=20a=20boot=20refusal=20behind=20it,=20never=20an=20auto?= =?UTF-8?q?matic=20boot=20step?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maintainer 2026-09-04: 「我建议 18.0 的主要考虑是客户数据变化比较大,而且需要手工执行升级脚本。」 Refs #13564 Co-Authored-By: Claude Fable 5.1 --- ...ation-ownership-no-null-organization-id.md | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md index b2559aa620..3e98eaf721 100644 --- a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md +++ b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md @@ -525,6 +525,35 @@ Template rows an admin has customized (`customized: true`) are the one populatio rule on (§6 Q1): keep them readable as the Default Organization's overrides (half of the D6 door), or accept the loss under the startup posture with a release note. +**The migration is a manual, operator-run ceremony — never an automatic boot step.** Maintainer, +2026-09-04, verbatim: 「我建议 18.0 的主要考虑是客户数据变化比较大,而且需要手工执行升级脚本。」 The v18 +change rewrites customer data at a scale no boot may perform silently — column drops and mirror-row +deletions are irreversible, attribution rewrites ownership, the constraint locks the shape — so it ships +as an `os migrate`-family command the operator runs, in this order and with these guarantees: + +1. **Preflight, read-only** (`--plan`): per table, the fate, the row counts each fate will touch, the rows + whose owner cannot be derived (listed by id), and the tables that will and will not receive the NOT + NULL constraint. The plan is written to a file the operator keeps; nothing is changed. ⛔ A plan that + cannot enumerate a table refuses instead of reporting it as empty. +2. **Backup is the operator's act and the precondition** the apply step asks about explicitly; the + platform does not take backups for the operator and does not assume one exists. +3. **Apply** (`--apply`) executes the plan in fate order — attribution before mirror deletion (D10 fate + 2's gate is the verified rewrite), column drops last — **idempotent and resumable**: a second run + after an interruption continues from the recorded checkpoint, never re-derives owners already written. +4. **Post-check** re-runs the plan and prints zero-remaining per table; the constraint lands only on + tables that read zero, exactly as D10 states. Tables still reporting NULL rows are named with the + remedy and stay unconstrained until the operator resolves them and re-runs. +5. **A v18 runtime refuses to boot against a database the ceremony has not completed** — the same + fail-fast shape as [ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md) D5 — naming the + command to run; it never auto-migrates, never runs degraded on a half-migrated schema, and offers no + escape hatch that silently skips the check (an operator who wants to run 17.x semantics runs 17.x). + +The ceremony reuses the [ADR-0120](./0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md) +D4 migration ceremony where it exists (index and column changes) rather than inventing a second one. +On cloud each customer environment is its own database: the ceremony runs per environment, and the +control plane's own database is one more environment — cloud's release runbook carries the sequence +(C10). Documentation for self-hosters (C11) leads with this ceremony, not with the feature list. + ### D11 — Postures differ only in enforcement Ownership rules, refusals and constraints are identical under `single`, `group` and `isolated`; only @@ -640,8 +669,10 @@ Maintainer, 2026-09-04: 「我发 17.3,然后后续这么大的改动应该放 D4's name references, D6's template mode and the retirement of the per-organization overlay axis, D7's column drops, D8's single predicate, D9's refusals, D10's four-fate migration, D13's retirements. Within the line the order of §8 holds (C1–C6 before C7 before C8), and the migration keeps its per-table gate - (D10). The driver arms are removed in the same major, after C7. ⛔ No 17.x card narrows or removes an - arm, adds a name column beside an id column, or ships a half of this record. + (D10). The driver arms are removed in the same major, after C7. **The v18 upgrade is a manual + operator ceremony with a boot refusal behind it (D10) — the release notes and the self-hoster docs lead + with it.** ⛔ No 17.x card narrows or removes an arm, adds a name column beside an id column, or ships + a half of this record. Doing it in one major rather than additively across 17.x avoids carrying dual id/name columns and a registry-first-then-rows resolution through a public release — compatibility shims for a shape nobody @@ -687,6 +718,9 @@ has used yet. - **One new authoring key** (D3): `PositionSchema.permissionSets` — the binding had no declared vocabulary; every existing `sys_position_permission_set` row is migrated into the position definitions it binds (environment metadata under `single`; under a wall the residue is reported per organization, D10). +- **The v18 upgrade is a manual operator ceremony** (D10): plan → backup → apply → post-check, per-table + gate, resumable; a v18 runtime refuses to boot until it has run. Every self-hoster and every cloud + environment pays one operator session; the price of not paying it silently at boot. - **Four catalog objects retire** (D3/D13): `sys_position`, `sys_permission_set`, `sys_position_permission_set`, `sys_capability`. Every reader of those tables — authorization resolution, hierarchy security, delegated admin, sharing recipients, the Setup pages — moves to the @@ -818,7 +852,7 @@ One epic tracks the family. C0 and #15030 land before 17.3; every other card is | C5 | `sys_metadata` family tenant-less (environment definitions, UI-editable by metadata authors); per-organization overlay axis retired (org-scoped writes of the five tier-A types refused); `sys_view_definition` and its two runtime index migrations retired as inert (ADR-0087 entry; ADR-0017 amended); ADR-0005 amended | D6, D7, D13 | C1 | | C12 | Template install mode: manifest `installModes`, one-time import into the environment ledger with provenance, refusal on `group` / `isolated`, CLI + marketplace surfaces | D6 | C5 | | C6 | Deployment-level state has no column: settings global rung leaves `sys_setting`; plumbing objects drop the column; #12699 declaration made total | D7 | ADR merge (+ §6 Q3) | -| C7 | Inventory + migration: four fates, id→name rewrite verified before mirror deletion, per-table boot report | D10 | C2, C3, C4, C5, C6 | +| C7 | Inventory + the manual migration ceremony (`os migrate` family: plan / apply / post-check, idempotent + resumable, per-table constraint gate) and the v18 boot refusal against an un-migrated database | D10 | C2, C3, C4, C5, C6 | | C8 | One predicate; NOT NULL per cleared table; every-posture refusal; both arms and the ledger retired — protocol 18 | D1, D8, D9, D13, D14 | C7 | | C9 | objectui: Setup catalog pages read the registry (editor under `single`, read-only under a wall); assignment pages stay data pages; pickers list the registry; Studio lists and edits environment metadata | D3, D7 | C2 | | C10 | cloud: control plane and objectos-ee adopt the record (per-deployment no-column, backfill, tests) | D7, D8, D10 | C6, then C8 | From 697617985fc2b90d72065b1da3654ddbda6c2513 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Fri, 4 Sep 2026 11:46:46 +0800 Subject: [PATCH 23/24] =?UTF-8?q?docs(adr):=20ADR-0131=20=C2=A77=20?= =?UTF-8?q?=E2=80=94=20cite=20the=20drifted=20line=20as=20a=20number,=20no?= =?UTF-8?q?t=20a=20file:line=20anchor=20(check-adr-symbol-anchors)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- ...131-total-organization-ownership-no-null-organization-id.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md index 3e98eaf721..ce8465e9b6 100644 --- a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md +++ b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md @@ -822,7 +822,8 @@ stamp-and-backfill repair (#12929, #13180, #13527, #13572, #13565, #14726) and # **closed unmerged** — PR #14923. Pre-17.2 behaviour (Choice 4A's NULL grant row, the legacy grant anchor, #10103 Option C, ADR-0005 overlays) is out of the audit's scope and moves in v18. -- The issue body of #13564 cites the arm at `sql-driver.ts:7320`; on `origin/main` `2514d49f3` the +- The issue body of #13564 cites the arm by a bare line number (7320) that had drifted by ~4,600 lines + before the first census read it; on `origin/main` `2514d49f3` the arms are inside `applyTenantScope` (~12016–12066) and there are **two**. This record cites symbols. - `ensure-default-organization.ts` cites "ADR-0081 D1" for the Default Organization; in this repository `docs/adr/0081` is the trusted React page tier — the "0081" is cloud's numbering. The From 82b2ba9e20f38f65c1898bf6b57d26cb328138ea Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Fri, 4 Sep 2026 12:31:00 +0800 Subject: [PATCH 24/24] =?UTF-8?q?docs(adr):=20ADR-0131=20=C2=A78=20?= =?UTF-8?q?=E2=80=94=20#14096=20ruling:=20the=20seed=20paths'=20namespace?= =?UTF-8?q?=20exemption=20is=20withdrawn=20(C1);=20its=20eight=20objects?= =?UTF-8?q?=20are=20C7's=20first=20named=20fate-3=20members?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maintainer 2026-09-04: 「同意」. Refs #13564 #14096 Co-Authored-By: Claude Fable 5.1 --- ...31-total-organization-ownership-no-null-organization-id.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md index ce8465e9b6..2f93aacc22 100644 --- a/docs/adr/0131-total-organization-ownership-no-null-organization-id.md +++ b/docs/adr/0131-total-organization-ownership-no-null-organization-id.md @@ -846,14 +846,14 @@ One epic tracks the family. C0 and #15030 land before 17.3; every other card is | # | Card | Decisions | Blocked by | |---|---|---|---| | C0 | Drop the reserved `organization_id` from `sys_metadata_activation` (`systemFields.tenant: false`) so 17.3 ships the activation ledger tenant-less | D6, D7, D14 | ADR merge; **before 17.3** | -| C1 | Default Organization load-bearing under `single` and created before application seed datasets load; the seed loader stamps every seed (the `sys_` exemption withdrawn); `resolveSystemInsertOrganization` derives it, refuses elsewhere | D3, D9, D11 | ADR merge | +| C1 | Default Organization load-bearing under `single` and created before application seed datasets load; the seed loader and `seed-tenancy-backfill` stamp every seed (the `sys_` / `cloud_` / `ai_` regexp exemption withdrawn — #14096, ruled 2026-09-04); `resolveSystemInsertOrganization` derives it, refuses elsewhere | D3, D9, D11 | ADR merge | | C2 | Catalog resolution reads the registry; assignment tables reference by name (id→name columns + rewrite); dangling-name boot report; every reader of the four catalog tables enumerated and converted | D2, D3, D4 | ADR merge | | C3 | Retire the seeders, the per-organization catalog machinery and the four catalog objects; `PositionSchema.permissionSets` added and `sys_position_permission_set` rows migrated into definitions; built-ins and audience anchors as declared metadata; Setup catalog creation = environment metadata write under `single`, refused under a wall; platform-admin grant row owned by the Default Organization | D2, D3, D5, D13 | C1, C2 | | C4 | Templates: `sendTemplate` resolves the registry; seed and provenance stamp retired; door closed; customized-rows ruling applied | D6, D10 | ADR merge (+ §6 Q1) | | C5 | `sys_metadata` family tenant-less (environment definitions, UI-editable by metadata authors); per-organization overlay axis retired (org-scoped writes of the five tier-A types refused); `sys_view_definition` and its two runtime index migrations retired as inert (ADR-0087 entry; ADR-0017 amended); ADR-0005 amended | D6, D7, D13 | C1 | | C12 | Template install mode: manifest `installModes`, one-time import into the environment ledger with provenance, refusal on `group` / `isolated`, CLI + marketplace surfaces | D6 | C5 | | C6 | Deployment-level state has no column: settings global rung leaves `sys_setting`; plumbing objects drop the column; #12699 declaration made total | D7 | ADR merge (+ §6 Q3) | -| C7 | Inventory + the manual migration ceremony (`os migrate` family: plan / apply / post-check, idempotent + resumable, per-table constraint gate) and the v18 boot refusal against an un-migrated database | D10 | C2, C3, C4, C5, C6 | +| C7 | Inventory + the manual migration ceremony (`os migrate` family: plan / apply / post-check, idempotent + resumable, per-table constraint gate) and the v18 boot refusal against an un-migrated database. First named fate-3 members (the #14096 census, ruled 2026-09-04): `sys_file`, `sys_upload_session`, `sys_approval_request`, `sys_approval_action`, `sys_approval_approver`, `sys_automation_run`, `sys_notification_delivery`, `sys_record_share` | D10 | C2, C3, C4, C5, C6 | | C8 | One predicate; NOT NULL per cleared table; every-posture refusal; both arms and the ledger retired — protocol 18 | D1, D8, D9, D13, D14 | C7 | | C9 | objectui: Setup catalog pages read the registry (editor under `single`, read-only under a wall); assignment pages stay data pages; pickers list the registry; Studio lists and edits environment metadata | D3, D7 | C2 | | C10 | cloud: control plane and objectos-ee adopt the record (per-deployment no-column, backfill, tests) | D7, D8, D10 | C6, then C8 |