From cc169793d12183fa5eeba04f2dd252846c72e17e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 26 Aug 2026 13:14:56 +0000 Subject: [PATCH] fix(spec): one sub-table per variant for a property that opens more than one object shape (#12316) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The measured remainder of #11601: 28 property rows repo-wide open two or more object shapes, and #12309 refused all of them because a heading had no single shape to name. The accessor grammar it introduced extends to cover them — a selector segment spliced in where the union sits, `[type='sidebar']` where the union has a discriminant and `[option 2]` where it does not. `nestedShapeOf` now reads its single-shape answer out of a general `nestedShapesOf` walk, so the two cannot drift apart about what one shape level means, and the section renderer loops over every shape instead of the one. Regenerated tree: 15 files, +1397 / -0 lines. All 1469 single-shape headings are byte-identical. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01E5LFCYBJ3q2s6yW6oMLxwy --- .../docs-multishape-variant-subtables.md | 65 +++ content/docs/references/ai/conversation.mdx | 36 ++ .../docs/references/ai/knowledge-source.mdx | 18 + .../references/automation/state-machine.mdx | 36 ++ content/docs/references/data/validation.mdx | 242 +++++++++ content/docs/references/identity/scim.mdx | 42 ++ .../docs/references/integration/connector.mdx | 124 +++++ .../docs/references/system/collaboration.mdx | 91 ++++ content/docs/references/system/job.mdx | 22 + content/docs/references/system/metrics.mdx | 8 + content/docs/references/system/migration.mdx | 140 ++++++ .../references/system/settings-manifest.mdx | 27 + content/docs/references/ui/action.mdx | 21 + content/docs/references/ui/app.mdx | 462 ++++++++++++++++++ content/docs/references/ui/component.mdx | 30 ++ content/docs/references/ui/view.mdx | 98 ++++ packages/spec/scripts/lib/format-type.ts | 197 ++++++-- packages/spec/scripts/lib/schema-section.ts | 39 +- packages/spec/scripts/nested-shape.test.ts | 371 +++++++++++++- 19 files changed, 2018 insertions(+), 51 deletions(-) create mode 100644 .changeset/docs-multishape-variant-subtables.md diff --git a/.changeset/docs-multishape-variant-subtables.md b/.changeset/docs-multishape-variant-subtables.md new file mode 100644 index 0000000000..19835654ec --- /dev/null +++ b/.changeset/docs-multishape-variant-subtables.md @@ -0,0 +1,65 @@ +--- +"@objectstack/spec": patch +--- + +fix(spec): reference pages carry every variant's `.describe()` text when a property opens more than one object shape (#12316) + +The designed-and-measured remainder of #11601. #12309 gave a property that +opens exactly ONE nested object shape a `### Nested Shape:` table, and refused +a property whose type is a union of TWO OR MORE object shapes — there was no +single "the shape of this property" for a heading to name, and naming one +would have meant a variant index, i.e. a second addressing notation. Those rows +kept the collapsed signature cell, and the cell has no description column, so +their nested `.describe()` text stayed exactly as unreachable as #11601 found +it. + +**The population, re-measured on `origin/main@7bd6447`.** 28 of the 8604 +rendered property rows open two or more shapes, and **all 28** carry describe +text on at least one variant: `ui/App.navigation`, +`ui/NavigationArea.navigation` and `ui/NavigationContribution.items` with nine +variants each, `data/ConditionalValidation.then` / `.otherwise` and +`system/CRDTMergeResult.state` with five, `system/ChangeSet.operations` / +`.rollback` with seven, down to four two-variant rows. The census is unchanged +from the one #11601 recorded when it deferred them. + +**What is rendered now.** One sub-table per variant, each under the same +`### Nested Shape:` heading in the same position — no new grammar, and no new +heading level, so the single-h1 invariant and the module-header numbering are +untouched. The accessor gains a SELECTOR segment, spliced in where the union +sits in the wrapper stack rather than appended to the finished path, so +`StateMachine.on[string][option 3][number]` reads left to right as *the record +value, its third option, an element of it* — the reading `[string][number]` +already had. + +Two spellings, in preference order: + +- **`[type='sidebar']`** where the union has a discriminant — a key every + shape-bearing variant pins to a *different* literal. It states what the + author writes to select that variant, in the same `formatLiteral` spelling + the Type cell prints two lines above, and it is stable: reordering the union + or adding a tenth variant moves no existing heading and breaks no existing + anchor. 22 of the 28 rows have one. +- **`[option 2]`** for the six that do not — deliberately the word the union + branch has printed under `### Union Options` since long before this, rather + than a bare `[2]`, which in a stack of `[number]`/`[string]` segments would + read as a tuple index into the property's own type. It counts position in the + union *including* arms that open no shape, so the number is checkable against + the `string | { … } | { … }[]` cell the table sits under. + +The distinctness half of the discriminant test is load-bearing: a union whose +arms pin the same `const` has no discriminant by this rule, because answering +one would emit two identical headings — two identical anchors on one page, the +defect the `Schema.key` qualification exists to prevent. + +Every bound #11601 set still holds. Depth stays at `SHAPE_DEPTH_LIMIT = 1` — +what was lifted is the multi-shape *refusal* at level 1, not the budget, so a +shape nested inside a variant is exactly as unreachable as it was inside a lone +shape. "Only where there is text to publish" is now decided **per variant**, +which is the same rule one level finer: `ui/FormView.submitBehavior` opens four +shapes and one carries prose, so it gets one table, not four. And a variant +table still relocates no vocabulary — it is a third position for those keys. + +A property opening exactly one shape gains no selector segment, because the +stamp is conditioned on the union's own yield and not on the row's: all 1469 +single-shape headings #11601 published are byte-identical. The regenerated tree +is **purely additive** — 15 files, **+1397 / -0** lines, no reordering. diff --git a/content/docs/references/ai/conversation.mdx b/content/docs/references/ai/conversation.mdx index 1bcc5cfb81..06c181bcf0 100644 --- a/content/docs/references/ai/conversation.mdx +++ b/content/docs/references/ai/conversation.mdx @@ -104,6 +104,42 @@ const result = CodeContentSchema.parse(data); | **embedding** | `number[]` | optional | Vector embedding for semantic search | | **metadata** | `Record` | optional | | +### Nested Shape: `ConversationMessage.content[number][type='text']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'text'` | ✅ | | +| **text** | `string` | ✅ | Text content | +| **metadata** | `Record` | optional | | + +### Nested Shape: `ConversationMessage.content[number][type='image']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'image'` | ✅ | | +| **imageUrl** | `string` | ✅ | Image URL | +| **detail** | `Enum<'low' \| 'high' \| 'auto'>` | optional (default: `"auto"`) | | +| **metadata** | `Record` | optional | | + +### Nested Shape: `ConversationMessage.content[number][type='file']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'file'` | ✅ | | +| **fileUrl** | `string` | ✅ | File attachment URL | +| **mimeType** | `string` | ✅ | MIME type | +| **fileName** | `string` | optional | | +| **metadata** | `Record` | optional | | + +### Nested Shape: `ConversationMessage.content[number][type='code']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'code'` | ✅ | | +| **text** | `string` | ✅ | Code snippet | +| **language** | `string` | optional (default: `"text"`) | | +| **metadata** | `Record` | optional | | + ### Nested Shape: `ConversationMessage.functionCall` | Property | Type | Required | Description | diff --git a/content/docs/references/ai/knowledge-source.mdx b/content/docs/references/ai/knowledge-source.mdx index b602d6a3ef..29275b0f1f 100644 --- a/content/docs/references/ai/knowledge-source.mdx +++ b/content/docs/references/ai/knowledge-source.mdx @@ -87,6 +87,24 @@ const result = FileKnowledgeSourceSchema.parse(data); | **refresh** | `{ onRecordChange?: boolean; cron?: string }` | optional (default: `{}`) | | | **aiExposed** | `boolean` | optional (default: `true`) | | +### Nested Shape: `KnowledgeSource.source[kind='object']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **kind** | `'object'` | ✅ | | +| **object** | `string` | ✅ | Short object name to index | +| **contentFields** | `string[]` | ✅ | Fields contributing to document content | +| **metadataFields** | `string[]` | optional (default: `[]`) | | +| **where** | `Record` | optional | | + +### Nested Shape: `KnowledgeSource.source[kind='file']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **kind** | `'file'` | ✅ | | +| **prefix** | `string` | ✅ | Storage prefix | +| **mimeTypes** | `string[]` | optional (default: `[]`) | | + ### Nested Shape: `KnowledgeSource.embedding` | Property | Type | Required | Description | diff --git a/content/docs/references/automation/state-machine.mdx b/content/docs/references/automation/state-machine.mdx index 536fa71a7a..139862f2d0 100644 --- a/content/docs/references/automation/state-machine.mdx +++ b/content/docs/references/automation/state-machine.mdx @@ -159,6 +159,24 @@ Type: `string` | **states** | `Record; entry?: (string \| object)[]; exit?: (string \| object)[]; on?: Record; … }>` | optional | | | **meta** | `{ label?: string; description?: string; color?: string; aiInstructions?: string }` | optional | | +### Nested Shape: `StateMachine.on[string][option 2]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **target** | `string` | optional | Target State ID | +| **cond** | `string \| { type: string; params?: Record }` | optional | Condition (Guard) required to take this path | +| **actions** | `(string \| { type: string; params?: Record })[]` | optional | Actions to execute during transition | +| **description** | `string` | optional | Human readable description of this rule | + +### Nested Shape: `StateMachine.on[string][option 3][number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **target** | `string` | optional | Target State ID | +| **cond** | `string \| { type: string; params?: Record }` | optional | Condition (Guard) required to take this path | +| **actions** | `(string \| { type: string; params?: Record })[]` | optional | Actions to execute during transition | +| **description** | `string` | optional | Human readable description of this rule | + --- @@ -177,6 +195,24 @@ Type: `string` | **states** | `Record` | optional | | | **meta** | `{ label?: string; description?: string; color?: string; aiInstructions?: string }` | optional | | +### Nested Shape: `StateNode.on[string][option 2]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **target** | `string` | optional | Target State ID | +| **cond** | `string \| { type: string; params?: Record }` | optional | Condition (Guard) required to take this path | +| **actions** | `(string \| { type: string; params?: Record })[]` | optional | Actions to execute during transition | +| **description** | `string` | optional | Human readable description of this rule | + +### Nested Shape: `StateNode.on[string][option 3][number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **target** | `string` | optional | Target State ID | +| **cond** | `string \| { type: string; params?: Record }` | optional | Condition (Guard) required to take this path | +| **actions** | `(string \| { type: string; params?: Record })[]` | optional | Actions to execute during transition | +| **description** | `string` | optional | Human readable description of this rule | + ### Nested Shape: `StateNode.always[number]` | Property | Type | Required | Description | diff --git a/content/docs/references/data/validation.mdx b/content/docs/references/data/validation.mdx index 498f7f7362..00734203bc 100644 --- a/content/docs/references/data/validation.mdx +++ b/content/docs/references/data/validation.mdx @@ -118,6 +118,248 @@ const result = ConditionalValidationSchema.parse(data); | **then** | `{ name: string; label?: string; description?: string; active?: boolean; … } \| [ConditionalValidation](#conditionalvalidation) \| … +4 more` | ✅ | Validation rule to apply when condition is true | | **otherwise** | `{ name: string; label?: string; description?: string; active?: boolean; … } \| [ConditionalValidation](#conditionalvalidation) \| … +4 more` | optional | Validation rule to apply when condition is false | +### Nested Shape: `ConditionalValidation.then[type='script']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Unique rule name (snake_case) | +| **label** | `string` | optional | Human-readable label for the rule listing | +| **description** | `string` | optional | Administrative notes explaining the business reason | +| **active** | `boolean` | optional (default: `true`) | | +| **events** | `Enum<'insert' \| 'update'>[]` | optional (default: `["insert","update"]`) | Write contexts the rule runs on. `delete` is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a `beforeDelete` lifecycle hook | +| **priority** | `integer` | optional (default: `100`) | Execution priority (lower runs first, default: 100) | +| **tags** | `string[]` | optional | Categorization tags (e.g., "compliance", "billing") | +| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | optional (default: `"error"`) | | +| **message** | `string` | ✅ | Error message to display to the user | +| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | +| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. | +| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). | +| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). | +| **_packageId** | `string` | optional | Owning package machine id. | +| **_packageVersion** | `string` | optional | Owning package version. | +| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **type** | `'script'` | ✅ | | +| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL). If TRUE, validation fails. e.g. P`record.amount < 0` | + +### Nested Shape: `ConditionalValidation.then[type='state_machine']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Unique rule name (snake_case) | +| **label** | `string` | optional | Human-readable label for the rule listing | +| **description** | `string` | optional | Administrative notes explaining the business reason | +| **active** | `boolean` | optional (default: `true`) | | +| **events** | `Enum<'insert' \| 'update'>[]` | optional (default: `["insert","update"]`) | Write contexts the rule runs on. `delete` is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a `beforeDelete` lifecycle hook | +| **priority** | `integer` | optional (default: `100`) | Execution priority (lower runs first, default: 100) | +| **tags** | `string[]` | optional | Categorization tags (e.g., "compliance", "billing") | +| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | optional (default: `"error"`) | | +| **message** | `string` | ✅ | Error message to display to the user | +| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | +| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. | +| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). | +| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). | +| **_packageId** | `string` | optional | Owning package machine id. | +| **_packageVersion** | `string` | optional | Owning package version. | +| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **type** | `'state_machine'` | ✅ | | +| **field** | `string` | ✅ | State field (e.g. status) | +| **transitions** | `Record` | ✅ | Map of `{ OldState: [AllowedNewStates] }` | +| **initialStates** | `string[]` | optional | States a record may be CREATED in. When set, an INSERT whose state field carries a value outside this list is rejected (server-enforced) — the FSM entry point. `transitions` only governs UPDATE, and a `select` field permits ANY declared option as an initial value, so without this a record could be born mid-flow (e.g. created already `approved`). Omit to keep the legacy behavior (no initial-state check on insert). #3165. | + +### Nested Shape: `ConditionalValidation.then[type='format']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Unique rule name (snake_case) | +| **label** | `string` | optional | Human-readable label for the rule listing | +| **description** | `string` | optional | Administrative notes explaining the business reason | +| **active** | `boolean` | optional (default: `true`) | | +| **events** | `Enum<'insert' \| 'update'>[]` | optional (default: `["insert","update"]`) | Write contexts the rule runs on. `delete` is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a `beforeDelete` lifecycle hook | +| **priority** | `integer` | optional (default: `100`) | Execution priority (lower runs first, default: 100) | +| **tags** | `string[]` | optional | Categorization tags (e.g., "compliance", "billing") | +| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | optional (default: `"error"`) | | +| **message** | `string` | ✅ | Error message to display to the user | +| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | +| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. | +| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). | +| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). | +| **_packageId** | `string` | optional | Owning package machine id. | +| **_packageVersion** | `string` | optional | Owning package version. | +| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **type** | `'format'` | ✅ | | +| **field** | `string` | ✅ | | +| **regex** | `string` | optional | | +| **format** | `Enum<'email' \| 'url' \| 'phone' \| 'json'>` | optional | | + +### Nested Shape: `ConditionalValidation.then[type='cross_field']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Unique rule name (snake_case) | +| **label** | `string` | optional | Human-readable label for the rule listing | +| **description** | `string` | optional | Administrative notes explaining the business reason | +| **active** | `boolean` | optional (default: `true`) | | +| **events** | `Enum<'insert' \| 'update'>[]` | optional (default: `["insert","update"]`) | Write contexts the rule runs on. `delete` is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a `beforeDelete` lifecycle hook | +| **priority** | `integer` | optional (default: `100`) | Execution priority (lower runs first, default: 100) | +| **tags** | `string[]` | optional | Categorization tags (e.g., "compliance", "billing") | +| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | optional (default: `"error"`) | | +| **message** | `string` | ✅ | Error message to display to the user | +| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | +| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. | +| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). | +| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). | +| **_packageId** | `string` | optional | Owning package machine id. | +| **_packageVersion** | `string` | optional | Owning package version. | +| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **type** | `'cross_field'` | ✅ | | +| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL) comparing fields. e.g. P`record.end_date > record.start_date` | +| **fields** | `string[]` | ✅ | Fields involved. Only fields[0] is read (labels which field the violation attaches to); the rest are advisory. Shares script’s evaluation path. | + +### Nested Shape: `ConditionalValidation.then[type='json_schema']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Unique rule name (snake_case) | +| **label** | `string` | optional | Human-readable label for the rule listing | +| **description** | `string` | optional | Administrative notes explaining the business reason | +| **active** | `boolean` | optional (default: `true`) | | +| **events** | `Enum<'insert' \| 'update'>[]` | optional (default: `["insert","update"]`) | Write contexts the rule runs on. `delete` is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a `beforeDelete` lifecycle hook | +| **priority** | `integer` | optional (default: `100`) | Execution priority (lower runs first, default: 100) | +| **tags** | `string[]` | optional | Categorization tags (e.g., "compliance", "billing") | +| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | optional (default: `"error"`) | | +| **message** | `string` | ✅ | Error message to display to the user | +| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | +| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. | +| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). | +| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). | +| **_packageId** | `string` | optional | Owning package machine id. | +| **_packageVersion** | `string` | optional | Owning package version. | +| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **type** | `'json_schema'` | ✅ | | +| **field** | `string` | ✅ | JSON field to validate | +| **schema** | `Record` | ✅ | JSON Schema object definition | + +### Nested Shape: `ConditionalValidation.otherwise[type='script']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Unique rule name (snake_case) | +| **label** | `string` | optional | Human-readable label for the rule listing | +| **description** | `string` | optional | Administrative notes explaining the business reason | +| **active** | `boolean` | optional (default: `true`) | | +| **events** | `Enum<'insert' \| 'update'>[]` | optional (default: `["insert","update"]`) | Write contexts the rule runs on. `delete` is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a `beforeDelete` lifecycle hook | +| **priority** | `integer` | optional (default: `100`) | Execution priority (lower runs first, default: 100) | +| **tags** | `string[]` | optional | Categorization tags (e.g., "compliance", "billing") | +| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | optional (default: `"error"`) | | +| **message** | `string` | ✅ | Error message to display to the user | +| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | +| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. | +| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). | +| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). | +| **_packageId** | `string` | optional | Owning package machine id. | +| **_packageVersion** | `string` | optional | Owning package version. | +| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **type** | `'script'` | ✅ | | +| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL). If TRUE, validation fails. e.g. P`record.amount < 0` | + +### Nested Shape: `ConditionalValidation.otherwise[type='state_machine']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Unique rule name (snake_case) | +| **label** | `string` | optional | Human-readable label for the rule listing | +| **description** | `string` | optional | Administrative notes explaining the business reason | +| **active** | `boolean` | optional (default: `true`) | | +| **events** | `Enum<'insert' \| 'update'>[]` | optional (default: `["insert","update"]`) | Write contexts the rule runs on. `delete` is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a `beforeDelete` lifecycle hook | +| **priority** | `integer` | optional (default: `100`) | Execution priority (lower runs first, default: 100) | +| **tags** | `string[]` | optional | Categorization tags (e.g., "compliance", "billing") | +| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | optional (default: `"error"`) | | +| **message** | `string` | ✅ | Error message to display to the user | +| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | +| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. | +| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). | +| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). | +| **_packageId** | `string` | optional | Owning package machine id. | +| **_packageVersion** | `string` | optional | Owning package version. | +| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **type** | `'state_machine'` | ✅ | | +| **field** | `string` | ✅ | State field (e.g. status) | +| **transitions** | `Record` | ✅ | Map of `{ OldState: [AllowedNewStates] }` | +| **initialStates** | `string[]` | optional | States a record may be CREATED in. When set, an INSERT whose state field carries a value outside this list is rejected (server-enforced) — the FSM entry point. `transitions` only governs UPDATE, and a `select` field permits ANY declared option as an initial value, so without this a record could be born mid-flow (e.g. created already `approved`). Omit to keep the legacy behavior (no initial-state check on insert). #3165. | + +### Nested Shape: `ConditionalValidation.otherwise[type='format']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Unique rule name (snake_case) | +| **label** | `string` | optional | Human-readable label for the rule listing | +| **description** | `string` | optional | Administrative notes explaining the business reason | +| **active** | `boolean` | optional (default: `true`) | | +| **events** | `Enum<'insert' \| 'update'>[]` | optional (default: `["insert","update"]`) | Write contexts the rule runs on. `delete` is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a `beforeDelete` lifecycle hook | +| **priority** | `integer` | optional (default: `100`) | Execution priority (lower runs first, default: 100) | +| **tags** | `string[]` | optional | Categorization tags (e.g., "compliance", "billing") | +| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | optional (default: `"error"`) | | +| **message** | `string` | ✅ | Error message to display to the user | +| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | +| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. | +| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). | +| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). | +| **_packageId** | `string` | optional | Owning package machine id. | +| **_packageVersion** | `string` | optional | Owning package version. | +| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **type** | `'format'` | ✅ | | +| **field** | `string` | ✅ | | +| **regex** | `string` | optional | | +| **format** | `Enum<'email' \| 'url' \| 'phone' \| 'json'>` | optional | | + +### Nested Shape: `ConditionalValidation.otherwise[type='cross_field']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Unique rule name (snake_case) | +| **label** | `string` | optional | Human-readable label for the rule listing | +| **description** | `string` | optional | Administrative notes explaining the business reason | +| **active** | `boolean` | optional (default: `true`) | | +| **events** | `Enum<'insert' \| 'update'>[]` | optional (default: `["insert","update"]`) | Write contexts the rule runs on. `delete` is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a `beforeDelete` lifecycle hook | +| **priority** | `integer` | optional (default: `100`) | Execution priority (lower runs first, default: 100) | +| **tags** | `string[]` | optional | Categorization tags (e.g., "compliance", "billing") | +| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | optional (default: `"error"`) | | +| **message** | `string` | ✅ | Error message to display to the user | +| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | +| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. | +| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). | +| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). | +| **_packageId** | `string` | optional | Owning package machine id. | +| **_packageVersion** | `string` | optional | Owning package version. | +| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **type** | `'cross_field'` | ✅ | | +| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL) comparing fields. e.g. P`record.end_date > record.start_date` | +| **fields** | `string[]` | ✅ | Fields involved. Only fields[0] is read (labels which field the violation attaches to); the rest are advisory. Shares script’s evaluation path. | + +### Nested Shape: `ConditionalValidation.otherwise[type='json_schema']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Unique rule name (snake_case) | +| **label** | `string` | optional | Human-readable label for the rule listing | +| **description** | `string` | optional | Administrative notes explaining the business reason | +| **active** | `boolean` | optional (default: `true`) | | +| **events** | `Enum<'insert' \| 'update'>[]` | optional (default: `["insert","update"]`) | Write contexts the rule runs on. `delete` is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a `beforeDelete` lifecycle hook | +| **priority** | `integer` | optional (default: `100`) | Execution priority (lower runs first, default: 100) | +| **tags** | `string[]` | optional | Categorization tags (e.g., "compliance", "billing") | +| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | optional (default: `"error"`) | | +| **message** | `string` | ✅ | Error message to display to the user | +| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | +| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. | +| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). | +| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). | +| **_packageId** | `string` | optional | Owning package machine id. | +| **_packageVersion** | `string` | optional | Owning package version. | +| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **type** | `'json_schema'` | ✅ | | +| **field** | `string` | ✅ | JSON field to validate | +| **schema** | `Record` | ✅ | JSON Schema object definition | + --- diff --git a/content/docs/references/identity/scim.mdx b/content/docs/references/identity/scim.mdx index 5442b93015..92dbe07f04 100644 --- a/content/docs/references/identity/scim.mdx +++ b/content/docs/references/identity/scim.mdx @@ -277,6 +277,48 @@ const result = SCIMAddressSchema.parse(data); | **startIndex** | `integer` | optional | Start index (1-based) | | **itemsPerPage** | `integer` | optional | Items per page | +### Nested Shape: `SCIMListResponse.Resources[number][option 1]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **schemas** | `string[]` | optional (default: `["urn:ietf:params:scim:schemas:core:2.0:User"]`) | SCIM schema URIs (must include User schema) | +| **id** | `string` | optional | Unique resource identifier | +| **externalId** | `string` | optional | External identifier from client system | +| **userName** | `string` | ✅ | Unique username (REQUIRED) | +| **name** | `{ formatted?: string; familyName?: string; givenName?: string; middleName?: string; … }` | optional | Structured name components | +| **displayName** | `string` | optional | Display name for UI | +| **nickName** | `string` | optional | Nickname | +| **profileUrl** | `string` | optional | Profile page URL | +| **title** | `string` | optional | Job title | +| **userType** | `string` | optional | User type (employee, contractor) | +| **preferredLanguage** | `string` | optional | Preferred language (ISO 639-1) | +| **locale** | `string` | optional | Locale (e.g., en-US) | +| **timezone** | `string` | optional | Timezone | +| **active** | `boolean` | optional (default: `true`) | Account active status | +| **password** | `string` | optional | Password (write-only) | +| **emails** | `{ value: string; type?: Enum<'work' \| 'home' \| 'other'>; display?: string; primary: boolean }[]` | optional | Email addresses | +| **phoneNumbers** | `{ value: string; type?: Enum<'work' \| 'home' \| 'mobile' \| 'fax' \| 'pager' \| 'other'>; display?: string; primary: boolean }[]` | optional | Phone numbers | +| **ims** | `{ value: string; type?: string; primary?: boolean }[]` | optional | IM addresses | +| **photos** | `{ value: string; type?: Enum<'photo' \| 'thumbnail'>; primary?: boolean }[]` | optional | Photo URLs | +| **addresses** | `{ formatted?: string; streetAddress?: string; locality?: string; region?: string; … }[]` | optional | Physical addresses | +| **groups** | `{ value: string; $ref?: string; display?: string; type?: Enum<'direct' \| 'indirect'> }[]` | optional | Group memberships | +| **entitlements** | `{ value: string; type?: string; primary?: boolean }[]` | optional | Entitlements | +| **roles** | `{ value: string; type?: string; primary?: boolean }[]` | optional | Roles | +| **x509Certificates** | `{ value: string; type?: string; primary?: boolean }[]` | optional | X509 certificates | +| **meta** | `{ resourceType?: string; created?: string; lastModified?: string; location?: string; … }` | optional | Resource metadata | +| **urn:ietf:params:scim:schemas:extension:enterprise:2.0:User** | `{ employeeNumber?: string; costCenter?: string; organization?: string; division?: string; … }` | optional | Enterprise user attributes | + +### Nested Shape: `SCIMListResponse.Resources[number][option 2]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **schemas** | `string[]` | optional (default: `["urn:ietf:params:scim:schemas:core:2.0:Group"]`) | SCIM schema URIs (must include Group schema) | +| **id** | `string` | optional | Unique resource identifier | +| **externalId** | `string` | optional | External identifier from client system | +| **displayName** | `string` | ✅ | Group display name (REQUIRED) | +| **members** | `{ value: string; $ref?: string; type?: Enum<'User' \| 'Group'>; display?: string }[]` | optional | Group members | +| **meta** | `{ resourceType?: string; created?: string; lastModified?: string; location?: string; … }` | optional | Resource metadata | + --- diff --git a/content/docs/references/integration/connector.mdx b/content/docs/references/integration/connector.mdx index b396992797..e170e12b31 100644 --- a/content/docs/references/integration/connector.mdx +++ b/content/docs/references/integration/connector.mdx @@ -191,6 +191,68 @@ Circuit breaker configuration | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `Connector.authentication[type='oauth2']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'oauth2'` | ✅ | | +| **authorizationUrl** | `string` | ✅ | OAuth2 authorization endpoint | +| **tokenUrl** | `string` | ✅ | OAuth2 token endpoint | +| **clientId** | `string` | ✅ | OAuth2 client ID | +| **clientSecret** | `string` | ✅ | OAuth2 client secret (typically from ENV) | +| **scopes** | `string[]` | optional | Requested OAuth2 scopes | +| **redirectUri** | `string` | optional | OAuth2 redirect URI | +| **refreshToken** | `string` | optional | Refresh token for token renewal | +| **tokenExpiry** | `number` | optional | Token expiry timestamp | + +### Nested Shape: `Connector.authentication[type='api-key']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'api-key'` | ✅ | | +| **key** | `string` | ✅ | API key value | +| **headerName** | `string` | optional (default: `"X-API-Key"`) | HTTP header name for API key | +| **paramName** | `string` | optional | Query parameter name (alternative to header) | + +### Nested Shape: `Connector.authentication[type='basic']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'basic'` | ✅ | | +| **username** | `string` | ✅ | Username | +| **password** | `string` | ✅ | Password | + +### Nested Shape: `Connector.authentication[type='bearer']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'bearer'` | ✅ | | +| **token** | `string` | ✅ | Bearer token | + +### Nested Shape: `Connector.auth[type='bearer']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'bearer'` | ✅ | | +| **credentialRef** | `string` | ✅ | Secrets-layer reference (e.g. an env-var name in the open tier) resolved to the bearer token at materialization. Never an inline token. | + +### Nested Shape: `Connector.auth[type='api-key']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'api-key'` | ✅ | | +| **credentialRef** | `string` | ✅ | Secrets-layer reference resolved to the API key at materialization. Never an inline key. | +| **headerName** | `string` | optional | HTTP header carrying the key (default X-API-Key). | +| **paramName** | `string` | optional | Query parameter carrying the key (alternative to header). | + +### Nested Shape: `Connector.auth[type='basic']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'basic'` | ✅ | | +| **username** | `string` | ✅ | Username (not a secret; safe to keep in metadata). | +| **credentialRef** | `string` | ✅ | Secrets-layer reference resolved to the password at materialization. Never an inline password. | + ### Nested Shape: `Connector.actions[number]` | Property | Type | Required | Description | @@ -639,6 +701,68 @@ Connector type | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `DeclarativeConnectorEntry.authentication[type='oauth2']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'oauth2'` | ✅ | | +| **authorizationUrl** | `string` | ✅ | OAuth2 authorization endpoint | +| **tokenUrl** | `string` | ✅ | OAuth2 token endpoint | +| **clientId** | `string` | ✅ | OAuth2 client ID | +| **clientSecret** | `string` | ✅ | OAuth2 client secret (typically from ENV) | +| **scopes** | `string[]` | optional | Requested OAuth2 scopes | +| **redirectUri** | `string` | optional | OAuth2 redirect URI | +| **refreshToken** | `string` | optional | Refresh token for token renewal | +| **tokenExpiry** | `number` | optional | Token expiry timestamp | + +### Nested Shape: `DeclarativeConnectorEntry.authentication[type='api-key']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'api-key'` | ✅ | | +| **key** | `string` | ✅ | API key value | +| **headerName** | `string` | optional (default: `"X-API-Key"`) | HTTP header name for API key | +| **paramName** | `string` | optional | Query parameter name (alternative to header) | + +### Nested Shape: `DeclarativeConnectorEntry.authentication[type='basic']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'basic'` | ✅ | | +| **username** | `string` | ✅ | Username | +| **password** | `string` | ✅ | Password | + +### Nested Shape: `DeclarativeConnectorEntry.authentication[type='bearer']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'bearer'` | ✅ | | +| **token** | `string` | ✅ | Bearer token | + +### Nested Shape: `DeclarativeConnectorEntry.auth[type='bearer']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'bearer'` | ✅ | | +| **credentialRef** | `string` | ✅ | Secrets-layer reference (e.g. an env-var name in the open tier) resolved to the bearer token at materialization. Never an inline token. | + +### Nested Shape: `DeclarativeConnectorEntry.auth[type='api-key']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'api-key'` | ✅ | | +| **credentialRef** | `string` | ✅ | Secrets-layer reference resolved to the API key at materialization. Never an inline key. | +| **headerName** | `string` | optional | HTTP header carrying the key (default X-API-Key). | +| **paramName** | `string` | optional | Query parameter carrying the key (alternative to header). | + +### Nested Shape: `DeclarativeConnectorEntry.auth[type='basic']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'basic'` | ✅ | | +| **username** | `string` | ✅ | Username (not a secret; safe to keep in metadata). | +| **credentialRef** | `string` | ✅ | Secrets-layer reference resolved to the password at materialization. Never an inline password. | + ### Nested Shape: `DeclarativeConnectorEntry.actions[number]` | Property | Type | Required | Description | diff --git a/content/docs/references/system/collaboration.mdx b/content/docs/references/system/collaboration.mdx index 93256ecaa9..7dc255263c 100644 --- a/content/docs/references/system/collaboration.mdx +++ b/content/docs/references/system/collaboration.mdx @@ -121,6 +121,49 @@ const result = AwarenessEventSchema.parse(data); | **state** | `{ type: 'lww-register'; value: any; timestamp: string; replicaId: string; … } \| { type: 'g-counter'; counts: Record } \| { type: 'pn-counter'; positive: Record; negative: Record } \| { type: 'or-set'; elements: object[] } \| … +1 more` | ✅ | Merged CRDT state | | **conflicts** | `{ type: string; description: string; resolved: boolean }[]` | optional | Conflicts encountered during merge | +### Nested Shape: `CRDTMergeResult.state[type='lww-register']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'lww-register'` | ✅ | | +| **value** | `any` | ✅ | Current register value | +| **timestamp** | `string` | ✅ | ISO 8601 datetime of last write | +| **replicaId** | `string` | ✅ | ID of replica that performed last write | +| **vectorClock** | `{ clock: Record }` | optional | Optional vector clock for causality tracking | + +### Nested Shape: `CRDTMergeResult.state[type='g-counter']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'g-counter'` | ✅ | | +| **counts** | `Record` | ✅ | Map of replica ID to count | + +### Nested Shape: `CRDTMergeResult.state[type='pn-counter']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'pn-counter'` | ✅ | | +| **positive** | `Record` | ✅ | Positive increments per replica | +| **negative** | `Record` | ✅ | Negative increments per replica | + +### Nested Shape: `CRDTMergeResult.state[type='or-set']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'or-set'` | ✅ | | +| **elements** | `{ value: any; timestamp: string; replicaId: string; uid: string; … }[]` | ✅ | Set elements with metadata | + +### Nested Shape: `CRDTMergeResult.state[type='text']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'text'` | ✅ | | +| **documentId** | `string` | ✅ | Document identifier | +| **content** | `string` | ✅ | Current text content | +| **operations** | `{ operationId: string; replicaId: string; position: integer; insert?: string; … }[]` | ✅ | History of operations | +| **lamportClock** | `integer` | ✅ | Current Lamport clock value | +| **vectorClock** | `{ clock: Record }` | ✅ | Vector clock for causality | + ### Nested Shape: `CRDTMergeResult.conflicts[number]` | Property | Type | Required | Description | @@ -339,6 +382,31 @@ This schema accepts one of the following structures: | **lastUpdate** | `string` | ✅ | ISO 8601 datetime of last cursor update | | **metadata** | `Record` | optional | Additional cursor metadata | +### Nested Shape: `CollaborationSession.operations[number][option 1]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **operationId** | `string` | ✅ | Unique operation identifier | +| **documentId** | `string` | ✅ | Document identifier | +| **userId** | `string` | ✅ | User who created the operation | +| **sessionId** | `string` | ✅ | Session identifier | +| **components** | `({ type: 'insert'; text: string; attributes?: Record } \| { type: 'delete'; count: integer } \| { type: 'retain'; count: integer; attributes?: Record })[]` | ✅ | Operation components | +| **baseVersion** | `integer` | ✅ | Document version this operation is based on | +| **timestamp** | `string` | ✅ | ISO 8601 datetime when operation was created | +| **metadata** | `Record` | optional | Additional operation metadata | + +### Nested Shape: `CollaborationSession.operations[number][option 2]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **operationId** | `string` | ✅ | Unique operation identifier | +| **replicaId** | `string` | ✅ | Replica identifier | +| **position** | `integer` | ✅ | Position in document | +| **insert** | `string` | optional | Text to insert | +| **delete** | `integer` | optional | Number of characters to delete | +| **timestamp** | `string` | ✅ | ISO 8601 datetime of operation | +| **lamportTimestamp** | `integer` | ✅ | Lamport timestamp for ordering | + --- @@ -643,6 +711,29 @@ This schema accepts one of the following structures: | **timestamp** | `string` | ✅ | ISO 8601 datetime when operation was created | | **metadata** | `Record` | optional | Additional operation metadata | +### Nested Shape: `OTOperation.components[number][type='insert']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'insert'` | ✅ | | +| **text** | `string` | ✅ | Text to insert | +| **attributes** | `Record` | optional | Text formatting attributes (e.g., bold, italic) | + +### Nested Shape: `OTOperation.components[number][type='delete']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'delete'` | ✅ | | +| **count** | `integer` | ✅ | Number of characters to delete | + +### Nested Shape: `OTOperation.components[number][type='retain']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'retain'` | ✅ | | +| **count** | `integer` | ✅ | Number of characters to retain | +| **attributes** | `Record` | optional | Attribute changes to apply | + --- diff --git a/content/docs/references/system/job.mdx b/content/docs/references/system/job.mdx index a38d332ab0..62eb0901ca 100644 --- a/content/docs/references/system/job.mdx +++ b/content/docs/references/system/job.mdx @@ -71,6 +71,28 @@ const result = CronScheduleSchema.parse(data); | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `Job.schedule[type='cron']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'cron'` | ✅ | | +| **expression** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | ✅ | Cron expression — cron`0 0 * * *` for daily at midnight. Build emits `{dialect:"cron",source}` envelope. | +| **timezone** | `string` | optional (default: `"UTC"`) | Timezone for cron execution (e.g., "America/New_York") | + +### Nested Shape: `Job.schedule[type='interval']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'interval'` | ✅ | | +| **intervalMs** | `integer` | ✅ | Interval in milliseconds | + +### Nested Shape: `Job.schedule[type='once']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'once'` | ✅ | | +| **at** | `string` | ✅ | ISO 8601 datetime when to execute | + ### Nested Shape: `Job.retryPolicy` | Property | Type | Required | Description | diff --git a/content/docs/references/system/metrics.mdx b/content/docs/references/system/metrics.mdx index 9db2e67afa..7632ab036e 100644 --- a/content/docs/references/system/metrics.mdx +++ b/content/docs/references/system/metrics.mdx @@ -396,6 +396,14 @@ Service Level Indicator | **window** | `{ size: integer; rolling?: boolean }` | ✅ | Measurement window | | **enabled** | `boolean` | optional (default: `true`) | | +### Nested Shape: `ServiceLevelIndicator.successCriteria[option 1]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **threshold** | `number` | ✅ | Threshold value | +| **operator** | `Enum<'lt' \| 'lte' \| 'gt' \| 'gte' \| 'eq'>` | ✅ | Comparison operator | +| **percentile** | `number` | optional | Percentile (0-1) | + ### Nested Shape: `ServiceLevelIndicator.window` | Property | Type | Required | Description | diff --git a/content/docs/references/system/migration.mdx b/content/docs/references/system/migration.mdx index 73da0b552b..c8ecd886c4 100644 --- a/content/docs/references/system/migration.mdx +++ b/content/docs/references/system/migration.mdx @@ -154,6 +154,146 @@ Dependency reference to another migration that must run first | **migrationId** | `string` | ✅ | ID of the migration this depends on | | **package** | `string` | optional | Package that owns the dependency migration | +### Nested Shape: `ChangeSet.operations[number][type='add_field']` + +Add a new field to an existing object + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'add_field'` | ✅ | | +| **objectName** | `string` | ✅ | Target object name | +| **fieldName** | `string` | ✅ | Name of the field to add | +| **field** | `{ name?: string; label?: string; type: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; description?: string; … }` | ✅ | Full field definition to add | + +### Nested Shape: `ChangeSet.operations[number][type='modify_field']` + +Modify properties of an existing field + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'modify_field'` | ✅ | | +| **objectName** | `string` | ✅ | Target object name | +| **fieldName** | `string` | ✅ | Name of the field to modify | +| **changes** | `Record` | ✅ | Partial field definition updates | + +### Nested Shape: `ChangeSet.operations[number][type='remove_field']` + +Remove a field from an existing object + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'remove_field'` | ✅ | | +| **objectName** | `string` | ✅ | Target object name | +| **fieldName** | `string` | ✅ | Name of the field to remove | + +### Nested Shape: `ChangeSet.operations[number][type='create_object']` + +Create a new object + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'create_object'` | ✅ | | +| **object** | `{ name: string; label?: string; pluralLabel?: string; description?: string; … }` | ✅ | Full object definition to create | + +### Nested Shape: `ChangeSet.operations[number][type='rename_object']` + +Rename an existing object + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'rename_object'` | ✅ | | +| **oldName** | `string` | ✅ | Current object name | +| **newName** | `string` | ✅ | New object name | + +### Nested Shape: `ChangeSet.operations[number][type='delete_object']` + +Delete an existing object + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'delete_object'` | ✅ | | +| **objectName** | `string` | ✅ | Name of the object to delete | + +### Nested Shape: `ChangeSet.operations[number][type='execute_sql']` + +Execute a raw SQL statement + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'execute_sql'` | ✅ | | +| **sql** | `string` | ✅ | Raw SQL statement to execute | +| **description** | `string` | optional | Human-readable description of the SQL | + +### Nested Shape: `ChangeSet.rollback[number][type='add_field']` + +Add a new field to an existing object + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'add_field'` | ✅ | | +| **objectName** | `string` | ✅ | Target object name | +| **fieldName** | `string` | ✅ | Name of the field to add | +| **field** | `{ name?: string; label?: string; type: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; description?: string; … }` | ✅ | Full field definition to add | + +### Nested Shape: `ChangeSet.rollback[number][type='modify_field']` + +Modify properties of an existing field + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'modify_field'` | ✅ | | +| **objectName** | `string` | ✅ | Target object name | +| **fieldName** | `string` | ✅ | Name of the field to modify | +| **changes** | `Record` | ✅ | Partial field definition updates | + +### Nested Shape: `ChangeSet.rollback[number][type='remove_field']` + +Remove a field from an existing object + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'remove_field'` | ✅ | | +| **objectName** | `string` | ✅ | Target object name | +| **fieldName** | `string` | ✅ | Name of the field to remove | + +### Nested Shape: `ChangeSet.rollback[number][type='create_object']` + +Create a new object + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'create_object'` | ✅ | | +| **object** | `{ name: string; label?: string; pluralLabel?: string; description?: string; … }` | ✅ | Full object definition to create | + +### Nested Shape: `ChangeSet.rollback[number][type='rename_object']` + +Rename an existing object + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'rename_object'` | ✅ | | +| **oldName** | `string` | ✅ | Current object name | +| **newName** | `string` | ✅ | New object name | + +### Nested Shape: `ChangeSet.rollback[number][type='delete_object']` + +Delete an existing object + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'delete_object'` | ✅ | | +| **objectName** | `string` | ✅ | Name of the object to delete | + +### Nested Shape: `ChangeSet.rollback[number][type='execute_sql']` + +Execute a raw SQL statement + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'execute_sql'` | ✅ | | +| **sql** | `string` | ✅ | Raw SQL statement to execute | +| **description** | `string` | optional | Human-readable description of the SQL | + --- diff --git a/content/docs/references/system/settings-manifest.mdx b/content/docs/references/system/settings-manifest.mdx index 5c67be8332..dc5b5569ea 100644 --- a/content/docs/references/system/settings-manifest.mdx +++ b/content/docs/references/system/settings-manifest.mdx @@ -239,6 +239,33 @@ const result = ResolvedSettingValueSchema.parse(data); | **description** | `string` | optional | Optional helper text | | **icon** | `string` | optional | Optional Lucide icon name | +### Nested Shape: `Specifier.handler[kind='http']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **kind** | `'http'` | ✅ | | +| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH'>` | optional (default: `"POST"`) | | +| **url** | `string` | ✅ | Endpoint URL; supports $`{...}` interpolation | +| **body** | `Record` | optional | Optional JSON body; supports $`{...}` interpolation | +| **confirmText** | `string \| Record` | optional | Confirm dialog text before invoking (omit = no confirm) | + +### Nested Shape: `Specifier.handler[kind='action']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **kind** | `'action'` | ✅ | | +| **name** | `string` | ✅ | Registered action machine name | +| **params** | `Record` | optional | | +| **confirmText** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | + +### Nested Shape: `Specifier.handler[kind='navigate']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **kind** | `'navigate'` | ✅ | | +| **url** | `string` | ✅ | Target URL or in-app route | +| **target** | `Enum<'_self' \| '_blank'>` | optional (default: `"_self"`) | | + --- diff --git a/content/docs/references/ui/action.mdx b/content/docs/references/ui/action.mdx index ebec09be03..d78624a814 100644 --- a/content/docs/references/ui/action.mdx +++ b/content/docs/references/ui/action.mdx @@ -106,6 +106,27 @@ const result = ActionSchema.parse(data); | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `Action.body[language='expression']` + +L1 expression body — pure formula, no IO + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **language** | `'expression'` | ✅ | | +| **source** | `string` | ✅ | Formula expression source | + +### Nested Shape: `Action.body[language='js']` + +L2 sandboxed JS body — runs inside an isolated VM with declared capabilities + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **language** | `'js'` | ✅ | | +| **source** | `string` | ✅ | Function body source | +| **capabilities** | `Enum<'api.read' \| 'api.write' \| 'api.transaction' \| 'crypto.uuid' \| 'log'>[]` | optional (default: `[]`) | Granted capability tokens | +| **timeoutMs** | `integer` | optional | Per-invocation timeout (ms) | +| **memoryMb** | `integer` | optional | Per-invocation memory cap (MB) | + ### Nested Shape: `Action.params[number]` | Property | Type | Required | Description | diff --git a/content/docs/references/ui/app.mdx b/content/docs/references/ui/app.mdx index 89ae404f7e..ec784a6d1e 100644 --- a/content/docs/references/ui/app.mdx +++ b/content/docs/references/ui/app.mdx @@ -111,6 +111,160 @@ const result = ActionNavItemSchema.parse(data); | **logo** | `string` | optional | Custom logo URL for this app | | **favicon** | `string` | optional | Custom favicon URL for this app | +### Nested Shape: `App.navigation[number][type='object']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | +| **label** | `string \| Record` | ✅ | Display proper label | +| **icon** | `string` | optional | Icon name | +| **order** | `number` | optional | Sort order within the same level (lower = first) | +| **badge** | `string \| number` | optional | Badge text or count displayed on the item | +| **badgeVariant** | `Enum<'default' \| 'secondary' \| 'destructive' \| 'outline'>` | optional | Visual variant of the nav badge. Declared to match the objectui NavigationRenderer read (inverse-drift fix, liveness audit #1878/#1891/#1894). | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL). e.g. P`'org_admin' in current_user.positions` | +| **requiredPermissions** | `string[]` | optional | Permissions required to access this item | +| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime | +| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered | +| **type** | `'object'` | ✅ | | +| **objectName** | `string` | ✅ | Target object name | +| **viewName** | `string` | optional | Default list view to open. Defaults to "all". Ignored when `recordId` is set. | +| **recordId** | `string` | optional | Navigate directly to this record id instead of the list view. Supports template vars: `{current_user_id}`, `{current_org_id}`. | +| **recordMode** | `Enum<'view' \| 'edit'>` | optional | Open the record in view (default) or edit mode. Only meaningful when `recordId` is set. | +| **filters** | `Record` | optional | URL filter conditions — targets the /:objectName/data bare surface via filter[``]=`` params instead of a saved view. Values support template vars `{current_user_id}`, `{current_org_id}`. Mutually exclusive with recordId/viewName. | +| **runAction** | `string` | optional | Auto-run this declared action once on arrival at the object's list surface (deep-link "navigate = run action", #4848). Must name an action defined in the stack; validated by defineStack and lint. Not combinable with recordId. | +| **children** | `({ id: string; label: string \| Record; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| … +7 more)[]` | optional | Child navigation items (e.g. specific views) | + +### Nested Shape: `App.navigation[number][type='dashboard']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | +| **label** | `string \| Record` | ✅ | Display proper label | +| **icon** | `string` | optional | Icon name | +| **order** | `number` | optional | Sort order within the same level (lower = first) | +| **badge** | `string \| number` | optional | Badge text or count displayed on the item | +| **badgeVariant** | `Enum<'default' \| 'secondary' \| 'destructive' \| 'outline'>` | optional | Visual variant of the nav badge. Declared to match the objectui NavigationRenderer read (inverse-drift fix, liveness audit #1878/#1891/#1894). | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL). e.g. P`'org_admin' in current_user.positions` | +| **requiredPermissions** | `string[]` | optional | Permissions required to access this item | +| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime | +| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered | +| **type** | `'dashboard'` | ✅ | | +| **dashboardName** | `string` | ✅ | Target dashboard name | + +### Nested Shape: `App.navigation[number][type='page']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | +| **label** | `string \| Record` | ✅ | Display proper label | +| **icon** | `string` | optional | Icon name | +| **order** | `number` | optional | Sort order within the same level (lower = first) | +| **badge** | `string \| number` | optional | Badge text or count displayed on the item | +| **badgeVariant** | `Enum<'default' \| 'secondary' \| 'destructive' \| 'outline'>` | optional | Visual variant of the nav badge. Declared to match the objectui NavigationRenderer read (inverse-drift fix, liveness audit #1878/#1891/#1894). | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL). e.g. P`'org_admin' in current_user.positions` | +| **requiredPermissions** | `string[]` | optional | Permissions required to access this item | +| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime | +| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered | +| **type** | `'page'` | ✅ | | +| **pageName** | `string` | ✅ | Target custom page component name | +| **params** | `Record` | optional | Parameters passed to the page context | + +### Nested Shape: `App.navigation[number][type='url']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | +| **label** | `string \| Record` | ✅ | Display proper label | +| **icon** | `string` | optional | Icon name | +| **order** | `number` | optional | Sort order within the same level (lower = first) | +| **badge** | `string \| number` | optional | Badge text or count displayed on the item | +| **badgeVariant** | `Enum<'default' \| 'secondary' \| 'destructive' \| 'outline'>` | optional | Visual variant of the nav badge. Declared to match the objectui NavigationRenderer read (inverse-drift fix, liveness audit #1878/#1891/#1894). | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL). e.g. P`'org_admin' in current_user.positions` | +| **requiredPermissions** | `string[]` | optional | Permissions required to access this item | +| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime | +| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered | +| **type** | `'url'` | ✅ | | +| **url** | `string` | ✅ | Target external URL | +| **target** | `Enum<'_self' \| '_blank'>` | optional (default: `"_self"`) | Link target window | + +### Nested Shape: `App.navigation[number][type='report']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | +| **label** | `string \| Record` | ✅ | Display proper label | +| **icon** | `string` | optional | Icon name | +| **order** | `number` | optional | Sort order within the same level (lower = first) | +| **badge** | `string \| number` | optional | Badge text or count displayed on the item | +| **badgeVariant** | `Enum<'default' \| 'secondary' \| 'destructive' \| 'outline'>` | optional | Visual variant of the nav badge. Declared to match the objectui NavigationRenderer read (inverse-drift fix, liveness audit #1878/#1891/#1894). | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL). e.g. P`'org_admin' in current_user.positions` | +| **requiredPermissions** | `string[]` | optional | Permissions required to access this item | +| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime | +| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered | +| **type** | `'report'` | ✅ | | +| **reportName** | `string` | ✅ | Target report name | + +### Nested Shape: `App.navigation[number][type='action']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | +| **label** | `string \| Record` | ✅ | Display proper label | +| **icon** | `string` | optional | Icon name | +| **order** | `number` | optional | Sort order within the same level (lower = first) | +| **badge** | `string \| number` | optional | Badge text or count displayed on the item | +| **badgeVariant** | `Enum<'default' \| 'secondary' \| 'destructive' \| 'outline'>` | optional | Visual variant of the nav badge. Declared to match the objectui NavigationRenderer read (inverse-drift fix, liveness audit #1878/#1891/#1894). | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL). e.g. P`'org_admin' in current_user.positions` | +| **requiredPermissions** | `string[]` | optional | Permissions required to access this item | +| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime | +| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered | +| **type** | `'action'` | ✅ | | +| **actionDef** | `{ actionName: string; params?: Record }` | ✅ | Action definition to execute when clicked | + +### Nested Shape: `App.navigation[number][type='component']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | +| **label** | `string \| Record` | ✅ | Display proper label | +| **icon** | `string` | optional | Icon name | +| **order** | `number` | optional | Sort order within the same level (lower = first) | +| **badge** | `string \| number` | optional | Badge text or count displayed on the item | +| **badgeVariant** | `Enum<'default' \| 'secondary' \| 'destructive' \| 'outline'>` | optional | Visual variant of the nav badge. Declared to match the objectui NavigationRenderer read (inverse-drift fix, liveness audit #1878/#1891/#1894). | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL). e.g. P`'org_admin' in current_user.positions` | +| **requiredPermissions** | `string[]` | optional | Permissions required to access this item | +| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime | +| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered | +| **type** | `'component'` | ✅ | | +| **componentRef** | `string` | ✅ | Component registry key (e.g. "metadata:directory") | +| **params** | `Record` | optional | Props passed to the component | + +### Nested Shape: `App.navigation[number][type='separator']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'separator'` | ✅ | | +| **id** | `string` | optional | Optional id for the separator | +| **order** | `number` | optional | Sort order within the same level (lower = first) | + +### Nested Shape: `App.navigation[number][type='group']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | +| **label** | `string \| Record` | ✅ | Display proper label | +| **icon** | `string` | optional | Icon name | +| **order** | `number` | optional | Sort order within the same level (lower = first) | +| **badge** | `string \| number` | optional | Badge text or count displayed on the item | +| **badgeVariant** | `Enum<'default' \| 'secondary' \| 'destructive' \| 'outline'>` | optional | Visual variant of the nav badge. Declared to match the objectui NavigationRenderer read (inverse-drift fix, liveness audit #1878/#1891/#1894). | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL). e.g. P`'org_admin' in current_user.positions` | +| **requiredPermissions** | `string[]` | optional | Permissions required to access this item | +| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime | +| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered | +| **type** | `'group'` | ✅ | | +| **expanded** | `boolean` | optional (default: `false`) | Default expansion state in sidebar | +| **children** | `({ id: string; label: string \| Record; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| … +7 more)[]` | ✅ | Child navigation items | + ### Nested Shape: `App.areas[number]` | Property | Type | Required | Description | @@ -261,6 +415,160 @@ const result = ActionNavItemSchema.parse(data); | **description** | `string \| Record` | optional | Area description | | **navigation** | `({ id: string; label: string \| Record; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| … +7 more)[]` | ✅ | Navigation items within this area | +### Nested Shape: `NavigationArea.navigation[number][type='object']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | +| **label** | `string \| Record` | ✅ | Display proper label | +| **icon** | `string` | optional | Icon name | +| **order** | `number` | optional | Sort order within the same level (lower = first) | +| **badge** | `string \| number` | optional | Badge text or count displayed on the item | +| **badgeVariant** | `Enum<'default' \| 'secondary' \| 'destructive' \| 'outline'>` | optional | Visual variant of the nav badge. Declared to match the objectui NavigationRenderer read (inverse-drift fix, liveness audit #1878/#1891/#1894). | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL). e.g. P`'org_admin' in current_user.positions` | +| **requiredPermissions** | `string[]` | optional | Permissions required to access this item | +| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime | +| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered | +| **type** | `'object'` | ✅ | | +| **objectName** | `string` | ✅ | Target object name | +| **viewName** | `string` | optional | Default list view to open. Defaults to "all". Ignored when `recordId` is set. | +| **recordId** | `string` | optional | Navigate directly to this record id instead of the list view. Supports template vars: `{current_user_id}`, `{current_org_id}`. | +| **recordMode** | `Enum<'view' \| 'edit'>` | optional | Open the record in view (default) or edit mode. Only meaningful when `recordId` is set. | +| **filters** | `Record` | optional | URL filter conditions — targets the /:objectName/data bare surface via filter[``]=`` params instead of a saved view. Values support template vars `{current_user_id}`, `{current_org_id}`. Mutually exclusive with recordId/viewName. | +| **runAction** | `string` | optional | Auto-run this declared action once on arrival at the object's list surface (deep-link "navigate = run action", #4848). Must name an action defined in the stack; validated by defineStack and lint. Not combinable with recordId. | +| **children** | `({ id: string; label: string \| Record; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| … +7 more)[]` | optional | Child navigation items (e.g. specific views) | + +### Nested Shape: `NavigationArea.navigation[number][type='dashboard']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | +| **label** | `string \| Record` | ✅ | Display proper label | +| **icon** | `string` | optional | Icon name | +| **order** | `number` | optional | Sort order within the same level (lower = first) | +| **badge** | `string \| number` | optional | Badge text or count displayed on the item | +| **badgeVariant** | `Enum<'default' \| 'secondary' \| 'destructive' \| 'outline'>` | optional | Visual variant of the nav badge. Declared to match the objectui NavigationRenderer read (inverse-drift fix, liveness audit #1878/#1891/#1894). | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL). e.g. P`'org_admin' in current_user.positions` | +| **requiredPermissions** | `string[]` | optional | Permissions required to access this item | +| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime | +| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered | +| **type** | `'dashboard'` | ✅ | | +| **dashboardName** | `string` | ✅ | Target dashboard name | + +### Nested Shape: `NavigationArea.navigation[number][type='page']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | +| **label** | `string \| Record` | ✅ | Display proper label | +| **icon** | `string` | optional | Icon name | +| **order** | `number` | optional | Sort order within the same level (lower = first) | +| **badge** | `string \| number` | optional | Badge text or count displayed on the item | +| **badgeVariant** | `Enum<'default' \| 'secondary' \| 'destructive' \| 'outline'>` | optional | Visual variant of the nav badge. Declared to match the objectui NavigationRenderer read (inverse-drift fix, liveness audit #1878/#1891/#1894). | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL). e.g. P`'org_admin' in current_user.positions` | +| **requiredPermissions** | `string[]` | optional | Permissions required to access this item | +| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime | +| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered | +| **type** | `'page'` | ✅ | | +| **pageName** | `string` | ✅ | Target custom page component name | +| **params** | `Record` | optional | Parameters passed to the page context | + +### Nested Shape: `NavigationArea.navigation[number][type='url']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | +| **label** | `string \| Record` | ✅ | Display proper label | +| **icon** | `string` | optional | Icon name | +| **order** | `number` | optional | Sort order within the same level (lower = first) | +| **badge** | `string \| number` | optional | Badge text or count displayed on the item | +| **badgeVariant** | `Enum<'default' \| 'secondary' \| 'destructive' \| 'outline'>` | optional | Visual variant of the nav badge. Declared to match the objectui NavigationRenderer read (inverse-drift fix, liveness audit #1878/#1891/#1894). | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL). e.g. P`'org_admin' in current_user.positions` | +| **requiredPermissions** | `string[]` | optional | Permissions required to access this item | +| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime | +| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered | +| **type** | `'url'` | ✅ | | +| **url** | `string` | ✅ | Target external URL | +| **target** | `Enum<'_self' \| '_blank'>` | optional (default: `"_self"`) | Link target window | + +### Nested Shape: `NavigationArea.navigation[number][type='report']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | +| **label** | `string \| Record` | ✅ | Display proper label | +| **icon** | `string` | optional | Icon name | +| **order** | `number` | optional | Sort order within the same level (lower = first) | +| **badge** | `string \| number` | optional | Badge text or count displayed on the item | +| **badgeVariant** | `Enum<'default' \| 'secondary' \| 'destructive' \| 'outline'>` | optional | Visual variant of the nav badge. Declared to match the objectui NavigationRenderer read (inverse-drift fix, liveness audit #1878/#1891/#1894). | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL). e.g. P`'org_admin' in current_user.positions` | +| **requiredPermissions** | `string[]` | optional | Permissions required to access this item | +| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime | +| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered | +| **type** | `'report'` | ✅ | | +| **reportName** | `string` | ✅ | Target report name | + +### Nested Shape: `NavigationArea.navigation[number][type='action']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | +| **label** | `string \| Record` | ✅ | Display proper label | +| **icon** | `string` | optional | Icon name | +| **order** | `number` | optional | Sort order within the same level (lower = first) | +| **badge** | `string \| number` | optional | Badge text or count displayed on the item | +| **badgeVariant** | `Enum<'default' \| 'secondary' \| 'destructive' \| 'outline'>` | optional | Visual variant of the nav badge. Declared to match the objectui NavigationRenderer read (inverse-drift fix, liveness audit #1878/#1891/#1894). | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL). e.g. P`'org_admin' in current_user.positions` | +| **requiredPermissions** | `string[]` | optional | Permissions required to access this item | +| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime | +| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered | +| **type** | `'action'` | ✅ | | +| **actionDef** | `{ actionName: string; params?: Record }` | ✅ | Action definition to execute when clicked | + +### Nested Shape: `NavigationArea.navigation[number][type='component']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | +| **label** | `string \| Record` | ✅ | Display proper label | +| **icon** | `string` | optional | Icon name | +| **order** | `number` | optional | Sort order within the same level (lower = first) | +| **badge** | `string \| number` | optional | Badge text or count displayed on the item | +| **badgeVariant** | `Enum<'default' \| 'secondary' \| 'destructive' \| 'outline'>` | optional | Visual variant of the nav badge. Declared to match the objectui NavigationRenderer read (inverse-drift fix, liveness audit #1878/#1891/#1894). | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL). e.g. P`'org_admin' in current_user.positions` | +| **requiredPermissions** | `string[]` | optional | Permissions required to access this item | +| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime | +| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered | +| **type** | `'component'` | ✅ | | +| **componentRef** | `string` | ✅ | Component registry key (e.g. "metadata:directory") | +| **params** | `Record` | optional | Props passed to the component | + +### Nested Shape: `NavigationArea.navigation[number][type='separator']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'separator'` | ✅ | | +| **id** | `string` | optional | Optional id for the separator | +| **order** | `number` | optional | Sort order within the same level (lower = first) | + +### Nested Shape: `NavigationArea.navigation[number][type='group']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | +| **label** | `string \| Record` | ✅ | Display proper label | +| **icon** | `string` | optional | Icon name | +| **order** | `number` | optional | Sort order within the same level (lower = first) | +| **badge** | `string \| number` | optional | Badge text or count displayed on the item | +| **badgeVariant** | `Enum<'default' \| 'secondary' \| 'destructive' \| 'outline'>` | optional | Visual variant of the nav badge. Declared to match the objectui NavigationRenderer read (inverse-drift fix, liveness audit #1878/#1891/#1894). | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL). e.g. P`'org_admin' in current_user.positions` | +| **requiredPermissions** | `string[]` | optional | Permissions required to access this item | +| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime | +| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered | +| **type** | `'group'` | ✅ | | +| **expanded** | `boolean` | optional (default: `false`) | Default expansion state in sidebar | +| **children** | `({ id: string; label: string \| Record; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| … +7 more)[]` | ✅ | Child navigation items | + --- @@ -277,6 +585,160 @@ A navigation contribution: a package injecting nav items into an app it does not | **priority** | `integer` | optional (default: `200`) | Merge priority within the target group — lower applied first (matches object extender priority) | | **items** | `({ id: string; label: string \| Record; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| … +7 more)[]` | ✅ | Navigation items contributed into the target app/group | +### Nested Shape: `NavigationContribution.items[number][type='object']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | +| **label** | `string \| Record` | ✅ | Display proper label | +| **icon** | `string` | optional | Icon name | +| **order** | `number` | optional | Sort order within the same level (lower = first) | +| **badge** | `string \| number` | optional | Badge text or count displayed on the item | +| **badgeVariant** | `Enum<'default' \| 'secondary' \| 'destructive' \| 'outline'>` | optional | Visual variant of the nav badge. Declared to match the objectui NavigationRenderer read (inverse-drift fix, liveness audit #1878/#1891/#1894). | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL). e.g. P`'org_admin' in current_user.positions` | +| **requiredPermissions** | `string[]` | optional | Permissions required to access this item | +| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime | +| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered | +| **type** | `'object'` | ✅ | | +| **objectName** | `string` | ✅ | Target object name | +| **viewName** | `string` | optional | Default list view to open. Defaults to "all". Ignored when `recordId` is set. | +| **recordId** | `string` | optional | Navigate directly to this record id instead of the list view. Supports template vars: `{current_user_id}`, `{current_org_id}`. | +| **recordMode** | `Enum<'view' \| 'edit'>` | optional | Open the record in view (default) or edit mode. Only meaningful when `recordId` is set. | +| **filters** | `Record` | optional | URL filter conditions — targets the /:objectName/data bare surface via filter[``]=`` params instead of a saved view. Values support template vars `{current_user_id}`, `{current_org_id}`. Mutually exclusive with recordId/viewName. | +| **runAction** | `string` | optional | Auto-run this declared action once on arrival at the object's list surface (deep-link "navigate = run action", #4848). Must name an action defined in the stack; validated by defineStack and lint. Not combinable with recordId. | +| **children** | `({ id: string; label: string \| Record; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| … +7 more)[]` | optional | Child navigation items (e.g. specific views) | + +### Nested Shape: `NavigationContribution.items[number][type='dashboard']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | +| **label** | `string \| Record` | ✅ | Display proper label | +| **icon** | `string` | optional | Icon name | +| **order** | `number` | optional | Sort order within the same level (lower = first) | +| **badge** | `string \| number` | optional | Badge text or count displayed on the item | +| **badgeVariant** | `Enum<'default' \| 'secondary' \| 'destructive' \| 'outline'>` | optional | Visual variant of the nav badge. Declared to match the objectui NavigationRenderer read (inverse-drift fix, liveness audit #1878/#1891/#1894). | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL). e.g. P`'org_admin' in current_user.positions` | +| **requiredPermissions** | `string[]` | optional | Permissions required to access this item | +| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime | +| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered | +| **type** | `'dashboard'` | ✅ | | +| **dashboardName** | `string` | ✅ | Target dashboard name | + +### Nested Shape: `NavigationContribution.items[number][type='page']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | +| **label** | `string \| Record` | ✅ | Display proper label | +| **icon** | `string` | optional | Icon name | +| **order** | `number` | optional | Sort order within the same level (lower = first) | +| **badge** | `string \| number` | optional | Badge text or count displayed on the item | +| **badgeVariant** | `Enum<'default' \| 'secondary' \| 'destructive' \| 'outline'>` | optional | Visual variant of the nav badge. Declared to match the objectui NavigationRenderer read (inverse-drift fix, liveness audit #1878/#1891/#1894). | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL). e.g. P`'org_admin' in current_user.positions` | +| **requiredPermissions** | `string[]` | optional | Permissions required to access this item | +| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime | +| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered | +| **type** | `'page'` | ✅ | | +| **pageName** | `string` | ✅ | Target custom page component name | +| **params** | `Record` | optional | Parameters passed to the page context | + +### Nested Shape: `NavigationContribution.items[number][type='url']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | +| **label** | `string \| Record` | ✅ | Display proper label | +| **icon** | `string` | optional | Icon name | +| **order** | `number` | optional | Sort order within the same level (lower = first) | +| **badge** | `string \| number` | optional | Badge text or count displayed on the item | +| **badgeVariant** | `Enum<'default' \| 'secondary' \| 'destructive' \| 'outline'>` | optional | Visual variant of the nav badge. Declared to match the objectui NavigationRenderer read (inverse-drift fix, liveness audit #1878/#1891/#1894). | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL). e.g. P`'org_admin' in current_user.positions` | +| **requiredPermissions** | `string[]` | optional | Permissions required to access this item | +| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime | +| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered | +| **type** | `'url'` | ✅ | | +| **url** | `string` | ✅ | Target external URL | +| **target** | `Enum<'_self' \| '_blank'>` | optional (default: `"_self"`) | Link target window | + +### Nested Shape: `NavigationContribution.items[number][type='report']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | +| **label** | `string \| Record` | ✅ | Display proper label | +| **icon** | `string` | optional | Icon name | +| **order** | `number` | optional | Sort order within the same level (lower = first) | +| **badge** | `string \| number` | optional | Badge text or count displayed on the item | +| **badgeVariant** | `Enum<'default' \| 'secondary' \| 'destructive' \| 'outline'>` | optional | Visual variant of the nav badge. Declared to match the objectui NavigationRenderer read (inverse-drift fix, liveness audit #1878/#1891/#1894). | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL). e.g. P`'org_admin' in current_user.positions` | +| **requiredPermissions** | `string[]` | optional | Permissions required to access this item | +| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime | +| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered | +| **type** | `'report'` | ✅ | | +| **reportName** | `string` | ✅ | Target report name | + +### Nested Shape: `NavigationContribution.items[number][type='action']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | +| **label** | `string \| Record` | ✅ | Display proper label | +| **icon** | `string` | optional | Icon name | +| **order** | `number` | optional | Sort order within the same level (lower = first) | +| **badge** | `string \| number` | optional | Badge text or count displayed on the item | +| **badgeVariant** | `Enum<'default' \| 'secondary' \| 'destructive' \| 'outline'>` | optional | Visual variant of the nav badge. Declared to match the objectui NavigationRenderer read (inverse-drift fix, liveness audit #1878/#1891/#1894). | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL). e.g. P`'org_admin' in current_user.positions` | +| **requiredPermissions** | `string[]` | optional | Permissions required to access this item | +| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime | +| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered | +| **type** | `'action'` | ✅ | | +| **actionDef** | `{ actionName: string; params?: Record }` | ✅ | Action definition to execute when clicked | + +### Nested Shape: `NavigationContribution.items[number][type='component']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | +| **label** | `string \| Record` | ✅ | Display proper label | +| **icon** | `string` | optional | Icon name | +| **order** | `number` | optional | Sort order within the same level (lower = first) | +| **badge** | `string \| number` | optional | Badge text or count displayed on the item | +| **badgeVariant** | `Enum<'default' \| 'secondary' \| 'destructive' \| 'outline'>` | optional | Visual variant of the nav badge. Declared to match the objectui NavigationRenderer read (inverse-drift fix, liveness audit #1878/#1891/#1894). | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL). e.g. P`'org_admin' in current_user.positions` | +| **requiredPermissions** | `string[]` | optional | Permissions required to access this item | +| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime | +| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered | +| **type** | `'component'` | ✅ | | +| **componentRef** | `string` | ✅ | Component registry key (e.g. "metadata:directory") | +| **params** | `Record` | optional | Props passed to the component | + +### Nested Shape: `NavigationContribution.items[number][type='separator']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'separator'` | ✅ | | +| **id** | `string` | optional | Optional id for the separator | +| **order** | `number` | optional | Sort order within the same level (lower = first) | + +### Nested Shape: `NavigationContribution.items[number][type='group']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | +| **label** | `string \| Record` | ✅ | Display proper label | +| **icon** | `string` | optional | Icon name | +| **order** | `number` | optional | Sort order within the same level (lower = first) | +| **badge** | `string \| number` | optional | Badge text or count displayed on the item | +| **badgeVariant** | `Enum<'default' \| 'secondary' \| 'destructive' \| 'outline'>` | optional | Visual variant of the nav badge. Declared to match the objectui NavigationRenderer read (inverse-drift fix, liveness audit #1878/#1891/#1894). | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL). e.g. P`'org_admin' in current_user.positions` | +| **requiredPermissions** | `string[]` | optional | Permissions required to access this item | +| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime | +| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered | +| **type** | `'group'` | ✅ | | +| **expanded** | `boolean` | optional (default: `false`) | Default expansion state in sidebar | +| **children** | `({ id: string; label: string \| Record; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| … +7 more)[]` | ✅ | Child navigation items | + --- diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index c62c37a6e1..f4237e5258 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -396,6 +396,36 @@ Sort field and direction pair | **data** | `{ provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source binding (ViewDataSchema — discriminated on `provider`: object \| api \| value \| schema). Static inline rows live at `{ provider: 'value', items: [...] }`; the bare-array shortcut is refused — see migration `object-grid-data-view-data-converged` | | **staticData** | `any[]` | optional | Deprecated bare-array static-rows shortcut the renderer still reads. Prefer `data: { provider: 'value', items: [...] }` | +### Nested Shape: `ObjectGridProps.data[provider='object']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **provider** | `'object'` | ✅ | | +| **object** | `string` | ✅ | Target object name | + +### Nested Shape: `ObjectGridProps.data[provider='api']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **provider** | `'api'` | ✅ | | +| **read** | `{ url: string; method: Enum<'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'>; headers?: Record; params?: Record; … }` | optional | Configuration for fetching data | +| **write** | `{ url: string; method: Enum<'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'>; headers?: Record; params?: Record; … }` | optional | Configuration for submitting data (for forms/editable tables) | + +### Nested Shape: `ObjectGridProps.data[provider='value']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **provider** | `'value'` | ✅ | | +| **items** | `any[]` | ✅ | Static data array | + +### Nested Shape: `ObjectGridProps.data[provider='schema']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **provider** | `'schema'` | ✅ | | +| **schemaId** | `string` | ✅ | Schema identifier — typically the metadata type name | +| **schema** | `Record` | optional | Inline JSON Schema (Draft 2020-12). Optional when schemaId is resolvable. | + --- diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index f1e7c5e014..a88e24990d 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -362,6 +362,36 @@ View filter rule | **defaults** | `Record` | optional | Initial field values for create-mode forms (folded into ObjectUI ObjectForm initial values; framework#1894 / #2998). | | **aria** | `never` | optional | [REMOVED] `form.aria` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no form renderer ever applied it, so declared ARIA attributes silently did not reach the DOM. Delete the key. The form renderer emits its own semantic markup; report gaps as renderer issues rather than per-view attribute overrides. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +### Nested Shape: `FormView.data[provider='object']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **provider** | `'object'` | ✅ | | +| **object** | `string` | ✅ | Target object name | + +### Nested Shape: `FormView.data[provider='api']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **provider** | `'api'` | ✅ | | +| **read** | `{ url: string; method?: Enum<'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'>; headers?: Record; params?: Record; … }` | optional | Configuration for fetching data | +| **write** | `{ url: string; method?: Enum<'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'>; headers?: Record; params?: Record; … }` | optional | Configuration for submitting data (for forms/editable tables) | + +### Nested Shape: `FormView.data[provider='value']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **provider** | `'value'` | ✅ | | +| **items** | `any[]` | ✅ | Static data array | + +### Nested Shape: `FormView.data[provider='schema']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **provider** | `'schema'` | ✅ | | +| **schemaId** | `string` | ✅ | Schema identifier — typically the metadata type name | +| **schema** | `Record` | optional | Inline JSON Schema (Draft 2020-12). Optional when schemaId is resolvable. | + ### Nested Shape: `FormView.sections[number]` | Property | Type | Required | Description | @@ -417,6 +447,14 @@ View filter rule | **expiresAt** | `string` | optional | Expiration date/time in ISO 8601 format | | **allowAnonymous** | `boolean` | optional (default: `false`) | Allow access without authentication | +### Nested Shape: `FormView.submitBehavior[kind='redirect']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **kind** | `'redirect'` | ✅ | | +| **url** | `string` | ✅ | Where the browser goes after a successful submit. Ruled 2026-08-11 (#7496): (1) RELATIVE paths only — it must start with `/`, and absolute or protocol-relative URLs are refused, which is what closes the open-redirect face; (2) interpolation ONLY from declared record fields, spelled `{{record.field_name}}`, and every interpolated value is URL-escaped when the redirect is built; (3) a verbatim redirect on the resolved relative path is the intended consumption. To send the browser OUT of the app, use an app navigation item (`{ type: 'url', url }`) instead. | +| **delayMs** | `integer` | optional | | + ### Nested Shape: `FormView.buttons` | Property | Type | Required | Description | @@ -703,6 +741,36 @@ Map view configuration | **bordered** | `never` | optional | [REMOVED] `view.bordered` was removed in @objectstack/spec 17.0.0 (#7176, ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it (the grid frame is the renderer's own constant, not authorable). Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | | **virtualScroll** | `never` | optional | [REMOVED] `view.virtualScroll` was removed in @objectstack/spec 17.0.0 (#7176, ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no grid ever virtualized off it; authoring it was a parse-clean no-op. Delete the key; large datasets page via `pagination`. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +### Nested Shape: `ListView.data[provider='object']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **provider** | `'object'` | ✅ | | +| **object** | `string` | ✅ | Target object name | + +### Nested Shape: `ListView.data[provider='api']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **provider** | `'api'` | ✅ | | +| **read** | `{ url: string; method?: Enum<'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'>; headers?: Record; params?: Record; … }` | optional | Configuration for fetching data | +| **write** | `{ url: string; method?: Enum<'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'>; headers?: Record; params?: Record; … }` | optional | Configuration for submitting data (for forms/editable tables) | + +### Nested Shape: `ListView.data[provider='value']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **provider** | `'value'` | ✅ | | +| **items** | `any[]` | ✅ | Static data array | + +### Nested Shape: `ListView.data[provider='schema']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **provider** | `'schema'` | ✅ | | +| **schemaId** | `string` | ✅ | Schema identifier — typically the metadata type name | +| **schema** | `Record` | optional | Inline JSON Schema (Draft 2020-12). Optional when schemaId is resolvable. | + ### Nested Shape: `ListView.columns[number]` | Property | Type | Required | Description | @@ -1068,6 +1136,36 @@ Tab configuration for multi-tab view interface | **virtualScroll** | `never` | optional | [REMOVED] `view.virtualScroll` was removed in @objectstack/spec 17.0.0 (#7176, ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no grid ever virtualized off it; authoring it was a parse-clean no-op. Delete the key; large datasets page via `pagination`. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | | **userFilters** | `{ element?: Enum<'dropdown' \| 'toggle'>; fields?: object[] }` | optional | | +### Nested Shape: `ObjectListView.data[provider='object']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **provider** | `'object'` | ✅ | | +| **object** | `string` | ✅ | Target object name | + +### Nested Shape: `ObjectListView.data[provider='api']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **provider** | `'api'` | ✅ | | +| **read** | `{ url: string; method?: Enum<'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'>; headers?: Record; params?: Record; … }` | optional | Configuration for fetching data | +| **write** | `{ url: string; method?: Enum<'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'>; headers?: Record; params?: Record; … }` | optional | Configuration for submitting data (for forms/editable tables) | + +### Nested Shape: `ObjectListView.data[provider='value']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **provider** | `'value'` | ✅ | | +| **items** | `any[]` | ✅ | Static data array | + +### Nested Shape: `ObjectListView.data[provider='schema']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **provider** | `'schema'` | ✅ | | +| **schemaId** | `string` | ✅ | Schema identifier — typically the metadata type name | +| **schema** | `Record` | optional | Inline JSON Schema (Draft 2020-12). Optional when schemaId is resolvable. | + ### Nested Shape: `ObjectListView.columns[number]` | Property | Type | Required | Description | diff --git a/packages/spec/scripts/lib/format-type.ts b/packages/spec/scripts/lib/format-type.ts index 499dd2aa18..b101a4b34b 100644 --- a/packages/spec/scripts/lib/format-type.ts +++ b/packages/spec/scripts/lib/format-type.ts @@ -335,7 +335,7 @@ function isNeverNode(prop: any): boolean { * a numeric `enum`), so a node-level `type` test would mis-render the mixed * case that a per-value test gets right for free. */ -function formatLiteral(value: unknown): string { +export function formatLiteral(value: unknown): string { // The only kind that IS quoted — and it keeps its quotes exactly as before. if (typeof value === 'string') return `'${value}'`; // `z.literal(null)` → `{ type: 'null', const: null }`. `null` is a keyword, @@ -577,6 +577,19 @@ export interface NestedShape { * for the docs: `Props['items'][number]` IS the element type. That is what * lets a heading name the shape without a second omission/relocation sigil in * a table that already carries `…` and `… +N more`. + * + * ## The variant selector (#12316) + * + * A property whose type is a union of two or more object shapes has no single + * "the shape of this property", so one more segment joins the composition at + * the point the walk crosses the union — `[type='sidebar']`, or `[option 2]` + * where the union carries no discriminant. It is a SELECTOR and not an index: + * it sits exactly where the union sits in the wrapper stack, so + * `on[string][option 3][number]` reads left to right as *the record value, its + * third option, an element of it* — the same reading `[string][number]` + * already has. See `nestedShapesOf` for why the two spellings, and why the + * segment is absent from every single-shape accessor (all 1469 of them keep + * the byte-identical heading they had). */ accessor: string; /** Live (non-tombstone) key names, in declaration order. */ @@ -612,16 +625,14 @@ export interface NestedShape { * one shape, and all three reach it here the same way `renderType` does: * anonymous `$defs` expanded, tombstones filtered, cycles refused. * - * ## Why more than one shape returns `null` + * ## Why more than one shape still returns `null` HERE * - * `formatPropertyType`'s vocabulary relocation is matched to its cell CONDITION - * FOR CONDITION, and refuses the positions where "the allowed values of this - * property" would not be the whole truth. Same rule here: a property whose type - * is a union of several object shapes has no single "the shape of this - * property" to name — `App.navigation`'s nine variants would need a variant - * index in the heading, i.e. a second addressing notation — so those keep the - * rendering they have. Measured on the tree at the time of the fix: 28 of 1293 - * shape-opening property rows are in that state. + * Those rows are no longer refused by the renderer — `nestedShapesOf` names all + * of them and #12316 gives each its own sub-table — but they have no single + * answer to give THIS function, whose whole signature is "the one shape". A + * caller wanting the multi-shape rows calls `nestedShapesOf` and reads its + * length; a caller asking this question gets `null`, which is the truthful + * answer to it and the one `nested-shape.test.ts` pins in both directions. * * A NAMED `$ref` also returns `null`, and a self `$ref` (`"#"`) with it: both * resolve to a schema with its own `## Section` on some page, where the keys @@ -630,61 +641,175 @@ export interface NestedShape { * this is the branch that keeps the rule true if that changes.) */ export function nestedShapeOf(prop: any, ctx?: TypeContext): NestedShape | null { - const found: NestedShape[] = []; + // ONE walk, two readings — they cannot drift apart about what "one shape + // level" means, the same way `formatPropertyType`'s cell and its relocation + // come from one `elideEnum` call. + const found = nestedShapesOf(prop, ctx); + return found.length === 1 ? found[0] : null; +} + +/** + * Which key of a union identifies its variants to an author — the key every + * shape-bearing variant pins to a DIFFERENT literal. + * + * Both halves are load-bearing. "Present on every variant with a `const`" is + * what makes the key readable as a selector at all; "different on each" is what + * makes the selector name ONE variant. A union where two variants pin the same + * value has no discriminant by this test even though `type` is a `const` on + * both — and answering `type` there would emit two identical headings, i.e. + * two identical anchors on one page, the defect the `Schema.key` qualification + * of `### Allowed Values:` exists to prevent. + * + * Read from the VARIANT node rather than from the shape found beneath it: a + * variant may be an array or a record whose element carries the keys, and + * `Transition[]` has no `const` anywhere on the node the author selects. + * + * First qualifying key in declaration order. Zod emits the discriminator first + * for `z.discriminatedUnion()`, so declaration order is the author's own answer + * to "which key tells these apart" rather than a guess this function makes. + */ +function discriminantKeyOf(variants: any[], perVariant: NestedShape[][]): string | null { + const contributing = variants.filter((_, index) => perVariant[index].length > 0); + if (contributing.length < 2) return null; + + const first = contributing[0]; + if (!first?.properties || typeof first.properties !== 'object') return null; + + for (const key of Object.keys(first.properties)) { + const values: string[] = []; + let qualifies = true; + for (const variant of contributing) { + const child = variant?.properties?.[key]; + if (!child || typeof child !== 'object' || child.const === undefined) { + qualifies = false; + break; + } + values.push(formatLiteral(child.const)); + } + if (qualifies && new Set(values).size === contributing.length) return key; + } + return null; +} - const walk = (node: any, expanding: Set, accessor: string, guard: number): void => { - if (!node || typeof node !== 'object' || guard > 8) return; - // Two or more already: the answer is `null` whatever else we find. - if (found.length > 1) return; +/** + * EVERY shape a property opens at depth 0, each with the accessor that selects + * it — the general answer `nestedShapeOf` reads its single-shape case out of. + * + * ## What #12309 left on the floor, and why it is safe to pick up now (#12316) + * + * #12309 refused a property that opens two or more object shapes, for a reason + * that was true when it was written: "a heading would need a variant index, + * i.e. a second addressing notation". Measured then and re-measured on + * `origin/main` at `7bd6447`: **28 of the 8604 rendered property rows** are in + * that state, all 28 with describe text on at least one variant — + * `ui/App.navigation` and `ui/NavigationContribution.items` with nine variants + * each, `data/ConditionalValidation.then` / `.otherwise` with five, and so on + * down to the four two-variant rows. + * + * What makes the index unnecessary is that the accessor is a COMPOSITION, not a + * name. Every segment it already has answers "which way down did the walk go" — + * `[number]` for an element, `[string]` for a record value — and a union is one + * more way down. So the variant segment joins the composition at the union's + * own position rather than being appended to the finished path, and the grammar + * gains a segment instead of gaining a notation. + * + * ## The two spellings, and why the discriminant one is preferred + * + * `[type='sidebar']` states what the AUTHOR WRITES to select that variant: the + * literal is the same `formatLiteral` spelling the Type cell prints two lines + * above, so a reader copying `type: 'sidebar'` off the heading is copying the + * schema's own answer. It is also STABLE — reordering the union, or adding a + * tenth navigation variant, moves no existing heading and therefore breaks no + * existing anchor. 22 of the 28 rows have one. + * + * `[option 2]` is the fallback for the six that do not, and it is deliberately + * the word the union branch of `schema-section.ts` has printed since long + * before this — `#### Option 2` under `### Union Options` — rather than a bare + * `[2]`, which in a stack of `[number]`/`[string]` segments would read as a + * TUPLE INDEX into the property's own type. It counts POSITION IN THE UNION, + * including variants that open no shape at all, so `string | { … } | { … }[]` + * numbers its objects 2 and 3: the number is then checkable against the Type + * cell the reader already has, which is the only thing that makes a positional + * selector honest. + * + * ## What does not change + * + * A property opening exactly ONE shape gets no selector segment — the union is + * not what identifies its shape, and 1469 of the 1497 shape-opening rows are in + * that state. This is why the stamp is conditioned on the union's OWN yield + * (`total >= 2`) and not on the row's: `string | { … }` crosses a union and + * still names its shape `Schema.key`, byte for byte as #12309 left it. + */ +export function nestedShapesOf(prop: any, ctx?: TypeContext): NestedShape[] { + const walk = (node: any, expanding: Set, accessor: string, guard: number): NestedShape[] => { + if (!node || typeof node !== 'object' || guard > 8) return []; // A tombstone accepts nothing; there is no shape under it. - if (isNeverNode(node)) return; + if (isNeverNode(node)) return []; if (node.$ref) { // Both spellings name a schema documented in its own section. - if (node.$ref === '#') return; + if (node.$ref === '#') return []; const name = refName(node.$ref); - if (!isAnonymousRef(name)) return; - if (expanding.has(name)) return; + if (!isAnonymousRef(name)) return []; + if (expanding.has(name)) return []; const target = ctx?.defs?.[name]; - if (!target) return; + if (!target) return []; const next = new Set(expanding); next.add(name); - walk({ ...target, $ref: undefined }, next, accessor, guard + 1); - return; + return walk({ ...target, $ref: undefined }, next, accessor, guard + 1); } // Checked before the object branch for the same reason `renderType` checks // them there: a vocabulary or a literal is a leaf, never a shape. - if (node.enum || node.const !== undefined) return; + if (node.enum || node.const !== undefined) return []; if (node.type === 'array') { - walk(node.items, expanding, `${accessor}[number]`, guard + 1); - return; + return walk(node.items, expanding, `${accessor}[number]`, guard + 1); } if (Array.isArray(node.anyOf) || Array.isArray(node.oneOf)) { - for (const variant of node.anyOf || node.oneOf) { - walk(variant, new Set(expanding), accessor, guard + 1); - } - return; + const variants: any[] = node.anyOf || node.oneOf; + const perVariant = variants.map(variant => walk(variant, new Set(expanding), accessor, guard + 1)); + const total = perVariant.reduce((sum, shapes) => sum + shapes.length, 0); + // One shape (or none) below this union: the union is not what identifies + // it, so it contributes no segment. `string | { … }` keeps `Schema.key`. + if (total < 2) return perVariant.flat(); + + const discriminant = discriminantKeyOf(variants, perVariant); + const selected: NestedShape[] = []; + perVariant.forEach((shapes, index) => { + // A variant that opens no shape gets no selector — and the guard is not + // just an optimisation: `discriminantKeyOf` qualifies its key on the + // CONTRIBUTING variants only, so a `string` arm of + // `string | { type: 'a' … } | { type: 'b' … }` has no `properties` for + // the selector to read and computing one here would throw. + if (shapes.length === 0) return; + const selector = discriminant + ? `[${discriminant}=${formatLiteral(variants[index].properties[discriminant].const)}]` + : `[option ${index + 1}]`; + for (const shape of shapes) { + // Spliced in at the UNION's position, not appended to the finished + // path: everything the walk composed below this union came from + // inside the selected variant and has to read as such. + selected.push({ ...shape, accessor: accessor + selector + shape.accessor.slice(accessor.length) }); + } + }); + return selected; } if (node.type === 'object' || node.properties || node.additionalProperties) { const keys = node.properties ? Object.keys(node.properties).filter(k => !isNeverNode(node.properties[k])) : []; - if (keys.length > 0) { - found.push({ node, accessor, keys }); - return; - } + if (keys.length > 0) return [{ node, accessor, keys }]; if (node.additionalProperties && typeof node.additionalProperties === 'object') { - walk(node.additionalProperties, expanding, `${accessor}[string]`, guard + 1); + return walk(node.additionalProperties, expanding, `${accessor}[string]`, guard + 1); } } + return []; }; - walk(prop, new Set(ctx?.expanding ?? []), '', 0); - return found.length === 1 ? found[0] : null; + return walk(prop, new Set(ctx?.expanding ?? []), '', 0); } /** diff --git a/packages/spec/scripts/lib/schema-section.ts b/packages/spec/scripts/lib/schema-section.ts index a7872299cf..2ce01694fc 100644 --- a/packages/spec/scripts/lib/schema-section.ts +++ b/packages/spec/scripts/lib/schema-section.ts @@ -16,7 +16,7 @@ import { escapeMdxDescription } from './escape-mdx'; import { formatPropertyType, formatType, - nestedShapeOf, + nestedShapesOf, type NestedShape, type TypeContext, } from './format-type'; @@ -299,14 +299,25 @@ export function renderSchemaSection(schemaName: string, schema: any, ctx: Sectio : { cell: formatType(prop, { ...typeCtx, inShapeSummary: true }), allowedValues: null }; if (allowedValues) relocated.push({ key, members: allowedValues }); if (expandNested) { - const shape = nestedShapeOf(prop, typeCtx); - // Only when there is text to relocate. The cell already states the - // shape's KEYS (four of them, then `…`) and their types; what it - // structurally cannot state is a description, so a table carrying - // none would restate the cell in more space. Measured on the tree - // at the time of the fix: 1208 of the 1293 shape-opening property - // rows carry at least one described key. - if (shape && carriesDescription(shape)) { + // EVERY shape the cell opens, not just the single-shape case + // (#12316). A property whose type is a union of object shapes — + // `ui/App.navigation`'s nine variants, `data/ConditionalValidation`'s + // five — gets one sub-table per variant, each under the accessor + // path that selects that variant (`navigation[number][type='sidebar']`). + // The loop IS the whole change on this side: #12309 already emitted + // a list of tables, it was `nestedShapeOf` that could only ever put + // one thing in it. + for (const shape of nestedShapesOf(prop, typeCtx)) { + // Only where there is text to relocate, and decided PER VARIANT. + // The cell already states the shape's KEYS (four of them, then + // `…`) and their types; what it structurally cannot state is a + // description, so a table carrying none would restate the cell in + // more space. Applying it per variant is what keeps the rule the + // same rule: `ui/FormView.submitBehavior` opens four shapes and + // exactly one of them carries prose, so it gets exactly one + // table. Measured on the tree at the time of #12309: 1208 of the + // 1293 shape-opening rows carry at least one described key. + if (!carriesDescription(shape)) continue; const own = typeof shape.node.description === 'string' ? shape.node.description : ''; nested.push({ // Qualified by schema AND property, for the reason the @@ -315,10 +326,16 @@ export function renderSchemaSection(schemaName: string, schema: any, ctx: Sectio // give it two identical anchors. `schemaName` and not a // threaded `owner` parameter — a nested table opens no // relocation and no sub-table of its own, so an owner threaded - // into one would be a parameter nothing ever reads. + // into one would be a parameter nothing ever reads. The variant + // selector inside `accessor` is what extends that same + // qualification down one more level, so nine sub-tables under + // one property still carry nine distinct anchors. path: `${schemaName}.${key}${shape.accessor}`, // The element/value node's OWN describe, when it is not simply - // the property's — that one is already in the row above. + // the property's — that one is already in the row above. On a + // union this is the VARIANT's describe, which is the line that + // says what distinguishes it from its eight siblings, so it + // earns its position here more than it does on a lone shape. ownDescription: own && own !== prop.description ? own : '', shape, }); diff --git a/packages/spec/scripts/nested-shape.test.ts b/packages/spec/scripts/nested-shape.test.ts index 809decf110..822f875f8f 100644 --- a/packages/spec/scripts/nested-shape.test.ts +++ b/packages/spec/scripts/nested-shape.test.ts @@ -30,11 +30,22 @@ * — turns the seven rendering cases in the second block red and leaves the * `nestedShapeOf` structural block red too; the population census at the bottom * is what tells the two apart. + * + * THE MULTI-SHAPE REMAINDER (#12316). #11601 shipped with one row-shape left + * refused: a property whose type is a union of TWO OR MORE object shapes, which + * had no single "the shape of this property" for a heading to name. The last + * two blocks below pin the variant selector that lifts it — `[type='sidebar']` + * where the union has a discriminant, `[option 2]` where it does not — and, as + * importantly, pin that a SINGLE-shape row still gets no selector, which is the + * whole reason the 1469 headings #11601 published did not move. Re-measured on + * `origin/main@7bd6447`: 28 of 8604 rendered property rows are multi-shape, all + * 28 with describe text on at least one variant; publishing them adds 113 + * sub-tables across 15 pages, 1397 lines, and deletes nothing. */ import { describe, expect, it } from 'vitest'; -import { formatType, nestedShapeOf, type TypeContext } from './lib/format-type'; +import { formatType, nestedShapeOf, nestedShapesOf, type TypeContext } from './lib/format-type'; import { renderSchemaSection } from './lib/schema-section'; /** @@ -130,14 +141,19 @@ describe('nestedShapeOf — which shape a cell opens, and how it is addressed', }); it('refuses a union of TWO object shapes — there is no single shape to name', () => { - const shape = nestedShapeOf({ + // Still `null`, and deliberately so after #12316: this function's whole + // signature is "the ONE shape", and a two-shape union has no answer to give + // it. What changed is that the RENDERER no longer asks this question — it + // asks `nestedShapesOf`, pinned in the block below, which names both. + const prop = { anyOf: [ { type: 'object', properties: { id: { type: 'string', description: 'Item id' } } }, { type: 'object', properties: { type: { type: 'string', const: 'separator' } } }, ], - }); + }; - expect(shape).toBeNull(); + expect(nestedShapeOf(prop)).toBeNull(); + expect(nestedShapesOf(prop)).toHaveLength(2); }); it('refuses a leaf — scalar, vocabulary, literal, array of scalars', () => { @@ -351,3 +367,350 @@ describe('nestedShapeOf agrees with the cell it completes', () => { } }); }); + +/** + * `ui/App.navigation` reduced to three of its nine variants — an ARRAY of a + * union discriminated on `type`, which is the census's most common multi-shape + * shape (22 of the 28 rows have a discriminant, 12 of them behind `[number]`). + */ +const APP_NAVIGATION = { + type: 'object', + properties: { + navigation: { + type: 'array', + description: 'Navigation items', + items: { + anyOf: [ + { + type: 'object', + properties: { + type: { type: 'string', const: 'object' }, + objectName: { type: 'string', description: 'Target object name' }, + }, + required: ['type', 'objectName'], + }, + { + type: 'object', + properties: { + type: { type: 'string', const: 'separator' }, + order: { type: 'number', description: 'Sort order within the same level' }, + }, + }, + { + type: 'object', + properties: { + type: { type: 'string', const: 'url' }, + url: { type: 'string', description: 'External URL to open' }, + }, + }, + ], + }, + }, + }, + required: ['navigation'], +}; + +/** + * `automation/StateMachine.on` reduced: a RECORD whose value is + * `string | Transition | Transition[]`. Two object shapes, no discriminant, and + * the two are reached at DIFFERENT depths — the case that decides whether the + * selector is appended to the finished path or spliced in where the union sits. + */ +const STATE_MACHINE_ON = { + type: 'object', + properties: { + on: { + type: 'object', + additionalProperties: { + anyOf: [ + { type: 'string' }, + { type: 'object', properties: { target: { type: 'string', description: 'Target State ID' } } }, + { + type: 'array', + items: { type: 'object', properties: { target: { type: 'string', description: 'Target State ID' } } }, + }, + ], + }, + }, + }, +}; + +describe('nestedShapesOf — naming every shape a multi-shape row opens (#12316)', () => { + it('selects a discriminated variant by the literal the author writes', () => { + const shapes = nestedShapesOf(APP_NAVIGATION.properties.navigation); + + // The selector joins the composition AFTER `[number]`, because that is + // where the union sits: an element of the array, then one variant of it. + expect(shapes.map(s => s.accessor)).toEqual([ + "[number][type='object']", + "[number][type='separator']", + "[number][type='url']", + ]); + expect(shapes[0].keys).toEqual(['type', 'objectName']); + }); + + it('spells the literal the way the Type cell two lines above spells it', () => { + // One `formatLiteral`, so a reader copying `type: 'object'` off the heading + // is copying the same spelling the cell printed — quotes on a string, none + // on a number. A numeric discriminant is what makes this testable at all: + // an unconditional `'…'` wrapper (the #5729 defect) would quote it. + const shapes = nestedShapesOf({ + anyOf: [ + { type: 'object', properties: { v: { type: 'number', const: 1 }, a: { type: 'string', description: 'a' } } }, + { type: 'object', properties: { v: { type: 'number', const: 2 }, b: { type: 'string', description: 'b' } } }, + ], + }); + + expect(shapes.map(s => s.accessor)).toEqual(['[v=1]', '[v=2]']); + }); + + it('splices the selector in AT THE UNION, not onto the end of the path', () => { + const shapes = nestedShapesOf(STATE_MACHINE_ON.properties.on); + + // `[string][option 3][number]` reads left to right as *the record value, + // its third option, an element of it*. Appending would have produced + // `[string][number][option 3]`, which claims the union is below the array. + expect(shapes.map(s => s.accessor)).toEqual(['[string][option 2]', '[string][option 3][number]']); + }); + + it('counts POSITION IN THE UNION, scalar arms included', () => { + // The numbers are 2 and 3, not 1 and 2: the `string` arm is `[option 1]`. + // That is what makes a positional selector checkable — the reader counts + // the same `string | { … } | { … }[]` cell the table sits under. Numbering + // only the arms that open shapes would be a private ordering nothing on the + // page states. + const shapes = nestedShapesOf(STATE_MACHINE_ON.properties.on); + + expect(shapes.map(s => s.accessor.match(/\[option (\d+)\]/)![1])).toEqual(['2', '3']); + }); + + it('falls back to the positional selector when a `const` key repeats a value', () => { + // `type` is a `const` on both, so "present everywhere with a const" alone + // would answer `type` — and emit two IDENTICAL headings, i.e. two identical + // anchors on one page. Distinctness is the half of the test that prevents + // it, and the fallback is what it falls back TO. + const shapes = nestedShapesOf({ + anyOf: [ + { type: 'object', properties: { type: { type: 'string', const: 'row' }, a: { type: 'string', description: 'a' } } }, + { type: 'object', properties: { type: { type: 'string', const: 'row' }, b: { type: 'string', description: 'b' } } }, + ], + }); + + expect(shapes.map(s => s.accessor)).toEqual(['[option 1]', '[option 2]']); + }); + + it('reads the discriminant off the VARIANT, so an array arm cannot claim one', () => { + // The `const` lives on the element, not on the node the author selects, so + // `Transition[]` has no discriminant to read and the whole union falls back. + const shapes = nestedShapesOf({ + anyOf: [ + { type: 'object', properties: { type: { type: 'string', const: 'one' }, a: { type: 'string', description: 'a' } } }, + { + type: 'array', + items: { type: 'object', properties: { type: { type: 'string', const: 'many' }, b: { type: 'string', description: 'b' } } }, + }, + ], + }); + + expect(shapes.map(s => s.accessor)).toEqual(['[option 1]', '[option 2][number]']); + }); + + it('adds NO selector to a single-shape row — the #11601 headings do not move', () => { + // The stamp is conditioned on the UNION's own yield, never on the row's, so + // every one of the 1469 single-shape rows keeps its byte-identical heading. + // This is the assertion that makes the regenerated tree purely additive. + expect(nestedShapesOf({ anyOf: [{ type: 'string' }, { type: 'object', properties: { dialect: { type: 'string' } } }] }) + .map(s => s.accessor)).toEqual(['']); + expect(nestedShapesOf(PAGE_TABS_PROPS.properties.items).map(s => s.accessor)).toEqual(['[number]']); + }); + + it('is the ONE walk `nestedShapeOf` reads its answer out of', () => { + // Two readings of one call, so they cannot drift apart about what "one + // shape level" means — the same construction `formatPropertyType` uses for + // its cell and its relocation. + const single = PAGE_TABS_PROPS.properties.items; + expect(nestedShapeOf(single)).toEqual(nestedShapesOf(single)[0]); + + for (const none of [{ type: 'string' }, { not: {} }, { $ref: '#' }]) { + expect(nestedShapesOf(none)).toEqual([]); + expect(nestedShapeOf(none)).toBeNull(); + } + }); + + it('still refuses the shapes #11601 refused, whatever the arity', () => { + // A union of NAMED refs opens no shape at all — both are published in their + // own sections. The multi-shape lift is about anonymous inline shapes. + const ctx: TypeContext = { defs: {}, currentSchema: 'View' }; + expect(nestedShapesOf({ anyOf: [{ $ref: '#/$defs/Field' }, { $ref: '#/$defs/Layout' }] }, ctx)).toEqual([]); + // Tombstones too: a union arm that accepts nothing is not a variant. + expect(nestedShapesOf({ anyOf: [{ not: {} }, { not: {} }] })).toEqual([]); + }); +}); + +describe('renderSchemaSection — a sub-table per variant reaches the page (#12316)', () => { + const md = renderSchemaSection('App', APP_NAVIGATION); + + it("THE ACCEPTANCE: every variant's describe text is on the page", () => { + for (const text of ['Target object name', 'Sort order within the same level', 'External URL to open']) { + expect(md).toContain(text); + } + }); + + it('gives each variant its own heading under the variant-indexed accessor', () => { + expect(md).toContain("### Nested Shape: `App.navigation[number][type='object']`"); + expect(md).toContain("### Nested Shape: `App.navigation[number][type='separator']`"); + expect(md).toContain("### Nested Shape: `App.navigation[number][type='url']`"); + }); + + it('emits ONE heading per variant and no bare unqualified one', () => { + const headings = md.match(/^### Nested Shape: .*$/gm) ?? []; + expect(headings).toHaveLength(3); + expect(new Set(headings).size).toBe(3); + // The heading a reader could mistake for "the shape of this property" — + // the exact claim a multi-shape row cannot make — is never emitted. + expect(md).not.toContain('### Nested Shape: `App.navigation[number]`\n'); + }); + + it('leaves the collapsed signature cell exactly where it was', () => { + // Additive, the same way #11601 was: the union summary still states the + // arity the reader counts the `[option N]` selectors against. + expect(md).toContain( + "| **navigation** | `({ type: 'object'; objectName: string } \\| { type?: 'separator'; order?: number } \\| { type?: 'url'; url?: string })[]` | ✅ | Navigation items |", + ); + }); + + it('decides "is there text to publish" PER VARIANT, not per row', () => { + // `ui/FormView.submitBehavior` in miniature: four shapes, one with prose. + // The rule #11601 wrote — a table only where the cell cannot carry the text + // — is the same rule, applied one level finer. Publishing all four would + // restate the cell three times over. + const mixed = renderSchemaSection('FormView', { + type: 'object', + properties: { + submitBehavior: { + anyOf: [ + { type: 'object', properties: { kind: { type: 'string', const: 'stay' }, toast: { type: 'boolean' } } }, + { + type: 'object', + properties: { + kind: { type: 'string', const: 'navigate' }, + to: { type: 'string', description: 'Route to open after a successful submit' }, + }, + }, + ], + }, + }, + }); + + expect(mixed).toContain("### Nested Shape: `FormView.submitBehavior[kind='navigate']`"); + expect(mixed).not.toContain("[kind='stay']"); + expect(mixed.match(/^### Nested Shape: /gm) ?? []).toHaveLength(1); + }); + + it("carries the VARIANT's own describe under its heading", () => { + // On a union this line is what says how this variant differs from its + // siblings, so it earns the position more than it does on a lone shape. + const described = renderSchemaSection('Job', { + type: 'object', + properties: { + schedule: { + anyOf: [ + { + type: 'object', + description: 'Run on a cron expression', + properties: { type: { type: 'string', const: 'cron' }, expr: { type: 'string', description: 'Cron expression' } }, + }, + { + type: 'object', + description: 'Run once at a fixed instant', + properties: { type: { type: 'string', const: 'once' }, at: { type: 'string', description: 'ISO-8601 instant' } }, + }, + ], + }, + }, + }); + + expect(described).toContain("### Nested Shape: `Job.schedule[type='cron']`\n\nRun on a cron expression\n"); + expect(described).toContain("### Nested Shape: `Job.schedule[type='once']`\n\nRun once at a fixed instant\n"); + }); + + it('still stops at ONE level — a variant table opens no table of its own', () => { + // `SHAPE_DEPTH_LIMIT` is untouched by #12316. What was lifted is the + // refusal at level 1, not the budget: a shape nested inside a VARIANT is + // exactly as unreachable as it was inside a lone shape. + const deep = renderSchemaSection('Deep', { + type: 'object', + properties: { + outer: { + anyOf: [ + { + type: 'object', + properties: { + type: { type: 'string', const: 'a' }, + // The variant's OWN described key, so this variant qualifies + // for a table on its own account — otherwise the case would + // prove nothing about depth, only about `carriesDescription`. + label: { type: 'string', description: 'one level down, on the discriminated variant' }, + inner: { type: 'object', properties: { leaf: { type: 'string', description: 'two levels down' } } }, + }, + }, + { + type: 'object', + properties: { type: { type: 'string', const: 'b' }, flag: { type: 'boolean', description: 'one level down' } }, + }, + ], + }, + }, + }); + + expect(deep.match(/^### Nested Shape: /gm) ?? []).toHaveLength(2); + expect(deep).toContain('one level down, on the discriminated variant'); + expect(deep).toContain('one level down'); + // `inner` is a level BELOW a variant, and the budget was spent reaching the + // variant. Its `leaf` describe is exactly as unreachable as #11601 left it. + expect(deep).not.toContain('two levels down'); + expect(deep).not.toContain("[type='a'].inner"); + }); + + it('does not relocate a vocabulary out of a variant table either', () => { + // A variant table is a THIRD position for those keys; #6225's relocation + // budget is still only spendable where the vocabulary's authoritative copy + // lives. Same `expandNested = false` flag, one more caller. + const wide = Array.from({ length: 60 }, (_, i) => `code_${i}`); + const withVocabulary = renderSchemaSection('Envelope', { + type: 'object', + properties: { + payload: { + anyOf: [ + { + type: 'object', + properties: { + kind: { type: 'string', const: 'error' }, + code: { type: 'string', enum: wide, description: 'Machine-readable error code' }, + }, + }, + { + type: 'object', + properties: { kind: { type: 'string', const: 'ok' }, value: { type: 'string', description: 'Result value' } }, + }, + ], + }, + }, + }); + + expect(withVocabulary).toContain("### Nested Shape: `Envelope.payload[kind='error']`"); + expect(withVocabulary).toContain('Machine-readable error code'); + expect(withVocabulary).not.toContain('### Allowed Values:'); + }); + + it('is purely ADDITIVE — a single-shape section renders byte for byte as #11601 left it', () => { + // The regression this guards is the one that would have made the whole + // change unreviewable: a selector leaking onto the 1469 single-shape rows. + // Measured on the whole tree: regenerating moved 15 files, +1397 / **-0** + // lines. + expect(renderSchemaSection('PageTabsProps', PAGE_TABS_PROPS)).toContain( + '### Nested Shape: `PageTabsProps.items[number]`', + ); + expect(renderSchemaSection('PageTabsProps', PAGE_TABS_PROPS)).not.toContain('[option '); + }); +});