You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 9abe79f
Browse filesBrowse the repository at this point in the historyBrowse files
docs(spec): regenerate the four reference pages the main merge left on one side
The merge of origin/main (afc3b64) routed four generated reference pages
through the os-regen driver, which defers them instead of text-merging:
ui/view, ui/page, data/object and api/protocol. Each was edited on both sides
since the fork point (eff0a96), so the merge commit carries one side only.
Regenerated from the merged tree with the chain the pre-commit hook names:
`gen:schema` then `gen:docs`. No hand edit. Against main the four pages now
differ by exactly the branch's own 32 changed lines, and against the pre-merge
branch tip by exactly main's 46: both sides survive, nothing else moved.
Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
|**settings**|`Record<string, any>`| optional | User-provided settings at install time |
1913
-
|**enableOnInstall**|`boolean`| optional (default: `true`) | Whether to enable immediately after install — restates the install-door request key, whose one authority is api/PackageInstallRequest; this protocol primitive does not read it|
1913
+
|**enableOnInstall**|`boolean`| optional | Whether to enable immediately after install — restates the install-door request key, whose one authority is api/PackageInstallRequest; this protocol primitive honours it on the registry row: `true` enables, `false` disables, and ABSENT keeps the row's current lifecycle state (a fresh install lands enabled)|
1914
1914
|**platformVersion**|`string`| optional | Current platform version for compatibility verification |
@@ -2274,7 +2274,7 @@ Installed package with runtime lifecycle state
2274
2274
| :--- | :--- | :--- | :--- |
2275
2275
|**type**|`string`| ✅ | Metadata type name |
2276
2276
|**name**|`string`| ✅ | Item name — lowercase snake_case segments, optionally dot-qualified (`crm_lead`, `crm_lead.pipeline`). The promotion door enforces the same grammar as `saveMetaItem`. |
2277
-
|**organizationId**|`string`| optional | Organization (tenant) scope for the promotion. The implementation resolves the draft through the org partition (ADR-0005), so a draft authored org-scoped must be published under the same scope or the lookup answers 404 `[no_draft]`. Absent = environment-wide. |
2277
+
|**organizationId**|`string`| optional | Organization (tenant) scope for the promotion. The implementation resolves the draft through the org partition (ADR-0005), so a draft authored org-scoped must be published under the same scope or the lookup answers 404 `NO_DRAFT`. Absent = environment-wide. |
2278
2278
|**actor**|`string`| optional | Identity recorded on the `op='publish'` history event. On the REST door this is the request's authenticated identity (one producer) — never a caller-supplied header. |
2279
2279
|**message**|`string`| optional | Optional human-readable note recorded with the publish history event. |
2280
2280
|**packageId**|`string \| null`| optional | ADR-0048 — the software package the draft being promoted was listed under, when the caller has one to state (`?package=<id>` on the REST door). ⚠️ `null` is NOT the same as absent, and the difference is load-bearing: the implementation branches on the KEY BEING PRESENT, so an ABSENT key keeps the historical "match any package" resolution while `null` pins the lookup to the package-UNBOUND row. Spread it in conditionally; a present-and-`undefined` key coerces to `null` downstream and makes a package-bound draft unfindable — a silent `no_draft` on the untouched path. |
|**fieldGroups**|`{ key: string; label: string; icon?: string; description?: string; … }[]`| optional | Ordered list of field groups (array order = display order). See ObjectFieldGroupSchema. |
|**requiredPermissions**|`string[] \| { read?: string[]; create?: string[]; update?: string[]; delete?: string[] }`| optional |[ADR-0066 D3/⑤] Capabilities required to access this object (AND-gate) — `string[]` gates all CRUD, or a `{read,create,update,delete}` map gates per operation. |
159
159
|**lifecycle**|`{ class: Enum<'record' \| 'audit' \| 'telemetry' \| 'transient' \| 'event'>; retention?: object; ttl?: object; storage?: object; … }`| optional | Data lifecycle contract (ADR-0057): class + retention/ttl/rotation/archive policies enforced by the platform LifecycleService. |
@@ -218,7 +218,7 @@ const result = ApiMethod.parse(data);
218
218
|**label**|`string`| optional | Human readable label |
219
219
|**type**|`Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>`| ✅ | Field Data Type |
220
220
|**description**|`string`| optional | Tooltip/Help text |
221
-
|**format**|`string`| optional | Format string (e.g. email, phone) |
221
+
| **format** | `string` | optional | Free-form string whose meaning depends on the field type and on the reader. The spec declares NO vocabulary for it and checks nothing but that it is a string, so any string parses on any field type. On an `autonumber` field it is the record-number PATTERN — the shorthand that predates `autonumberFormat`, which wins when both are present: `format: 'INV-{0000}'` mints `INV-0001` on both the query engine and the SQL driver, while a value carrying no `{...}` token is emitted as literal text with the bare counter appended (`format: 'email'` mints `email1`). Prefer `autonumberFormat` on a new field. On any other field type the server does not act on it: it picks no column type, coerces no value and runs no check from it. The Studio UI reads it as a display hint, in words and with defaults that its renderers own and declare. For example, the `date` and `datetime` cells read it as a display STYLE, and on a plain-text field the shared cell-renderer resolver reads a small set of words that promote the cell to a richer renderer, such as a link; each of those falls back silently to a default rendering when it does not recognise the word. To constrain a VALUE, use the field `type` (the write-time record validator's built-in email, url and phone checks key on `type`, never on this key) or a `format` validation rule, whose own `format` key is the closed set `email` \| `url` \| `phone` \| `json`. |
222
222
|**required**|`boolean`| optional (default: `false`) | Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. On a multi-value lookup (`multiple: true`) required means NON-EMPTY array — an emptied required set fails validation loudly; `[]` does not satisfy it (maintainer ruling 2026-08-18). NOT a column constraint — the physical NOT NULL is a separate explicit opt-in (`storage.notNull`), so tightening this on a deployed object is safe: existing null rows stay readable, and editable as long as the write does not touch this field. |
223
223
|**storage**|`{ notNull?: boolean }`| optional | Physical storage constraints (ADR-0113). Owns the DDL the write contract deliberately does not imply. Absent = no storage-level constraint requested. |
224
224
|**searchable**|`boolean`| optional (default: `false`) | Is searchable |
@@ -311,15 +311,14 @@ const result = ApiMethod.parse(data);
|**collapsible**|`boolean`| optional |[DEPRECATED → collapse] Boolean pair with `collapsed`; use the `collapse` enum. |
314
-
|**collapsed**|`boolean`| optional |[DEPRECATED → collapse]Boolean pair with `collapsible`; use the `collapse` enum. |
314
+
|**collapsed**|`boolean`| optional |[DEPRECATED → collapse]true → 'collapsed' (collapsible, starts closed) on its own — it needs no `collapsible` and outranks `collapsible: false`; false → 'none', or 'expanded' beside `collapsible: true`. Use the `collapse` enum. |
315
315
316
316
### Nested Shape: `Object.tenancy`
317
317
318
318
| Property | Type | Required | Description |
319
319
| :--- | :--- | :--- | :--- |
320
320
|**enabled**|`boolean`| ✅ | Enable multi-tenancy for this object |
321
321
|**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. |
322
-
| **organizationField** | `string` | optional | STAMP-ONLY: 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`. |
323
322
324
323
### Nested Shape: `Object.access`
325
324
@@ -461,7 +460,7 @@ const result = ApiMethod.parse(data);
461
460
|**successMessage**|`string \| Record<string, string>`| optional | Success message to show after execution |
462
461
|**errorMessage**|`string \| Record<string, string>`| optional | Error message to show when the action fails (overrides the raw error). |
463
462
|**refreshAfter**|`boolean`| optional (default: `false`) | Refresh view after execution |
464
-
|**undoable**|`boolean`| optional | Offer an Undo affordance after this single-record update action succeeds. `operation: 'update'` is the one declared operation and the declared form of that action: what the undo captures is the prior value of EVERY field the action writes — the merged write bag, `patch` UNDER the collected `params`, not `patch` alone. An action with no `operation` declares no write set, so nothing anchors the capture there. |
463
+
|**undoable**|`boolean`| optional | Offer an Undo affordance after this single-record update action succeeds. `operation: 'update'` is the one declared operation and the declared form of that action: what the undo captures is the prior value of EVERY field the action writes — the merged write bag, `patch` UNDER the collected `params`, not `patch` alone. An action with no `operation` declares no write set here, but `type: 'api'` is fulfilled by the console, which builds the undo envelope from `undoable` alone. Those two shapes — `operation: 'update'` and `type: 'api'` — are the whole fulfillable set; on a registered action `undoable` beside any other shape is refused, because nothing would anchor the capture. |
|**visible**|`boolean \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }`| optional | Visibility predicate — `true`/`false` literal, CEL string, or `{dialect, source}` envelope. The action is offered when it evaluates TRUE. Omit = always visible. |
467
466
|**requiresFeature**|`Enum<'twoFactor' \| 'organization' \| 'multiOrgEnabled' \| 'degradedTenancy' \| …>`| optional | Public auth feature flag gating this action; lowered into `visible` at parse time. |
@@ -551,7 +550,7 @@ const result = ApiMethod.parse(data);
551
550
|**label**|`string`| optional | Human readable label |
552
551
|**type**|`Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>`| ✅ | Field Data Type |
553
552
|**description**|`string`| optional | Tooltip/Help text |
554
-
|**format**|`string`| optional | Format string (e.g. email, phone) |
553
+
| **format** | `string` | optional | Free-form string whose meaning depends on the field type and on the reader. The spec declares NO vocabulary for it and checks nothing but that it is a string, so any string parses on any field type. On an `autonumber` field it is the record-number PATTERN — the shorthand that predates `autonumberFormat`, which wins when both are present: `format: 'INV-{0000}'` mints `INV-0001` on both the query engine and the SQL driver, while a value carrying no `{...}` token is emitted as literal text with the bare counter appended (`format: 'email'` mints `email1`). Prefer `autonumberFormat` on a new field. On any other field type the server does not act on it: it picks no column type, coerces no value and runs no check from it. The Studio UI reads it as a display hint, in words and with defaults that its renderers own and declare. For example, the `date` and `datetime` cells read it as a display STYLE, and on a plain-text field the shared cell-renderer resolver reads a small set of words that promote the cell to a richer renderer, such as a link; each of those falls back silently to a default rendering when it does not recognise the word. To constrain a VALUE, use the field `type` (the write-time record validator's built-in email, url and phone checks key on `type`, never on this key) or a `format` validation rule, whose own `format` key is the closed set `email` \| `url` \| `phone` \| `json`. |
555
554
|**required**|`boolean`| optional (default: `false`) | Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. On a multi-value lookup (`multiple: true`) required means NON-EMPTY array — an emptied required set fails validation loudly; `[]` does not satisfy it (maintainer ruling 2026-08-18). NOT a column constraint — the physical NOT NULL is a separate explicit opt-in (`storage.notNull`), so tightening this on a deployed object is safe: existing null rows stay readable, and editable as long as the write does not touch this field. |
556
555
|**storage**|`{ notNull?: boolean }`| optional | Physical storage constraints (ADR-0113). Owns the DDL the write contract deliberately does not imply. Absent = no storage-level constraint requested. |
557
556
|**searchable**|`boolean`| optional (default: `false`) | Is searchable |
|**collapsible**|`boolean`| optional |[DEPRECATED → collapse] Boolean pair with `collapsed`; use the `collapse` enum. |
670
-
|**collapsed**|`boolean`| optional |[DEPRECATED → collapse]Boolean pair with `collapsible`; use the `collapse` enum. |
669
+
|**collapsed**|`boolean`| optional |[DEPRECATED → collapse]true → 'collapsed' (collapsible, starts closed) on its own — it needs no `collapsible` and outranks `collapsible: false`; false → 'none', or 'expanded' beside `collapsible: true`. Use the `collapse` enum. |
671
670
672
671
673
672
---
@@ -748,7 +747,6 @@ Boolean-or-predicates override for a built-in CRUD affordance.
748
747
| :--- | :--- | :--- | :--- |
749
748
|**enabled**|`boolean`| ✅ | Enable multi-tenancy for this object |
750
749
|**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. |
751
-
| **organizationField** | `string` | optional | STAMP-ONLY: 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`. |
0 commit comments