From cf29316fb0389712b0beb9b31e2a8de99fdabbd2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 23:07:38 +0000 Subject: [PATCH] docs(spec): organizationField pin annotation reflects the #10101 landing Refresh the #8778 scope-pin annotation and the .describe() beside tenancy.organizationField in packages/spec/src/data/object.zod.ts. The annotation (landed by #10999) said consumer 1 (audit stamping) was "still the only one wired up" and consumers 2/3 (approvals, automation runs) were "sanctioned but not yet implemented". #10101's PR #11311 (merged 2026-08-23) landed both: resolveRecordOrganizationField was promoted to a shared resolver in @objectstack/metadata-core and all three sanctioned platform-row writers now call it. Also corrects the resolver-home naming ("plugin-audit's resolveRecordOrganizationField" -> @objectstack/metadata-core's, plugin-audit re-exports). Prose accuracy only: the pin's load-bearing "exactly THREE consumers, and no others" sentence and the fourth-consumer refusal posture are unchanged verbatim. No accept/reject behaviour change, no schema shape change, no new keys. --- ...tion-field-pin-annotation-prose-refresh.md | 21 ++++++++ content/docs/references/data/object.mdx | 2 +- packages/spec/src/data/object.zod.ts | 49 +++++++++++-------- 3 files changed, 51 insertions(+), 21 deletions(-) create mode 100644 .changeset/organization-field-pin-annotation-prose-refresh.md diff --git a/.changeset/organization-field-pin-annotation-prose-refresh.md b/.changeset/organization-field-pin-annotation-prose-refresh.md new file mode 100644 index 0000000000..d405717e95 --- /dev/null +++ b/.changeset/organization-field-pin-annotation-prose-refresh.md @@ -0,0 +1,21 @@ +--- +'@objectstack/spec': patch +--- + +Refresh the `tenancy.organizationField` scope-pin annotation and its +`.describe()` in `packages/spec/src/data/object.zod.ts` — prose accuracy +only, no schema or behaviour change. + +The #8778 scope-pin annotation (landed by #10999) said consumer 1 (audit +stamping) was "as of this annotation, still the only one wired up" and that +consumers 2 and 3 (the approval-row writer and the automation-run recorder) +were "sanctioned but not yet implemented". #10101's PR #11311 (merged +2026-08-23) landed both: `resolveRecordOrganizationField` was promoted to a +shared resolver in `@objectstack/metadata-core` (plugin-audit re-exports it +from its original path) and all three sanctioned platform-row writers now +call it. The annotation and the `.describe()` string are updated to name all +three live consumers; the pin's load-bearing property — "The ruling sanctions +exactly THREE consumers of this key, and no others", a fourth consumer needs +its own ruling — is unchanged and still stated verbatim. + +No accept/reject behaviour change, no schema shape change, no new keys. diff --git a/content/docs/references/data/object.mdx b/content/docs/references/data/object.mdx index 32f001de13..b4531354bc 100644 --- a/content/docs/references/data/object.mdx +++ b/content/docs/references/data/object.mdx @@ -314,7 +314,7 @@ Boolean-or-predicates override for a built-in CRUD affordance. | :--- | :--- | :--- | :--- | | **enabled** | `boolean` | ✅ | Enable multi-tenancy for this object | | **tenantField** | `string` | optional | Column this object is tenant-scoped by. Omit it unless the tenant column genuinely is not the platform's: when undeclared the driver falls back to `organization_id`, the kernel-injected column the RLS predicates and `tenantPolicy()` also assume. A declared name is honoured only when the object really has that field — otherwise the same `organization_id` fallback applies. No default is materialized here on purpose (#5315). | -| **organizationField** | `string` | optional | STAMP-ONLY (#8778): column carrying the organization a row is ABOUT, consulted exclusively when audit rows are stamped. It does NOT tenant-scope anything — no read path (`applyTenantScope`, `injectTenantOnInsert`, `computeTenantLayer0Filter`) reads it, so declaring it never walls the object and never hides rows. Declare it only when the organization a row belongs to lives under a column that deliberately is NOT the tenant column: `sys_api_key` is the shipped example — a credential table that must stay unwalled (`enabled: false`) while history/revocation audit rows stamp the organization of the key they describe (`active_organization_id`). Ordinary tenant objects omit it; their stamp column is resolved from `tenantField` / `organization_id` already. Honoured only when the object really has the field, like `tenantField`. | +| **organizationField** | `string` | optional | STAMP-ONLY (#8778, widened by cloud#1395): column carrying the organization a row is ABOUT, consulted by the three sanctioned platform-row writers — audit stamping, the approval-row writer (`plugin-approvals`), and the automation-run recorder (`service-automation`) — via the shared `resolveRecordOrganizationField` resolver in `@objectstack/metadata-core`. It does NOT tenant-scope anything — no read path (`applyTenantScope`, `injectTenantOnInsert`, `computeTenantLayer0Filter`) reads it, so declaring it never walls the object and never hides rows. Declare it only when the organization a row belongs to lives under a column that deliberately is NOT the tenant column: `sys_api_key` is the shipped example — a credential table that must stay unwalled (`enabled: false`) while history/revocation audit rows stamp the organization of the key they describe (`active_organization_id`). Ordinary tenant objects omit it; their stamp column is resolved from `tenantField` / `organization_id` already. Honoured only when the object really has the field, like `tenantField`. | --- diff --git a/packages/spec/src/data/object.zod.ts b/packages/spec/src/data/object.zod.ts index fbbdec604d..d8a18902ad 100644 --- a/packages/spec/src/data/object.zod.ts +++ b/packages/spec/src/data/object.zod.ts @@ -591,12 +591,14 @@ const TENANCY_MODES_EXPLAINER = * credential tables they deliberately do not — `sys_api_key` records the * organization a key authenticates into under `active_organization_id` * precisely so the credential table does NOT become org-walled (#8287). The - * key is consulted exclusively by audit stamping (plugin-audit's - * `resolveRecordOrganizationField`); no read path reads it, and that + * key is consulted only by platform-row stamping — the three sanctioned + * writers below, via `@objectstack/metadata-core`'s + * `resolveRecordOrganizationField` (plugin-audit re-exports it from its + * original path; public surface unchanged) — never by a read path, and that * read-neutrality is pinned by tests beside each read path. ⛔ Scope-pinned by * the #8778 ruling: this is ONE stamp-only declaration key, not the opening - * move of a general field-roles mechanism — a consumer other than audit - * stamping needs its own ruling before reading it. + * move of a general field-roles mechanism — a consumer other than the three + * sanctioned writers needs its own ruling before reading it. * * That pin is WIDENED **by name** by the maintainer ruling recorded on * cloud#1395, 2026-08-17T03:18Z, accepting the decision-inbox recommendations @@ -611,19 +613,22 @@ const TENANCY_MODES_EXPLAINER = * * The ruling sanctions exactly THREE consumers of this key, and no others: * - * 1. **audit stamping** — plugin-audit's `resolveRecordOrganizationField`; - * the original #8778 consumer and, as of this annotation, still the only - * one wired up; - * 2. **`plugin-approvals`** — the approval-row writer; - * 3. **the automation-run recorder** — reached when - * `resolveRecordOrganizationField` is promoted to the shared platform-row - * resolver. - * - * Consumers 2 and 3 are sanctioned but not yet implemented: #10101 carries that - * behaviour change (this card is annotation-only and changes no accept/reject - * behaviour). Which is why the `.describe()` below still speaks of audit rows — - * it states what reads the key TODAY, and #10101 updates it as the readers - * actually land. + * 1. **audit stamping** — `@objectstack/metadata-core`'s + * `resolveRecordOrganizationField`; the original #8778 consumer + * (plugin-audit re-exports it from its original path; public surface + * unchanged); + * 2. **`plugin-approvals`** — the approval-row writer (`openNodeRequest`, + * the only `sys_approval_request` insert site); + * 3. **the automation-run recorder** — both write paths of + * `ObjectStoreSuspendedRunStore` in `service-automation` (`serialize()` + * for paused rows, `recordTerminal()` for terminal rows). + * + * All three are live on `main`: #10101's PR #11311 (merged 2026-08-23) + * promoted `resolveRecordOrganizationField` to the shared resolver in + * `@objectstack/metadata-core` and wired all three platform-row writers to + * it — each resolves the SUBJECT record's organization first, falling back + * to actor context, exactly as the ruling above states. The `.describe()` + * below names all three consumers accordingly. * * ⛔ The refusal posture is UNCHANGED for a FOURTH consumer. Three named * platform-row writers are still not a general field-roles mechanism: anything @@ -663,9 +668,13 @@ export const TenancyConfigSchema = lazySchema(() => strictObject({ 'fallback applies. No default is materialized here on purpose (#5315).', ), organizationField: z.string().optional().describe( - 'STAMP-ONLY (#8778): column carrying the organization a row is ABOUT, ' + - 'consulted exclusively when audit rows are stamped. It does NOT ' + - 'tenant-scope anything — no read path (`applyTenantScope`, ' + + 'STAMP-ONLY (#8778, widened by cloud#1395): column carrying the ' + + 'organization a row is ABOUT, consulted by the three sanctioned ' + + 'platform-row writers — audit stamping, the approval-row writer ' + + '(`plugin-approvals`), and the automation-run recorder ' + + '(`service-automation`) — via the shared `resolveRecordOrganizationField` ' + + 'resolver in `@objectstack/metadata-core`. It does NOT tenant-scope ' + + 'anything — no read path (`applyTenantScope`, ' + '`injectTenantOnInsert`, `computeTenantLayer0Filter`) reads it, so ' + 'declaring it never walls the object and never hides rows. Declare it ' + 'only when the organization a row belongs to lives under a column that ' +