diff --git a/.changeset/docs-nested-item-shape-tables.md b/.changeset/docs-nested-item-shape-tables.md new file mode 100644 index 0000000000..132459d05d --- /dev/null +++ b/.changeset/docs-nested-item-shape-tables.md @@ -0,0 +1,58 @@ +--- +"@objectstack/spec": patch +--- + +fix(spec): reference pages carry a nested item shape's `.describe()` text instead of collapsing it into a signature cell (#11601) + +`build-docs.ts` renders a property whose type is an inline object as a +one-line signature — `{ label: string; icon?: string; visibleWhen?: string | +object; value?: string; … }[]` — into a table cell that has **no description +column**. Every `.describe()` an author wrote on a key of that shape was +therefore unreachable from the reference page: not truncated, not marked, +absent. `page:tabs`'s item-level `visibleWhen` carries a ~600-character +contract note whose whole point is that its evaluation environment is **not** +the page-component `visibleWhen` of the same name, and +`content/docs/references/ui/component.mdx` rendered that row with an empty +Description cell. + +The loss was invisible from both sides. `check:docs` compares generated output +with committed output, so it is green forever on prose neither side contains — +measured on the tree before this change, adding a `.describe()` to a nested +item key produced a **zero-line** `gen:docs` diff. + +**The population, measured on the emitted tree.** 1293 property rows across +566 published schemas and 13 of 14 categories open a nested shape; 1208 of them +have at least one key carrying describe text, 7502 described keys in total, +~473 KB of authored prose that reached no page. + +**What is rendered now.** A property that opens exactly one shape, and whose +shape has at least one described key, gets a `### Nested Shape:` table directly +under the Properties table — the same position, addressing and heading level +the `### Allowed Values:` relocation has used since #6225, so the page gains no +second grammar. The heading names the shape with a TypeScript indexed accessor +(`PageTabsProps.items[number]`, `Object.fields[string]`), which is a real +spelling rather than a sigil invented for the docs. + +Four bounds, each measured rather than chosen: + +- **One level**, matching the `SHAPE_DEPTH_LIMIT` budget a cell already spends. + A nested table opens no table of its own. +- **Only where there is text to publish.** A shape whose keys carry no + describe text keeps its cell; a table there would restate the cell in more + space. +- **A union of two or more object shapes keeps its cell.** There is no single + "the shape of this property" to name — the same reason `formatPropertyType` + refuses to relocate a vocabulary out of `Enum<…>[]`. +- **A nested table does not relocate vocabularies.** It is a second position + for those keys, so it elides them the way a `{ … }` summary does. Without + this rule the 288-member `ApiError.code` vocabulary was re-listed under every + nested `error` shape — 20,260 bullet lines across the tree, `api/metadata.mdx` + alone +6097. + +Tombstoned keys are rendered in a nested table, unlike in the cell above it: +`retiredKey()` puts the whole `[REMOVED]` migration prescription in +`description`, and a signature has no column to carry it. + +The regenerated tree is **purely additive** — 143 files, +14195 / -118 lines, +and every one of the 38177 pre-existing lines is still present byte for byte +(the 118 are re-ordering around the inserted sections, not removal). diff --git a/content/docs/references/ai/agent.mdx b/content/docs/references/ai/agent.mdx index 2be4b08617..bc4a5f12ea 100644 --- a/content/docs/references/ai/agent.mdx +++ b/content/docs/references/ai/agent.mdx @@ -71,6 +71,68 @@ const result = AIModelConfigSchema.parse(data); | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `Agent.model` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **provider** | `Enum<'openai' \| 'azure_openai' \| 'anthropic' \| 'local'>` | optional (default: `"openai"`) | | +| **model** | `string` | ✅ | Model name (e.g. gpt-4, claude-3-opus) | +| **temperature** | `number` | optional (default: `0.7`) | | +| **maxTokens** | `number` | optional | | +| **topP** | `number` | optional | | + +### Nested Shape: `Agent.lifecycle` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique Machine ID | +| **description** | `string` | optional | | +| **contextSchema** | `Record` | optional | Zod Schema for the machine context/memory | +| **initial** | `string` | ✅ | Initial State ID | +| **states** | `Record; entry?: (string \| object)[]; exit?: (string \| object)[]; on?: Record; … }>` | ✅ | State Nodes | +| **on** | `Record` | optional | | + +### Nested Shape: `Agent.planning` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **maxIterations** | `integer` | optional (default: `10`) | Maximum planning loop iterations | + +### Nested Shape: `Agent.memory` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **longTerm** | `{ enabled: boolean; store: Enum<'vector' \| 'database' \| 'redis'>; maxEntries?: integer }` | optional | Long-term / persistent memory | +| **reflectionInterval** | `integer` | optional | Reflect every N interactions to improve behavior | + +### Nested Shape: `Agent.guardrails` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **maxTokensPerInvocation** | `integer` | optional | Token budget per single invocation | +| **maxExecutionTimeSec** | `integer` | optional | Max execution time in seconds | +| **blockedTopics** | `string[]` | optional | Forbidden topics or action names | + +### Nested Shape: `Agent.structuredOutput` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **format** | `Enum<'json_object' \| 'json_schema' \| 'regex' \| 'grammar' \| 'xml'>` | ✅ | Expected output format | +| **schema** | `Record` | optional | JSON Schema definition for output | +| **strict** | `boolean` | optional (default: `false`) | Enforce exact schema compliance | +| **retryOnValidationFailure** | `boolean` | optional (default: `true`) | Retry generation when output fails validation | +| **maxRetries** | `integer` | optional (default: `3`) | Maximum retries on validation failure | +| **fallbackFormat** | `Enum<'json_object' \| 'json_schema' \| 'regex' \| 'grammar' \| 'xml'>` | optional | Fallback format if primary format fails | +| **transformPipeline** | `Enum<'trim' \| 'parse_json' \| 'validate' \| 'coerce_types'>[]` | optional | Post-processing steps applied to output | + +### Nested Shape: `Agent.protection` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | ✅ | Lock policy — none \| no-overlay \| no-delete \| full. | +| **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | +| **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | + --- diff --git a/content/docs/references/ai/conversation.mdx b/content/docs/references/ai/conversation.mdx index 42b4867c9c..1bcc5cfb81 100644 --- a/content/docs/references/ai/conversation.mdx +++ b/content/docs/references/ai/conversation.mdx @@ -104,6 +104,30 @@ const result = CodeContentSchema.parse(data); | **embedding** | `number[]` | optional | Vector embedding for semantic search | | **metadata** | `Record` | optional | | +### Nested Shape: `ConversationMessage.functionCall` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Function name | +| **arguments** | `string` | ✅ | JSON string of function arguments | +| **result** | `string` | optional | Function execution result | + +### Nested Shape: `ConversationMessage.toolCalls[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Tool call ID | +| **type** | `Enum<'function'>` | optional (default: `"function"`) | | +| **function** | `{ name: string; arguments: string; result?: string }` | ✅ | | + +### Nested Shape: `ConversationMessage.tokens` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **promptTokens** | `integer` | ✅ | Tokens consumed by the prompt | +| **completionTokens** | `integer` | ✅ | Tokens generated in the completion | +| **totalTokens** | `integer` | ✅ | Total tokens (prompt + completion) | + --- @@ -128,6 +152,79 @@ const result = CodeContentSchema.parse(data); | **expiresAt** | `string` | optional | ISO 8601 timestamp | | **metadata** | `Record` | optional | | +### Nested Shape: `ConversationSession.context` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **sessionId** | `string` | ✅ | Conversation session ID | +| **userId** | `string` | optional | User identifier | +| **agentId** | `string` | optional | AI agent identifier | +| **object** | `string` | optional | Related object (e.g., "case", "project") | +| **recordId** | `string` | optional | Related record ID | +| **scope** | `Record` | optional | Additional context scope | +| **systemMessage** | `string` | optional | System prompt/instructions | +| **metadata** | `Record` | optional | | + +### Nested Shape: `ConversationSession.tokenBudget` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **maxTokens** | `integer` | ✅ | Maximum total tokens | +| **maxPromptTokens** | `integer` | optional | Max tokens for prompt | +| **maxCompletionTokens** | `integer` | optional | Max tokens for completion | +| **reserveTokens** | `integer` | optional (default: `500`) | Reserve tokens for system messages | +| **bufferPercentage** | `number` | optional (default: `0.1`) | Buffer percentage (0.1 = 10%) | +| **strategy** | `Enum<'fifo' \| 'importance' \| 'semantic' \| 'sliding_window' \| 'summary'>` | optional (default: `"sliding_window"`) | | +| **slidingWindowSize** | `integer` | optional | Number of recent messages to keep | +| **minImportanceScore** | `number` | optional | Minimum importance to keep | +| **semanticThreshold** | `number` | optional | Semantic similarity threshold | +| **enableSummarization** | `boolean` | optional (default: `false`) | Enable context summarization | +| **summarizationThreshold** | `integer` | optional | Trigger summarization at N tokens | +| **summaryModel** | `string` | optional | Model ID for summarization | +| **warnThreshold** | `number` | optional (default: `0.8`) | Warn at % of budget (0.8 = 80%) | + +### Nested Shape: `ConversationSession.messages[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique message ID | +| **timestamp** | `string` | ✅ | ISO 8601 timestamp | +| **role** | `Enum<'system' \| 'user' \| 'assistant' \| 'function' \| 'tool'>` | ✅ | | +| **content** | `({ type: 'text'; text: string; metadata?: Record } \| { type: 'image'; imageUrl: string; detail: Enum<'low' \| 'high' \| 'auto'>; metadata?: Record } \| { type: 'file'; fileUrl: string; mimeType: string; fileName?: string; … } \| { type: 'code'; text: string; language: string; metadata?: Record })[]` | ✅ | Message content (multimodal array) | +| **functionCall** | `{ name: string; arguments: string; result?: string }` | optional | Legacy function call | +| **toolCalls** | `{ id: string; type: Enum<'function'>; function: object }[]` | optional | Tool calls | +| **toolCallId** | `string` | optional | Tool call ID this message responds to | +| **name** | `string` | optional | Name of the function/user | +| **tokens** | `{ promptTokens: integer; completionTokens: integer; totalTokens: integer }` | optional | Token usage for this message | +| **cost** | `number` | optional | Cost for this message in USD | +| **pinned** | `boolean` | optional (default: `false`) | Prevent removal during pruning | +| **importance** | `number` | optional | Importance score for pruning | +| **embedding** | `number[]` | optional | Vector embedding for semantic search | +| **metadata** | `Record` | optional | | + +### Nested Shape: `ConversationSession.tokens` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **promptTokens** | `integer` | optional (default: `0`) | | +| **completionTokens** | `integer` | optional (default: `0`) | | +| **totalTokens** | `integer` | optional (default: `0`) | | +| **budgetLimit** | `integer` | ✅ | | +| **budgetUsed** | `integer` | optional (default: `0`) | | +| **budgetRemaining** | `integer` | ✅ | | +| **budgetPercentage** | `number` | ✅ | Usage as percentage of budget | +| **messageCount** | `integer` | optional (default: `0`) | | +| **prunedMessageCount** | `integer` | optional (default: `0`) | | +| **summarizedMessageCount** | `integer` | optional (default: `0`) | | + +### Nested Shape: `ConversationSession.totalTokens` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **promptTokens** | `integer` | ✅ | Tokens consumed by the prompt | +| **completionTokens** | `integer` | ✅ | Tokens generated in the completion | +| **totalTokens** | `integer` | ✅ | Total tokens (prompt + completion) | + --- @@ -381,6 +478,14 @@ This schema accepts one of the following structures: | **type** | `Enum<'function'>` | optional (default: `"function"`) | | | **function** | `{ name: string; arguments: string; result?: string }` | ✅ | | +### Nested Shape: `ToolCall.function` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Function name | +| **arguments** | `string` | ✅ | JSON string of function arguments | +| **result** | `string` | optional | Function execution result | + --- diff --git a/content/docs/references/ai/knowledge-source.mdx b/content/docs/references/ai/knowledge-source.mdx index 45ca5bfdb4..b602d6a3ef 100644 --- a/content/docs/references/ai/knowledge-source.mdx +++ b/content/docs/references/ai/knowledge-source.mdx @@ -87,6 +87,26 @@ const result = FileKnowledgeSourceSchema.parse(data); | **refresh** | `{ onRecordChange?: boolean; cron?: string }` | optional (default: `{}`) | | | **aiExposed** | `boolean` | optional (default: `true`) | | +### Nested Shape: `KnowledgeSource.embedding` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **provider** | `Enum<'openai' \| 'cohere' \| 'azure_openai' \| 'huggingface' \| 'local' \| 'custom'>` | ✅ | | +| **model** | `string` | ✅ | Provider-specific model identifier | +| **dimensions** | `integer` | ✅ | Embedding vector dimensions | +| **endpoint** | `string` | optional | Custom endpoint URL | +| **secretRef** | `string` | optional | Reference to stored API key secret | + +### Nested Shape: `KnowledgeSource.vectorStore` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **provider** | `Enum<'pgvector' \| 'chroma' \| 'qdrant' \| 'pinecone' \| 'weaviate' \| 'milvus' \| 'redis' \| …>` | ✅ | | +| **collection** | `string` | ✅ | Collection / index / namespace name | +| **endpoint** | `string` | optional | Connection string or endpoint URL | +| **secretRef** | `string` | optional | Reference to stored credential secret | +| **dimensions** | `integer` | optional | | + --- diff --git a/content/docs/references/ai/model-registry.mdx b/content/docs/references/ai/model-registry.mdx index 11f389a09a..7c5d52054f 100644 --- a/content/docs/references/ai/model-registry.mdx +++ b/content/docs/references/ai/model-registry.mdx @@ -65,6 +65,36 @@ const result = ModelCapabilitySchema.parse(data); | **deprecated** | `boolean` | optional (default: `false`) | | | **recommendedFor** | `string[]` | optional | Use case recommendations | +### Nested Shape: `ModelConfig.capabilities` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **textGeneration** | `boolean` | optional (default: `true`) | Supports text generation | +| **textEmbedding** | `boolean` | optional (default: `false`) | Supports text embedding | +| **imageGeneration** | `boolean` | optional (default: `false`) | Supports image generation | +| **imageUnderstanding** | `boolean` | optional (default: `false`) | Supports image understanding | +| **functionCalling** | `boolean` | optional (default: `false`) | Supports function calling | +| **codeGeneration** | `boolean` | optional (default: `false`) | Supports code generation | +| **reasoning** | `boolean` | optional (default: `false`) | Supports advanced reasoning | + +### Nested Shape: `ModelConfig.limits` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **maxTokens** | `integer` | ✅ | Maximum tokens per request | +| **contextWindow** | `integer` | ✅ | Context window size | +| **maxOutputTokens** | `integer` | optional | Maximum output tokens | +| **rateLimit** | `{ requestsPerMinute?: integer; tokensPerMinute?: integer }` | optional | | + +### Nested Shape: `ModelConfig.pricing` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **currency** | `string` | optional (default: `"USD"`) | | +| **inputCostPer1kTokens** | `number` | optional | Cost per 1K input tokens | +| **outputCostPer1kTokens** | `number` | optional | Cost per 1K output tokens | +| **embeddingCostPer1kTokens** | `number` | optional | Cost per 1K embedding tokens | + --- @@ -124,6 +154,40 @@ const result = ModelCapabilitySchema.parse(data); | **defaultModel** | `string` | optional | Default model ID | | **enableAutoFallback** | `boolean` | optional (default: `true`) | Auto-fallback on errors | +### Nested Shape: `ModelRegistry.models[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **model** | `{ id: string; name: string; version: string; provider: Enum<'openai' \| 'azure_openai' \| 'anthropic' \| 'google' \| 'cohere' \| 'huggingface' \| 'local' \| 'custom'>; … }` | ✅ | | +| **status** | `Enum<'active' \| 'deprecated' \| 'experimental' \| 'disabled'>` | optional (default: `"active"`) | | +| **priority** | `integer` | optional (default: `0`) | Priority for model selection | +| **fallbackModels** | `string[]` | optional | Fallback model IDs | +| **healthCheck** | `{ enabled?: boolean; intervalSeconds?: integer; lastChecked?: string; status?: Enum<'healthy' \| 'unhealthy' \| 'unknown'> }` | optional | | + +### Nested Shape: `ModelRegistry.promptTemplates[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique template identifier | +| **name** | `string` | ✅ | Template name (snake_case) | +| **label** | `string` | ✅ | Display name | +| **system** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | System prompt — supports `{{var}}` interpolation | +| **user** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | ✅ | User prompt template — supports `{{var}}` interpolation | +| **assistant** | `string` | optional | Assistant message prefix | +| **variables** | `{ name: string; type?: Enum<'string' \| 'number' \| 'boolean' \| 'object' \| 'array'>; required?: boolean; defaultValue?: any; … }[]` | optional | Template variables | +| **modelId** | `string` | optional | Recommended model ID | +| **temperature** | `number` | optional | | +| **maxTokens** | `number` | optional | | +| **topP** | `number` | optional | | +| **frequencyPenalty** | `number` | optional | | +| **presencePenalty** | `number` | optional | | +| **stopSequences** | `string[]` | optional | | +| **version** | `string` | optional (default: `"1.0.0"`) | | +| **description** | `string` | optional | | +| **category** | `string` | optional | Template category (e.g., "code_generation", "support") | +| **tags** | `string[]` | optional | | +| **examples** | `{ input: Record; output: string }[]` | optional | | + --- @@ -139,6 +203,35 @@ const result = ModelCapabilitySchema.parse(data); | **fallbackModels** | `string[]` | optional | Fallback model IDs | | **healthCheck** | `{ enabled: boolean; intervalSeconds: integer; lastChecked?: string; status: Enum<'healthy' \| 'unhealthy' \| 'unknown'> }` | optional | | +### Nested Shape: `ModelRegistryEntry.model` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique model identifier | +| **name** | `string` | ✅ | Model display name | +| **version** | `string` | ✅ | Model version (e.g., "gpt-4-turbo-2024-04-09") | +| **provider** | `Enum<'openai' \| 'azure_openai' \| 'anthropic' \| 'google' \| 'cohere' \| 'huggingface' \| 'local' \| 'custom'>` | ✅ | | +| **capabilities** | `{ textGeneration: boolean; textEmbedding: boolean; imageGeneration: boolean; imageUnderstanding: boolean; … }` | ✅ | | +| **limits** | `{ maxTokens: integer; contextWindow: integer; maxOutputTokens?: integer; rateLimit?: object }` | ✅ | | +| **pricing** | `{ currency: string; inputCostPer1kTokens?: number; outputCostPer1kTokens?: number; embeddingCostPer1kTokens?: number }` | optional | | +| **endpoint** | `string` | optional | Custom API endpoint | +| **apiKey** | `string` | optional | API key (Warning: Prefer secretRef) | +| **secretRef** | `string` | optional | Reference to stored secret (e.g. system:openai_api_key) | +| **region** | `string` | optional | Deployment region (e.g., "us-east-1") | +| **description** | `string` | optional | | +| **tags** | `string[]` | optional | Tags for categorization | +| **deprecated** | `boolean` | optional (default: `false`) | | +| **recommendedFor** | `string[]` | optional | Use case recommendations | + +### Nested Shape: `ModelRegistryEntry.healthCheck` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | | +| **intervalSeconds** | `integer` | optional (default: `300`) | | +| **lastChecked** | `string` | optional | ISO timestamp | +| **status** | `Enum<'healthy' \| 'unhealthy' \| 'unknown'>` | optional (default: `"unknown"`) | | + --- @@ -184,6 +277,24 @@ const result = ModelCapabilitySchema.parse(data); | **tags** | `string[]` | optional | | | **examples** | `{ input: Record; output: string }[]` | optional | | +### Nested Shape: `PromptTemplate.variables[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Variable name (e.g., "user_name", "context") | +| **type** | `Enum<'string' \| 'number' \| 'boolean' \| 'object' \| 'array'>` | optional (default: `"string"`) | | +| **required** | `boolean` | optional (default: `false`) | | +| **defaultValue** | `any` | optional | | +| **description** | `string` | optional | | +| **validation** | `{ minLength?: number; maxLength?: number; pattern?: string; enum?: any[] }` | optional | | + +### Nested Shape: `PromptTemplate.examples[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **input** | `Record` | ✅ | Example variable values | +| **output** | `string` | ✅ | Expected output | + --- diff --git a/content/docs/references/ai/skill.mdx b/content/docs/references/ai/skill.mdx index 016e457eb8..b6a4978367 100644 --- a/content/docs/references/ai/skill.mdx +++ b/content/docs/references/ai/skill.mdx @@ -50,6 +50,22 @@ const result = SkillSchema.parse(data); | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `Skill.triggerConditions[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Context field to evaluate | +| **operator** | `Enum<'eq' \| 'neq' \| 'in' \| 'not_in' \| 'contains'>` | ✅ | Comparison operator | +| **value** | `string \| string[]` | ✅ | Expected value or values | + +### Nested Shape: `Skill.protection` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | ✅ | Lock policy — none \| no-overlay \| no-delete \| full. | +| **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | +| **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | + --- diff --git a/content/docs/references/ai/solution-blueprint.mdx b/content/docs/references/ai/solution-blueprint.mdx index d51985d498..3b12e1d0e2 100644 --- a/content/docs/references/ai/solution-blueprint.mdx +++ b/content/docs/references/ai/solution-blueprint.mdx @@ -32,6 +32,15 @@ const result = BlueprintAppSchema.parse(data); | **icon** | `string` | optional | Lucide icon for the App Launcher | | **nav** | `{ type: Enum<'object' \| 'dashboard'>; target: string; label?: string; icon?: string }[]` | optional | Navigation entries; omit to auto-surface every created object and dashboard | +### Nested Shape: `BlueprintApp.nav[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'object' \| 'dashboard'>` | optional (default: `"object"`) | What this nav entry opens | +| **target** | `string` | ✅ | Object or dashboard machine name to surface (snake_case) | +| **label** | `string` | optional | Nav entry label (defaults to the target label/name) | +| **icon** | `string` | optional | Lucide icon name for the nav entry | + --- @@ -58,6 +67,18 @@ const result = BlueprintAppSchema.parse(data); | **label** | `string` | optional | Human-readable dashboard label | | **widgets** | `{ id: string; title?: string; object?: string; chart?: Enum<'metric' \| 'bar' \| 'line' \| 'pie' \| 'table'>; … }[]` | optional | Widgets to place on the dashboard | +### Nested Shape: `BlueprintDashboard.widgets[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Widget id (snake_case) | +| **title** | `string` | optional | Widget title | +| **object** | `string` | optional | Source object for the widget | +| **chart** | `Enum<'metric' \| 'bar' \| 'line' \| 'pie' \| 'table'>` | optional | Widget visualization | +| **measure** | `string` | optional | The field this widget aggregates (e.g. "amount", "probability"), or "count" to count records. The aggregation is chosen automatically from the field type — a money field SUMs, a percentage/rate AVERAGEs — so name the FIELD, not "total_amount". A "total revenue" widget sets measure:"amount"; an "average win rate" widget sets measure:"win_rate"; a "number of deals" widget sets measure:"count". Omit to let the builder infer from the title. | +| **groupBy** | `string` | optional | The field to break the widget down by — the category or time axis (e.g. "stage", "created_at"). A "by status" chart MUST set this to the status field; the title and this field MUST name the SAME field. Omit for a single-number metric. | +| **condition** | `{ field: string; op: Enum<'lt' \| 'lte' \| 'gt' \| 'gte' \| 'eq' \| 'ne'>; value: number \| string \| boolean }` | optional | Restrict WHICH records the widget counts/aggregates when its title implies a threshold or status (e.g. "stock below 10" → `{field:"stock_quantity", op:"lt", value:10}`; "open tickets" → `{field:"status", op:"eq", value:"open"}`). Without it the widget covers ALL records — so a "低于10的备件预警" / "overdue" card would wrongly count everything. Omit when the widget genuinely spans every record. | + --- @@ -128,6 +149,17 @@ const result = BlueprintAppSchema.parse(data); * `tags` * `vector` +### Nested Shape: `BlueprintField.summaryOperations` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **object** | `string` | ✅ | The CHILD object whose records are aggregated (snake_case). It must carry a lookup / master_detail field pointing back at this parent, or the roll-up never computes. | +| **function** | `Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max'>` | ✅ | Aggregation: "数量 / 个数 / 计数" → count; "合计 / 总额 / 累计" → sum; "平均" → avg | +| **field** | `string` | optional | Numeric field on the CHILD object to aggregate. Ignored for "count" (pass "id" or omit it). | +| **relationshipField** | `string` | optional | The child FK field pointing back at this parent. Auto-detected from the child's lookup / master_detail; set it only when the child has more than one reference to this parent. | +| **conditions** | `{ field: string; op: Enum<'lt' \| 'lte' \| 'gt' \| 'gte' \| 'eq' \| 'ne'>; value: number \| string \| boolean }[]` | optional | CONDITIONAL roll-up: aggregate only the child rows matching these comparisons (ANDed). REQUIRED whenever the field name carries a qualifier — "已完成任务数 / 已收货金额 / 待处理工单数", any 已X / 未X / `<某状态>`的 count-or-sum → e.g. [`{ field: "status", op: "eq", value: "completed" }`]. WITHOUT it the roll-up silently counts EVERY child and reports a plausible-looking WRONG number, which is worse than a visible 0. | +| **filter** | `any` | optional | The same predicate as a canonical query filter map (e.g. `{ status: "completed" }`, `{ status: { $in: ["received", "partial"] } }`). Use it when hand-authoring a blueprint; the structured design path uses `conditions` instead. Wins over `conditions` when both are given. | + --- @@ -158,6 +190,19 @@ const result = BlueprintAppSchema.parse(data); | **sharingModel** | `Enum<'private' \| 'public_read' \| 'public_read_write' \| 'controlled_by_parent'>` | optional | Org-Wide Default record visibility (OWD) for INTERNAL users — the deliberate sharing choice for this object (ADR-0090). Canonical four only: private (owner-only) \| public_read (everyone reads, owner writes) \| public_read_write (everyone reads+writes) \| controlled_by_parent (derived from the master record — ONLY for an object whose fields include a master_detail reference). SET it when the user's description implies a visibility intent — personal/private data (HR, 绩效, salary, 个人隐私) → "private"; shared reference data everyone edits → "public_read_write". Omit to accept the platform's deterministic default (business object → public_read_write; master-detail child → controlled_by_parent) — omitting on privacy-sensitive data silently over-shares it. | | **nameField** | `string` | optional | The record title field — which field holds the human-readable name shown on cards, lookup chips, breadcrumbs and search (ADR-0079). Set it to the object's text label field (e.g. "product_name"). For a numbered entity (invoice/ticket), set it to a formula field that composes number + name (e.g. "`{order_no}` · `{customer}`"). Omitting it lets the platform auto-pick a text field, but declaring it is strongly preferred. | +### Nested Shape: `BlueprintObject.fields[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Field machine name (snake_case) | +| **label** | `string` | optional | Human-readable field label | +| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>` | ✅ | Field data type | +| **required** | `boolean` | optional | Whether the field is required | +| **reference** | `string` | optional | Target object name for lookup / master_detail relationship fields | +| **options** | `{ label: string; value: string }[]` | optional | Choices for select / multiselect / radio fields | +| **summaryOperations** | `{ object: string; function: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max'>; field?: string; relationshipField?: string; … }` | optional | REQUIRED when `type` is "summary" (a roll-up of child records: 任务总数 / 报名人数 / 合计金额 / 已完成任务数). Names the child object, the aggregation, and — for a qualified count/sum — the condition. A "summary" field without it materializes runtime-dead. | +| **expression** | `string` | optional | REQUIRED when `type` is "formula" — the CEL body the field computes, e.g. "record.quantity * record.unit_price", or "record.order_no + ' · ' + record.customer" for a composed title. A "formula" field without it materializes runtime-dead: the engine builds its formula plan only from fields that HAVE an expression, so the field reads null everywhere, forever. Same failure shape as a "summary" with no `summaryOperations`. Note `nameField` on the object recommends a formula for numbered entities (invoice/ticket) — that formula needs THIS key, or the record title is blank on every card, lookup chip and breadcrumb. | + --- @@ -186,6 +231,14 @@ const result = BlueprintAppSchema.parse(data); | **conditions** | `{ field: string; op: Enum<'lt' \| 'lte' \| 'gt' \| 'gte' \| 'eq' \| 'ne'>; value: number \| string \| boolean }[]` | optional | CONDITIONAL roll-up: aggregate only the child rows matching these comparisons (ANDed). REQUIRED whenever the field name carries a qualifier — "已完成任务数 / 已收货金额 / 待处理工单数", any 已X / 未X / `<某状态>`的 count-or-sum → e.g. [`{ field: "status", op: "eq", value: "completed" }`]. WITHOUT it the roll-up silently counts EVERY child and reports a plausible-looking WRONG number, which is worse than a visible 0. | | **filter** | `any` | optional | The same predicate as a canonical query filter map (e.g. `{ status: "completed" }`, `{ status: { $in: ["received", "partial"] } }`). Use it when hand-authoring a blueprint; the structured design path uses `conditions` instead. Wins over `conditions` when both are given. | +### Nested Shape: `BlueprintSummaryOperations.conditions[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field on the target object to filter by (e.g. "stock_quantity", "status") | +| **op** | `Enum<'lt' \| 'lte' \| 'gt' \| 'gte' \| 'eq' \| 'ne'>` | ✅ | Comparison operator | +| **value** | `number \| string \| boolean` | ✅ | Comparison value — for a select field use its option VALUE, never its label (e.g. "completed", not "已完成") | + --- @@ -233,6 +286,52 @@ const result = BlueprintAppSchema.parse(data); | **app** | `{ name: string; label?: string; icon?: string; nav?: object[] }` | optional | The navigation shell (app) that surfaces the created objects/dashboards to end users | | **seedData** | `{ object: string; records: Record[] }[]` | optional | Suggested seed data (reported, not auto-applied in Phase C) | +### Nested Shape: `SolutionBlueprint.objects[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Object machine name (snake_case) | +| **label** | `string` | optional | Human-readable singular label | +| **description** | `string` | optional | What this object represents | +| **fields** | `{ name: string; label?: string; type: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; required?: boolean; … }[]` | ✅ | Fields to create on the object | +| **sharingModel** | `Enum<'private' \| 'public_read' \| 'public_read_write' \| 'controlled_by_parent'>` | optional | Org-Wide Default record visibility (OWD) for INTERNAL users — the deliberate sharing choice for this object (ADR-0090). Canonical four only: private (owner-only) \| public_read (everyone reads, owner writes) \| public_read_write (everyone reads+writes) \| controlled_by_parent (derived from the master record — ONLY for an object whose fields include a master_detail reference). SET it when the user's description implies a visibility intent — personal/private data (HR, 绩效, salary, 个人隐私) → "private"; shared reference data everyone edits → "public_read_write". Omit to accept the platform's deterministic default (business object → public_read_write; master-detail child → controlled_by_parent) — omitting on privacy-sensitive data silently over-shares it. | +| **nameField** | `string` | optional | The record title field — which field holds the human-readable name shown on cards, lookup chips, breadcrumbs and search (ADR-0079). Set it to the object's text label field (e.g. "product_name"). For a numbered entity (invoice/ticket), set it to a formula field that composes number + name (e.g. "`{order_no}` · `{customer}`"). Omitting it lets the platform auto-pick a text field, but declaring it is strongly preferred. | + +### Nested Shape: `SolutionBlueprint.views[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **object** | `string` | ✅ | Object this view displays (snake_case) | +| **name** | `string` | ✅ | View machine name (snake_case) | +| **label** | `string` | optional | Human-readable view label | +| **type** | `Enum<'list' \| 'form' \| 'kanban' \| 'calendar' \| 'gallery' \| 'gantt'>` | optional (default: `"list"`) | View kind. Pick the surface that fits the data: "gallery" for a visual card/cover browse when the user asks for a 画廊/相册/卡片墙/封面/海报/图集 (a gallery / card wall / cover / poster grid) or the object has an image/avatar/file field worth showing as a card cover; "gantt" for a 甘特图/时间线/排期 (timeline / schedule) when the object has BOTH a start and an end date field; "kanban" for a board grouped by a status/select field; "calendar" for a single-date schedule; "form" for a record editor; else "list". | +| **columns** | `string[]` | optional | Field names shown as columns (in order). For a gallery, INCLUDE the image/avatar/file field (it becomes the card cover); for a gantt, INCLUDE the start date column before the end date column. | +| **groupBy** | `string` | optional | REQUIRED for kanban views: the select/status field whose options become the board columns (e.g. "stage", "status"). Without it a kanban renders as a plain list. Optional for gantt (groups leaf tasks into summary rows). | + +### Nested Shape: `SolutionBlueprint.dashboards[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Dashboard machine name (snake_case) | +| **label** | `string` | optional | Human-readable dashboard label | +| **widgets** | `{ id: string; title?: string; object?: string; chart?: Enum<'metric' \| 'bar' \| 'line' \| 'pie' \| 'table'>; … }[]` | optional | Widgets to place on the dashboard | + +### Nested Shape: `SolutionBlueprint.app` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | App machine name (snake_case) | +| **label** | `string` | optional | App display label | +| **icon** | `string` | optional | Lucide icon for the App Launcher | +| **nav** | `{ type: Enum<'object' \| 'dashboard'>; target: string; label?: string; icon?: string }[]` | optional | Navigation entries; omit to auto-surface every created object and dashboard | + +### Nested Shape: `SolutionBlueprint.seedData[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **object** | `string` | ✅ | Target object name (snake_case) | +| **records** | `Record[]` | ✅ | Rows to seed | + --- @@ -250,6 +349,45 @@ const result = BlueprintAppSchema.parse(data); | **dashboards** | `{ name: string; label: string \| null; widgets: object[] \| null }[] \| null` | ✅ | Dashboards to create, or null | | **app** | `{ name: string; label: string \| null; icon: string \| null; nav: object[] \| null } \| null` | ✅ | The navigation shell (app) that surfaces the created objects/dashboards, or null | +### Nested Shape: `SolutionBlueprintStrict.objects[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Object machine name (snake_case) | +| **label** | `string \| null` | ✅ | Human-readable singular label, or null | +| **description** | `string \| null` | ✅ | What this object represents, or null | +| **fields** | `{ name: string; label: string \| null; type: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; required: boolean \| null; … }[]` | ✅ | Fields to create on the object | +| **sharingModel** | `Enum<'private' \| 'public_read' \| 'public_read_write' \| 'controlled_by_parent'> \| null` | ✅ | Org-Wide Default record visibility (OWD) for INTERNAL users (ADR-0090), or null to accept the platform default (business object → public_read_write; master-detail child → controlled_by_parent). SET it when the user's description implies a visibility intent: personal/private data (HR, 绩效, salary, 个人隐私) → "private" (owner-only); "public_read" = everyone reads, owner writes; "public_read_write" = everyone reads+writes; "controlled_by_parent" ONLY for an object with a master_detail reference field. Null on privacy-sensitive data silently over-shares it. | +| **nameField** | `string \| null` | ✅ | The record title field — which field holds the human-readable name shown on cards, lookup chips, breadcrumbs and search (ADR-0079), or null to let the platform auto-pick a text field. Set it to the object's text label field (e.g. "product_name") — snake_case. For a numbered entity (invoice/ticket), set it to a formula field that composes number + name (e.g. "`{order_no}` · `{customer}`"). Declaring it is strongly preferred over null. | + +### Nested Shape: `SolutionBlueprintStrict.views[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **object** | `string` | ✅ | Object this view displays (snake_case) | +| **name** | `string` | ✅ | View machine name (snake_case) | +| **label** | `string \| null` | ✅ | Human-readable view label, or null | +| **type** | `Enum<'list' \| 'form' \| 'kanban' \| 'calendar' \| 'gallery' \| 'gantt'> \| null` | ✅ | View kind, or null for list. "gallery" = visual card/cover browse (画廊/相册/卡片墙/封面/海报, or an object with an image/avatar/file field); "gantt" = timeline/schedule (甘特图/时间线/排期, object with BOTH a start and an end date field); "kanban" = board grouped by a status/select field; "calendar" = single-date schedule; "form" = record editor. | +| **columns** | `string[] \| null` | ✅ | Field names shown as columns, or null. For a gallery, INCLUDE the image/avatar/file field (becomes the card cover); for a gantt, INCLUDE the start date column before the end date column. | +| **groupBy** | `string \| null` | ✅ | REQUIRED for kanban: the select/status field whose options become the board columns (e.g. "stage"). Optional for gantt (groups leaf tasks). Null for list/form/calendar/gallery. | + +### Nested Shape: `SolutionBlueprintStrict.dashboards[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Dashboard machine name (snake_case) | +| **label** | `string \| null` | ✅ | Human-readable dashboard label, or null | +| **widgets** | `{ id: string; title: string \| null; object: string \| null; chart: Enum<'metric' \| 'bar' \| 'line' \| 'pie' \| 'table'> \| null; … }[] \| null` | ✅ | Widgets to place on the dashboard, or null | + +### Nested Shape: `SolutionBlueprintStrict.app` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | App machine name (snake_case) | +| **label** | `string \| null` | ✅ | App display label, or null | +| **icon** | `string \| null` | ✅ | Lucide icon for the App Launcher, or null | +| **nav** | `{ type: Enum<'object' \| 'dashboard'>; target: string; label: string \| null; icon: string \| null }[] \| null` | ✅ | Navigation entries; null to auto-surface every created object and dashboard | + --- diff --git a/content/docs/references/ai/tool.mdx b/content/docs/references/ai/tool.mdx index ee03a7668f..59c334975f 100644 --- a/content/docs/references/ai/tool.mdx +++ b/content/docs/references/ai/tool.mdx @@ -44,6 +44,14 @@ AI tool definition. [READ-ONLY PROJECTION — not an execution entry point] Auth | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `Tool.protection` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | ✅ | Lock policy — none \| no-overlay \| no-delete \| full. | +| **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | +| **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | + --- diff --git a/content/docs/references/ai/usage.mdx b/content/docs/references/ai/usage.mdx index 5010f06e7a..1653be7ddb 100644 --- a/content/docs/references/ai/usage.mdx +++ b/content/docs/references/ai/usage.mdx @@ -51,6 +51,14 @@ const result = AIUsageRecordSchema.parse(data); | **latencyMs** | `number` | optional | | | **timestamp** | `string` | optional | | +### Nested Shape: `AIUsageRecord.usage` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **promptTokens** | `integer` | ✅ | Tokens consumed by the prompt | +| **completionTokens** | `integer` | ✅ | Tokens generated in the completion | +| **totalTokens** | `integer` | ✅ | Total tokens (prompt + completion) | + --- diff --git a/content/docs/references/api/analytics.mdx b/content/docs/references/api/analytics.mdx index 691acb57bb..1aba5dbfac 100644 --- a/content/docs/references/api/analytics.mdx +++ b/content/docs/references/api/analytics.mdx @@ -48,6 +48,28 @@ const result = AnalyticsEndpoint.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ name: string; title?: string; measures: object[]; dimensions: object[] }[]` | ✅ | Available cubes, each as the `CubeMeta` discovery projection — the cube name, its title, and the measures/dimensions a client may name in a query. A bare array: there is no `cubes` wrapper object, and no cube `sql` is published. | +### Nested Shape: `AnalyticsMetadataResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `AnalyticsMetadataResponse.data[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Cube name | +| **title** | `string` | optional | Human-readable cube title | +| **measures** | `{ name: string; type: string; title?: string }[]` | ✅ | Measures this cube accepts in `/analytics/query` | +| **dimensions** | `{ name: string; type: string; title?: string }[]` | ✅ | Dimensions this cube accepts in `/analytics/query` | + --- @@ -83,6 +105,27 @@ const result = AnalyticsEndpoint.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ rows: Record[]; fields: object[]; sql?: string }` | ✅ | | +### Nested Shape: `AnalyticsResultResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `AnalyticsResultResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **rows** | `Record[]` | ✅ | Result rows | +| **fields** | `{ name: string; type: string }[]` | ✅ | Column metadata | +| **sql** | `string` | optional | Executed SQL (if debug enabled) | + --- @@ -97,6 +140,19 @@ const result = AnalyticsEndpoint.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ sql: string; params: any[] }` | ✅ | | +### Nested Shape: `AnalyticsSqlResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + --- diff --git a/content/docs/references/api/auth-endpoints.mdx b/content/docs/references/api/auth-endpoints.mdx index 17301e3238..e84e79cce6 100644 --- a/content/docs/references/api/auth-endpoints.mdx +++ b/content/docs/references/api/auth-endpoints.mdx @@ -157,6 +157,35 @@ This schema accepts one of the following structures: | **socialProviders** | `{ id: string; name: string; enabled: boolean; type: Enum<'social' \| 'oidc'> }[]` | ✅ | Available social/OAuth providers | | **features** | `{ twoFactor: boolean; organization: boolean; ssoEnforced?: boolean; phoneNumber?: boolean; … }` | ✅ | Enabled authentication features | +### Nested Shape: `GetAuthConfigResponse.emailPassword` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | ✅ | Whether email/password auth is enabled | +| **disableSignUp** | `boolean` | optional | Whether new user registration is disabled | +| **requireEmailVerification** | `boolean` | optional | Whether email verification is required | + +### Nested Shape: `GetAuthConfigResponse.socialProviders[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Provider ID (e.g., google, github, microsoft, okta) | +| **name** | `string` | ✅ | Display name (e.g., Google, GitHub) | +| **enabled** | `boolean` | ✅ | Whether this provider is enabled | +| **type** | `Enum<'social' \| 'oidc'>` | optional (default: `"social"`) | Provider type | + +### Nested Shape: `GetAuthConfigResponse.features` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **twoFactor** | `boolean` | optional (default: `false`) | Two-factor authentication enabled | +| **passkeys** | `never` | optional | [REMOVED] `features.passkeys` was removed from GET /api/v1/auth/config in @objectstack/spec 17 (#7481, ADR-0049) — it was served from introduction and consumed by nothing: no login UI in any client reads it, and no better-auth passkey plugin is wired behind it, so a deployer who set `plugins.passkeys: true` flipped a switch that changed no behaviour anywhere. Delete the key. There is no replacement flag to read: passkey sign-in is not a capability this platform offers yet. It returns to this payload in the change that ships the login UI (objectui#4179), classified in PUBLIC_AUTH_FEATURES again at that point — do not re-add it ahead of a consumer. | +| **magicLink** | `never` | optional | [REMOVED] `features.magicLink` was removed from GET /api/v1/auth/config in @objectstack/spec 17 (#7481, ADR-0049) — the ADVERTISEMENT was inert, not the capability: no client renders a magic-link sign-in affordance off this flag, so it only told a deployer that a UI existed when none did. Delete the key. The server side is unchanged and still yours to call: `AuthPluginConfig.plugins.magicLink` wires better-auth's magic-link plugin, and `/api/v1/auth/magic-link/send` + `/magic-link/verify` answer exactly as before — drive them from your own UI, or wait for objectui#4179, which restores this flag along with the login UI that reads it. | +| **organization** | `boolean` | optional (default: `false`) | Multi-tenant organization support enabled | +| **ssoEnforced** | `boolean` | optional | SSO-only login enforced: the UI hides the local password form + self-registration (a break-glass "use a password" link remains) | +| **phoneNumber** | `boolean` | optional | Phone-number sign-in enabled (phone + password, #2766 V1.5) | +| **phoneNumberOtp** | `boolean` | optional | Phone-number OTP sign-in and self-service password reset available — requires the phoneNumber plugin plus a deliverable SMS service (#2780) | + --- diff --git a/content/docs/references/api/auth.mdx b/content/docs/references/api/auth.mdx index 07465d365c..edf49e334f 100644 --- a/content/docs/references/api/auth.mdx +++ b/content/docs/references/api/auth.mdx @@ -121,6 +121,27 @@ const result = AuthProvider.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ session: object; user: object; token?: string }` | ✅ | | +### Nested Shape: `SessionResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `SessionResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **session** | `{ id: string; expiresAt: string; token?: string; ipAddress?: string; … }` | ✅ | Active Session Info | +| **user** | `{ id: string; email: string; emailVerified: boolean; name: string; … }` | ✅ | Current User Details | +| **token** | `string` | optional | Bearer token if not using cookies | + --- @@ -157,6 +178,36 @@ const result = AuthProvider.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ id: string; email: string; emailVerified: boolean; name: string; … }` | ✅ | | +### Nested Shape: `UserProfileResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `UserProfileResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | User ID | +| **email** | `string` | ✅ | Email address | +| **emailVerified** | `boolean` | optional (default: `false`) | Is email verified? | +| **name** | `string` | ✅ | Display name | +| **image** | `string` | optional | Avatar URL | +| **username** | `string` | optional | Username (optional) | +| **roles** | `string[]` | optional (default: `[]`) | Assigned role IDs | +| **tenantId** | `string` | optional | Current tenant ID | +| **language** | `string` | optional (default: `"en"`) | Preferred language | +| **timezone** | `string` | optional | Preferred timezone | +| **createdAt** | `string` | optional | | +| **updatedAt** | `string` | optional | | + --- diff --git a/content/docs/references/api/automation-api.mdx b/content/docs/references/api/automation-api.mdx index 3726e211b1..9f04cbcc8e 100644 --- a/content/docs/references/api/automation-api.mdx +++ b/content/docs/references/api/automation-api.mdx @@ -109,6 +109,65 @@ const result = AutomationApiErrorCode.parse(data); | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `CreateFlowRequest.variables[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Variable name | +| **type** | `string` | ✅ | Data type (text, number, boolean, object, list) | +| **isInput** | `boolean` | optional (default: `false`) | Is input parameter | +| **isOutput** | `boolean` | optional (default: `false`) | Is output parameter | +| **defaultValue** | `any` | optional | Value bound at run start when no parameter supplies one — this is what makes a declared variable always bound. An explicitly supplied param wins, including `false` and `null`; the boundary is `params[name] !== undefined`. | + +### Nested Shape: `CreateFlowRequest.nodes[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Node unique ID | +| **type** | `string` | ✅ | Action type — a built-in FlowNodeAction id or a plugin-registered node type. Validated against the live action registry at registerFlow() (ADR-0018), not by a closed enum. | +| **label** | `string` | ✅ | Node label | +| **config** | `Record` | optional | Node configuration | +| **connectorConfig** | `{ connectorId: string; actionId: string; input?: Record }` | optional | | +| **position** | `{ x: number; y: number }` | optional | | +| **timeoutMs** | `integer` | optional | Maximum execution time for this node in milliseconds | +| **inputSchema** | `Record; required?: boolean; description?: string }>` | optional | Input parameter schema for this node | +| **outputSchema** | `never` | optional | [REMOVED] `flow.nodes[].outputSchema` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — it was never validated: the engine does not check node outputs against it, so it documented a contract nothing enforced. Delete the key. Downstream nodes read prior outputs via expressions (`{{nodeId.field}}`) regardless of any declaration. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **waitEventConfig** | `{ eventType: Enum<'timer' \| 'signal' \| 'webhook' \| 'manual' \| 'condition'>; timerDuration?: string; signalName?: string }` | optional | Configuration for wait node event resumption | +| **boundaryConfig** | `{ attachedToNodeId: string; eventType: Enum<'error' \| 'timer' \| 'signal' \| 'cancel'>; interrupting?: boolean; errorCode?: string; … }` | optional | Configuration for boundary events attached to host nodes | + +### Nested Shape: `CreateFlowRequest.edges[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Edge unique ID | +| **source** | `string` | ✅ | Source Node ID | +| **target** | `string` | ✅ | Target Node ID | +| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) returning boolean used for branching. | +| **type** | `Enum<'default' \| 'fault' \| 'conditional' \| 'back'>` | optional (default: `"default"`) | Connection type: default (normal flow), fault (error path), conditional (expression-guarded), or back (ADR-0044 declared back-edge — traversed normally at run time, but excluded from DAG cycle validation so a revise/rework loop can re-enter an earlier node) | +| **label** | `string` | optional | Label on the connector | +| **isDefault** | `boolean` | optional (default: `false`) | BPMN default flow: traverse this edge only when no sibling conditional edge of the same source node matched. Mutually exclusive with `condition`; at most one per source node. | + +### Nested Shape: `CreateFlowRequest.errorHandling` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **strategy** | `Enum<'fail' \| 'retry' \| 'continue'>` | optional (default: `"fail"`) | How to handle node execution errors. 'retry' governs ONE synchronous dispatch: a durable pause (approval/screen/wait) ends the retry-governed segment, so a failure after the run resumes is not retried. | +| **maxRetries** | `integer` | optional (default: `0`) | Retry attempts after the initial one. Read only under strategy: 'retry', which requires >= 1; 0 (the default) means no retry. | +| **backoffMs** | `integer` | optional (default: `1000`) | Base delay before the first retry (ms); subsequent delays multiply by backoffMultiplier | +| **backoffMultiplier** | `number` | optional (default: `1`) | Exponential backoff multiplier; 1 (the default) keeps the delay flat | +| **maxRetryDelayMs** | `integer` | optional (default: `30000`) | Ceiling for a single backoff delay (ms) | +| **jitter** | `boolean` | optional (default: `false`) | Randomize each delay within [50%, 100%] of its computed value — spreads a thundering herd of simultaneous retries | +| **retryDelayMs** | `never` | optional | [REMOVED] `retryDelayMs` was removed in @objectstack/spec 17.0.0 (#4661, #4964) — the retry policy now has ONE spelling for its base delay across every surface that carries it: `job.retryPolicy`, a `try_catch` node's `retry` and `flow.errorHandling`. Rename the key to `backoffMs`; the value (milliseconds before the first retry) is unchanged. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **fallbackNodeId** | `never` | optional | [REMOVED] `flow.errorHandling.fallbackNodeId` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — the engine routes unrecoverable node errors via per-node fault edges (an edge with type: 'fault'), and never read this key: a fallback configured here silently did not exist. Delete the key and draw a fault edge from the failing node to the handler node instead. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | + +### Nested Shape: `CreateFlowRequest.protection` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | ✅ | Lock policy — none \| no-overlay \| no-delete \| full. | +| **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | +| **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | + --- @@ -123,6 +182,47 @@ const result = AutomationApiErrorCode.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ name: string; label: string; description?: string; successMessage?: string; … }` | ✅ | The created flow definition | +### Nested Shape: `CreateFlowResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `CreateFlowResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Machine name | +| **label** | `string` | ✅ | Flow label | +| **description** | `string` | optional | | +| **successMessage** | `string` | optional | Message carried on AutomationResult for every terminal run (not only screen flows); the screen-flow UI shows it as a toast instead of a generic "Done". | +| **errorMessage** | `string` | optional | Message carried on AutomationResult for every terminal run (not only screen flows); the screen-flow UI shows it as a toast instead of the raw error. | +| **version** | `integer` | optional (default: `1`) | Version number | +| **status** | `Enum<'draft' \| 'active' \| 'obsolete' \| 'invalid'>` | optional (default: `"draft"`) | Deployment status | +| **template** | `never` | optional | [REMOVED] `flow.template` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no designer or engine path ever read it, so flagging a flow as a template/subflow did nothing. Delete the key. Shared logic is invoked via a subflow NODE referencing the flow by name. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **type** | `Enum<'autolaunched' \| 'record_change' \| 'schedule' \| 'screen' \| 'api'>` | ✅ | Flow type | +| **variables** | `{ name: string; type: string; isInput?: boolean; isOutput?: boolean; … }[]` | optional | Flow variables | +| **nodes** | `{ id: string; type: string; label: string; config?: Record; … }[]` | ✅ | Flow nodes | +| **edges** | `{ id: string; source: string; target: string; condition?: string \| object; … }[]` | ✅ | Flow connections | +| **active** | `never` | optional | [REMOVED] `flow.active` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — it never had an effect: the engine arms flows from `status`, and `active: false` did NOT stop a flow (worse, the default read as disabled while the engine treated unset as enabled). Delete the key. Use `status: 'obsolete'` (or 'invalid') to unbind and disable a flow, `status: 'active'` to arm it. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **runAs** | `Enum<'system' \| 'user'>` | optional (default: `"user"`) | Execution identity for the run: system = elevated (bypasses RLS), user = the triggering user (RLS-respecting). A run with no trigger user has no identity to scope to, so under user its data operations are REFUSED — declare system to make the elevation explicit. This covers schedule/time-relative/api triggers AND any record-change flow fired by a write that carried no user. | +| **errorHandling** | `{ strategy?: Enum<'fail' \| 'retry' \| 'continue'>; maxRetries?: integer; backoffMs?: integer; backoffMultiplier?: number; … }` | optional | Flow-level error handling configuration. A durable pause ends the retry-governed segment: strategy: 'retry' describes one synchronous dispatch, so a run that parks on an approval/screen/wait node and later resumes gets one attempt for anything that fails after the pause. Protect the post-pause half with its own failure handling in the flow — a try_catch node's retry around the post-resume work, or fault edges to a handler node. | +| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this flow. | +| **_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. | + --- @@ -148,6 +248,26 @@ const result = AutomationApiErrorCode.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ name: string; deleted: boolean }` | ✅ | | +### Nested Shape: `DeleteFlowResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `DeleteFlowResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Name of the deleted flow | +| **deleted** | `boolean` | ✅ | Whether the flow was deleted | + --- @@ -191,6 +311,47 @@ const result = AutomationApiErrorCode.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ name: string; label: string; description?: string; successMessage?: string; … }` | ✅ | Full flow definition | +### Nested Shape: `GetFlowResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `GetFlowResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Machine name | +| **label** | `string` | ✅ | Flow label | +| **description** | `string` | optional | | +| **successMessage** | `string` | optional | Message carried on AutomationResult for every terminal run (not only screen flows); the screen-flow UI shows it as a toast instead of a generic "Done". | +| **errorMessage** | `string` | optional | Message carried on AutomationResult for every terminal run (not only screen flows); the screen-flow UI shows it as a toast instead of the raw error. | +| **version** | `integer` | optional (default: `1`) | Version number | +| **status** | `Enum<'draft' \| 'active' \| 'obsolete' \| 'invalid'>` | optional (default: `"draft"`) | Deployment status | +| **template** | `never` | optional | [REMOVED] `flow.template` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no designer or engine path ever read it, so flagging a flow as a template/subflow did nothing. Delete the key. Shared logic is invoked via a subflow NODE referencing the flow by name. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **type** | `Enum<'autolaunched' \| 'record_change' \| 'schedule' \| 'screen' \| 'api'>` | ✅ | Flow type | +| **variables** | `{ name: string; type: string; isInput?: boolean; isOutput?: boolean; … }[]` | optional | Flow variables | +| **nodes** | `{ id: string; type: string; label: string; config?: Record; … }[]` | ✅ | Flow nodes | +| **edges** | `{ id: string; source: string; target: string; condition?: string \| object; … }[]` | ✅ | Flow connections | +| **active** | `never` | optional | [REMOVED] `flow.active` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — it never had an effect: the engine arms flows from `status`, and `active: false` did NOT stop a flow (worse, the default read as disabled while the engine treated unset as enabled). Delete the key. Use `status: 'obsolete'` (or 'invalid') to unbind and disable a flow, `status: 'active'` to arm it. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **runAs** | `Enum<'system' \| 'user'>` | optional (default: `"user"`) | Execution identity for the run: system = elevated (bypasses RLS), user = the triggering user (RLS-respecting). A run with no trigger user has no identity to scope to, so under user its data operations are REFUSED — declare system to make the elevation explicit. This covers schedule/time-relative/api triggers AND any record-change flow fired by a write that carried no user. | +| **errorHandling** | `{ strategy?: Enum<'fail' \| 'retry' \| 'continue'>; maxRetries?: integer; backoffMs?: integer; backoffMultiplier?: number; … }` | optional | Flow-level error handling configuration. A durable pause ends the retry-governed segment: strategy: 'retry' describes one synchronous dispatch, so a run that parks on an approval/screen/wait node and later resumes gets one attempt for anything that fails after the pause. Protect the post-pause half with its own failure handling in the flow — a try_catch node's retry around the post-resume work, or fault edges to a handler node. | +| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this flow. | +| **_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. | + --- @@ -217,6 +378,37 @@ const result = AutomationApiErrorCode.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ id: string; flowName: string; flowVersion?: integer; status: Enum<'pending' \| 'running' \| 'paused' \| 'completed' \| 'failed' \| 'cancelled' \| …>; … }` | ✅ | Full execution log with step details | +### Nested Shape: `GetRunResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `GetRunResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Execution instance ID | +| **flowName** | `string` | ✅ | Machine name of the executed flow | +| **flowVersion** | `integer` | optional | Version of the flow that was executed | +| **status** | `Enum<'pending' \| 'running' \| 'paused' \| 'completed' \| 'failed' \| 'cancelled' \| …>` | ✅ | Current execution status | +| **trigger** | `{ type: string; recordId?: string; object?: string; userId?: string; … }` | ✅ | What triggered this execution | +| **steps** | `{ nodeId: string; nodeType: string; nodeLabel?: string; status: Enum<'success' \| 'failure' \| 'skipped'>; … }[]` | ✅ | Ordered list of executed steps | +| **summary** | `{ selected: integer; acted: integer; skipped: integer; unmeasured?: integer; … }` | optional | Per-run rollup: records selected / acted on, gate skips, per-node status | +| **variables** | `Record` | optional | Final state of flow variables | +| **startedAt** | `string` | ✅ | Execution start timestamp | +| **completedAt** | `string` | optional | Execution completion timestamp | +| **durationMs** | `integer` | optional | Total execution duration in milliseconds | +| **runAs** | `Enum<'system' \| 'user'>` | optional | Execution context identity | +| **tenantId** | `string` | optional | Tenant ID for multi-tenant isolation | + --- @@ -245,6 +437,28 @@ const result = AutomationApiErrorCode.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ flows: object[]; total?: integer; nextCursor?: string; hasMore: boolean }` | ✅ | | +### Nested Shape: `ListFlowsResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `ListFlowsResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **flows** | `{ name: string; label: string; type: string; status: string; … }[]` | ✅ | Flow summaries | +| **total** | `integer` | optional | Total matching flows | +| **nextCursor** | `string` | optional | Cursor for the next page | +| **hasMore** | `boolean` | ✅ | Whether more flows are available | + --- @@ -273,6 +487,28 @@ const result = AutomationApiErrorCode.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ runs: object[]; total?: integer; nextCursor?: string; hasMore: boolean }` | ✅ | | +### Nested Shape: `ListRunsResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `ListRunsResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **runs** | `{ id: string; flowName: string; flowVersion?: integer; status: Enum<'pending' \| 'running' \| 'paused' \| 'completed' \| 'failed' \| 'cancelled' \| …>; … }[]` | ✅ | Execution run logs | +| **total** | `integer` | optional | Total matching runs | +| **nextCursor** | `string` | optional | Cursor for the next page | +| **hasMore** | `boolean` | ✅ | Whether more runs are available | + --- @@ -299,6 +535,26 @@ const result = AutomationApiErrorCode.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ name: string; enabled: boolean }` | ✅ | | +### Nested Shape: `ToggleFlowResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `ToggleFlowResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Flow name | +| **enabled** | `boolean` | ✅ | New enabled state | + --- @@ -329,6 +585,28 @@ const result = AutomationApiErrorCode.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ success: boolean; output?: any; error?: string; durationMs?: number }` | ✅ | | +### Nested Shape: `TriggerFlowResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `TriggerFlowResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **success** | `boolean` | ✅ | Whether the automation completed successfully | +| **output** | `any` | optional | Output data from the automation | +| **error** | `string` | optional | Error message if execution failed | +| **durationMs** | `number` | optional | Execution duration in milliseconds | + --- @@ -341,6 +619,34 @@ const result = AutomationApiErrorCode.parse(data); | **name** | `string` | ✅ | Flow machine name (snake_case) | | **definition** | `{ name?: string; label?: string; description?: string; successMessage?: string; … }` | ✅ | Partial flow definition to update | +### Nested Shape: `UpdateFlowRequest.definition` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional | Machine name | +| **label** | `string` | optional | Flow label | +| **description** | `string` | optional | | +| **successMessage** | `string` | optional | Message carried on AutomationResult for every terminal run (not only screen flows); the screen-flow UI shows it as a toast instead of a generic "Done". | +| **errorMessage** | `string` | optional | Message carried on AutomationResult for every terminal run (not only screen flows); the screen-flow UI shows it as a toast instead of the raw error. | +| **version** | `integer` | optional (default: `1`) | Version number | +| **status** | `Enum<'draft' \| 'active' \| 'obsolete' \| 'invalid'>` | optional (default: `"draft"`) | Deployment status | +| **template** | `never` | optional | [REMOVED] `flow.template` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no designer or engine path ever read it, so flagging a flow as a template/subflow did nothing. Delete the key. Shared logic is invoked via a subflow NODE referencing the flow by name. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **type** | `Enum<'autolaunched' \| 'record_change' \| 'schedule' \| 'screen' \| 'api'>` | optional | Flow type | +| **variables** | `{ name: string; type: string; isInput?: boolean; isOutput?: boolean; … }[]` | optional | Flow variables | +| **nodes** | `{ id: string; type: string; label: string; config?: Record; … }[]` | optional | Flow nodes | +| **edges** | `{ id: string; source: string; target: string; condition?: string \| object; … }[]` | optional | Flow connections | +| **active** | `never` | optional | [REMOVED] `flow.active` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — it never had an effect: the engine arms flows from `status`, and `active: false` did NOT stop a flow (worse, the default read as disabled while the engine treated unset as enabled). Delete the key. Use `status: 'obsolete'` (or 'invalid') to unbind and disable a flow, `status: 'active'` to arm it. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **runAs** | `Enum<'system' \| 'user'>` | optional (default: `"user"`) | Execution identity for the run: system = elevated (bypasses RLS), user = the triggering user (RLS-respecting). A run with no trigger user has no identity to scope to, so under user its data operations are REFUSED — declare system to make the elevation explicit. This covers schedule/time-relative/api triggers AND any record-change flow fired by a write that carried no user. | +| **errorHandling** | `{ strategy?: Enum<'fail' \| 'retry' \| 'continue'>; maxRetries?: integer; backoffMs?: integer; backoffMultiplier?: number; … }` | optional | Flow-level error handling configuration. A durable pause ends the retry-governed segment: strategy: 'retry' describes one synchronous dispatch, so a run that parks on an approval/screen/wait node and later resumes gets one attempt for anything that fails after the pause. Protect the post-pause half with its own failure handling in the flow — a try_catch node's retry around the post-resume work, or fault edges to a handler node. | +| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this flow. | +| **_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. | + --- @@ -355,6 +661,47 @@ const result = AutomationApiErrorCode.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ name: string; label: string; description?: string; successMessage?: string; … }` | ✅ | The updated flow definition | +### Nested Shape: `UpdateFlowResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `UpdateFlowResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Machine name | +| **label** | `string` | ✅ | Flow label | +| **description** | `string` | optional | | +| **successMessage** | `string` | optional | Message carried on AutomationResult for every terminal run (not only screen flows); the screen-flow UI shows it as a toast instead of a generic "Done". | +| **errorMessage** | `string` | optional | Message carried on AutomationResult for every terminal run (not only screen flows); the screen-flow UI shows it as a toast instead of the raw error. | +| **version** | `integer` | optional (default: `1`) | Version number | +| **status** | `Enum<'draft' \| 'active' \| 'obsolete' \| 'invalid'>` | optional (default: `"draft"`) | Deployment status | +| **template** | `never` | optional | [REMOVED] `flow.template` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no designer or engine path ever read it, so flagging a flow as a template/subflow did nothing. Delete the key. Shared logic is invoked via a subflow NODE referencing the flow by name. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **type** | `Enum<'autolaunched' \| 'record_change' \| 'schedule' \| 'screen' \| 'api'>` | ✅ | Flow type | +| **variables** | `{ name: string; type: string; isInput?: boolean; isOutput?: boolean; … }[]` | optional | Flow variables | +| **nodes** | `{ id: string; type: string; label: string; config?: Record; … }[]` | ✅ | Flow nodes | +| **edges** | `{ id: string; source: string; target: string; condition?: string \| object; … }[]` | ✅ | Flow connections | +| **active** | `never` | optional | [REMOVED] `flow.active` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — it never had an effect: the engine arms flows from `status`, and `active: false` did NOT stop a flow (worse, the default read as disabled while the engine treated unset as enabled). Delete the key. Use `status: 'obsolete'` (or 'invalid') to unbind and disable a flow, `status: 'active'` to arm it. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **runAs** | `Enum<'system' \| 'user'>` | optional (default: `"user"`) | Execution identity for the run: system = elevated (bypasses RLS), user = the triggering user (RLS-respecting). A run with no trigger user has no identity to scope to, so under user its data operations are REFUSED — declare system to make the elevation explicit. This covers schedule/time-relative/api triggers AND any record-change flow fired by a write that carried no user. | +| **errorHandling** | `{ strategy?: Enum<'fail' \| 'retry' \| 'continue'>; maxRetries?: integer; backoffMs?: integer; backoffMultiplier?: number; … }` | optional | Flow-level error handling configuration. A durable pause ends the retry-governed segment: strategy: 'retry' describes one synchronous dispatch, so a run that parks on an approval/screen/wait node and later resumes gets one attempt for anything that fails after the pause. Protect the post-pause half with its own failure handling in the flow — a try_catch node's retry around the post-resume work, or fault edges to a handler node. | +| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this flow. | +| **_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. | + --- diff --git a/content/docs/references/api/batch.mdx b/content/docs/references/api/batch.mdx index 2c3b3445f2..c8f3acc547 100644 --- a/content/docs/references/api/batch.mdx +++ b/content/docs/references/api/batch.mdx @@ -44,6 +44,15 @@ const result = BatchConfigSchema.parse(data); | **maxRecordsPerBatch** | `integer` | optional (default: `200`) | Maximum records per batch | | **defaultOptions** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean }` | optional | Default batch options | +### Nested Shape: `BatchConfig.defaultOptions` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **atomic** | `boolean` | optional (default: `false`) | Opt-in all-or-nothing. When explicitly true the whole batch runs inside ONE engine transaction: the first failure rolls back every prior write, and the response reports zero successes — each row carries `errors[0].code` ROLLED_BACK (written, then undone), the causal row its own error, and rows never reached NOT_ATTEMPTED. A runtime that cannot roll back REFUSES the request (501 NOT_IMPLEMENTED) rather than silently degrading to best-effort — probe `capabilities.transactionalBatch` on /discovery first. Takes precedence over continueOnError. Default false: sequential best-effort. | +| **returnRecords** | `boolean` | optional (default: `false`) | If true, return full record data in response | +| **continueOnError** | `boolean` | optional (default: `false`) | If true (and atomic=false), continue processing remaining records after errors. Default false: the first failure ENDS the run — records before it stay written (nothing is rolled back on this arm), and every record after it is reported `errors[0].code` NOT_ATTEMPTED rather than omitted, so `results` always covers all `total` records and `succeeded + failed === total` (#7539). | +| **validateOnly** | `never` | optional | [REMOVED] `options.validateOnly` was removed from BatchOptions in @objectstack/spec (#4052). It was never implemented: the batch surfaces persisted regardless, so a "dry-run" would have silently executed. There is no dry-run today — drop the key. If you need to preview a batch without writing, open an issue so it can be designed (no-commit cascade / constraint semantics) and reintroduced as a flag that actually holds. | + --- @@ -60,6 +69,29 @@ const result = BatchConfigSchema.parse(data); | **index** | `number` | optional | Index of the record in the request array | | **droppedFields** | `{ object: string; fields: string[]; reason: Enum<'readonly' \| 'readonly_when' \| 'primary_key'> }[]` | optional | Write-observability (#3407/#3431/#3455): caller-supplied fields LEGALLY stripped from THIS row before it was written — static `readonly` (#2948) / TRUE `readonlyWhen` (#3042) on update, or the #3043 create-ingress strip. Per-row because a batch can drop different fields on different rows (`readonlyWhen` is record-state-dependent). Present ONLY when ≥1 field was dropped for this row; the row still succeeded (success unchanged). A single response header cannot express per-row drops, so this body field is the canonical bulk channel — REST does not emit `X-ObjectStack-Dropped-Fields` for batches. Optional — omit-when-empty keeps the shape backward-compatible. | +### Nested Shape: `BatchOperationResult.errors[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `BatchOperationResult.droppedFields[number]` + +A write-path strip event: caller-supplied fields legally dropped from the payload (#3407) + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **object** | `string` | ✅ | Object the write targeted (resolved object name) | +| **fields** | `string[]` | ✅ | Caller-supplied field names the engine removed from the write payload | +| **reason** | `Enum<'readonly' \| 'readonly_when' \| 'primary_key'>` | ✅ | Why the fields were dropped: static readonly (#2948), a TRUE readonlyWhen predicate (#3042), or the primary-key strip of a payload id the engine ruled is not an identifier (#6437) | + --- @@ -112,6 +144,23 @@ const result = BatchConfigSchema.parse(data); | **records** | `{ id?: string; data?: Record; externalId?: string }[]` | ✅ | Array of records to process (server caps the count — see batch.maxBatchSize) | | **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean }` | optional | Batch operation options | +### Nested Shape: `BatchUpdateRequest.records[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | optional | Record ID (required for update/delete) | +| **data** | `Record` | optional | Record data (required for create/update/upsert) | +| **externalId** | `string` | optional | External ID for upsert matching | + +### Nested Shape: `BatchUpdateRequest.options` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **atomic** | `boolean` | optional (default: `false`) | Opt-in all-or-nothing. When explicitly true the whole batch runs inside ONE engine transaction: the first failure rolls back every prior write, and the response reports zero successes — each row carries `errors[0].code` ROLLED_BACK (written, then undone), the causal row its own error, and rows never reached NOT_ATTEMPTED. A runtime that cannot roll back REFUSES the request (501 NOT_IMPLEMENTED) rather than silently degrading to best-effort — probe `capabilities.transactionalBatch` on /discovery first. Takes precedence over continueOnError. Default false: sequential best-effort. | +| **returnRecords** | `boolean` | optional (default: `false`) | If true, return full record data in response | +| **continueOnError** | `boolean` | optional (default: `false`) | If true (and atomic=false), continue processing remaining records after errors. Default false: the first failure ENDS the run — records before it stay written (nothing is rolled back on this arm), and every record after it is reported `errors[0].code` NOT_ATTEMPTED rather than omitted, so `results` always covers all `total` records and `succeeded + failed === total` (#7539). | +| **validateOnly** | `never` | optional | [REMOVED] `options.validateOnly` was removed from BatchOptions in @objectstack/spec (#4052). It was never implemented: the batch surfaces persisted regardless, so a "dry-run" would have silently executed. There is no dry-run today — drop the key. If you need to preview a batch without writing, open an issue so it can be designed (no-commit cascade / constraint semantics) and reintroduced as a flag that actually holds. | + --- @@ -130,6 +179,30 @@ const result = BatchConfigSchema.parse(data); | **failed** | `number` | ✅ | Number of records that failed | | **results** | `{ id?: string; success: boolean; errors?: object[]; data?: Record; … }[]` | ✅ | Detailed results for each record | +### Nested Shape: `BatchUpdateResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `BatchUpdateResponse.results[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | optional | Record ID if operation succeeded | +| **success** | `boolean` | ✅ | Whether this record was processed successfully | +| **errors** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>; declaredCode?: string; message: string; userMessage?: string; … }[]` | optional | Array of errors if operation failed. Branch on `errors[0].code` — an atomic batch that rolled back marks rows that were written then undone with code ROLLED_BACK and rows never reached with NOT_ATTEMPTED, while the causal row keeps its own error (#4793). A NON-atomic batch that stopped (the `continueOnError: false` default) marks its un-attempted tail with the same NOT_ATTEMPTED code — rows before the failure stay written and keep reporting success, since nothing was rolled back (#7539). | +| **data** | `Record` | optional | Full record data (if returnRecords=true) | +| **index** | `number` | optional | Index of the record in the request array | +| **droppedFields** | `{ object: string; fields: string[]; reason: Enum<'readonly' \| 'readonly_when' \| 'primary_key'> }[]` | optional | Write-observability (#3407/#3431/#3455): caller-supplied fields LEGALLY stripped from THIS row before it was written — static `readonly` (#2948) / TRUE `readonlyWhen` (#3042) on update, or the #3043 create-ingress strip. Per-row because a batch can drop different fields on different rows (`readonlyWhen` is record-state-dependent). Present ONLY when ≥1 field was dropped for this row; the row still succeeded (success unchanged). A single response header cannot express per-row drops, so this body field is the canonical bulk channel — REST does not emit `X-ObjectStack-Dropped-Fields` for batches. Optional — omit-when-empty keeps the shape backward-compatible. | + --- @@ -172,6 +245,15 @@ A cross-object batch strip event: dropped fields plus the operation index | **operations** | `{ object: string; action: Enum<'create' \| 'update' \| 'delete'>; id?: string; data?: Record }[]` | ✅ | Ordered operations executed in one transaction | | **atomic** | `boolean` | optional (default: `true`) | Always true — the cross-object batch is all-or-nothing | +### Nested Shape: `CrossObjectBatchRequest.operations[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **object** | `string` | ✅ | Target object (table) name | +| **action** | `Enum<'create' \| 'update' \| 'delete'>` | optional (default: `"create"`) | Operation to perform (default: create) | +| **id** | `string` | optional | Target record id — required for update and delete | +| **data** | `Record` | optional | Record payload for create/update; a value may be `{ $ref: }` to reference an earlier op's created id | + --- @@ -184,6 +266,17 @@ A cross-object batch strip event: dropped fields plus the operation index | **results** | `any[]` | ✅ | Per-operation result, index-aligned with the request operations | | **droppedFields** | `{ object: string; fields: string[]; reason: Enum<'readonly' \| 'readonly_when' \| 'primary_key'>; index: integer }[]` | optional | Write-observability (#3407/#3431/#3455/#3794): caller-supplied fields the engine LEGALLY stripped from an operation before it was written — static `readonly` (#2948) or a TRUE `readonlyWhen` predicate (#3042). This endpoint is the console record form's save path (master-detail writes parent + children in one transaction), so without it the ONE surface where a user edits a `readonlyWhen` field reported plain success while the value never landed. Each event carries the `index` of its operation. Present ONLY when ≥1 field was dropped; the batch still committed without them (results/success semantics unchanged). Optional — omit-when-empty keeps the shape backward-compatible. | +### Nested Shape: `CrossObjectBatchResponse.droppedFields[number]` + +A cross-object batch strip event: dropped fields plus the operation index + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **object** | `string` | ✅ | Object the write targeted (resolved object name) | +| **fields** | `string[]` | ✅ | Caller-supplied field names the engine removed from the write payload | +| **reason** | `Enum<'readonly' \| 'readonly_when' \| 'primary_key'>` | ✅ | Why the fields were dropped: static readonly (#2948), a TRUE readonlyWhen predicate (#3042), or the primary-key strip of a payload id the engine ruled is not an identifier (#6437) | +| **index** | `integer` | ✅ | Index of the operation in the request `operations` array | + --- @@ -196,6 +289,15 @@ A cross-object batch strip event: dropped fields plus the operation index | **ids** | `string[]` | ✅ | Array of record IDs to delete (server caps the count — see batch.maxBatchSize) | | **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean }` | optional | Delete options | +### Nested Shape: `DeleteManyRequest.options` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **atomic** | `boolean` | optional (default: `false`) | Opt-in all-or-nothing. When explicitly true the whole batch runs inside ONE engine transaction: the first failure rolls back every prior write, and the response reports zero successes — each row carries `errors[0].code` ROLLED_BACK (written, then undone), the causal row its own error, and rows never reached NOT_ATTEMPTED. A runtime that cannot roll back REFUSES the request (501 NOT_IMPLEMENTED) rather than silently degrading to best-effort — probe `capabilities.transactionalBatch` on /discovery first. Takes precedence over continueOnError. Default false: sequential best-effort. | +| **returnRecords** | `boolean` | optional (default: `false`) | If true, return full record data in response | +| **continueOnError** | `boolean` | optional (default: `false`) | If true (and atomic=false), continue processing remaining records after errors. Default false: the first failure ENDS the run — records before it stay written (nothing is rolled back on this arm), and every record after it is reported `errors[0].code` NOT_ATTEMPTED rather than omitted, so `results` always covers all `total` records and `succeeded + failed === total` (#7539). | +| **validateOnly** | `never` | optional | [REMOVED] `options.validateOnly` was removed from BatchOptions in @objectstack/spec (#4052). It was never implemented: the batch surfaces persisted regardless, so a "dry-run" would have silently executed. There is no dry-run today — drop the key. If you need to preview a batch without writing, open an issue so it can be designed (no-commit cascade / constraint semantics) and reintroduced as a flag that actually holds. | + --- @@ -220,6 +322,22 @@ A cross-object batch strip event: dropped fields plus the operation index | **records** | `{ id: string; data: Record }[]` | ✅ | Array of records to update (server caps the count — see batch.maxBatchSize) | | **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean }` | optional | Update options | +### Nested Shape: `UpdateManyRequest.records[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Record ID | +| **data** | `Record` | ✅ | Fields to update | + +### Nested Shape: `UpdateManyRequest.options` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **atomic** | `boolean` | optional (default: `false`) | Opt-in all-or-nothing. When explicitly true the whole batch runs inside ONE engine transaction: the first failure rolls back every prior write, and the response reports zero successes — each row carries `errors[0].code` ROLLED_BACK (written, then undone), the causal row its own error, and rows never reached NOT_ATTEMPTED. A runtime that cannot roll back REFUSES the request (501 NOT_IMPLEMENTED) rather than silently degrading to best-effort — probe `capabilities.transactionalBatch` on /discovery first. Takes precedence over continueOnError. Default false: sequential best-effort. | +| **returnRecords** | `boolean` | optional (default: `false`) | If true, return full record data in response | +| **continueOnError** | `boolean` | optional (default: `false`) | If true (and atomic=false), continue processing remaining records after errors. Default false: the first failure ENDS the run — records before it stay written (nothing is rolled back on this arm), and every record after it is reported `errors[0].code` NOT_ATTEMPTED rather than omitted, so `results` always covers all `total` records and `succeeded + failed === total` (#7539). | +| **validateOnly** | `never` | optional | [REMOVED] `options.validateOnly` was removed from BatchOptions in @objectstack/spec (#4052). It was never implemented: the batch surfaces persisted regardless, so a "dry-run" would have silently executed. There is no dry-run today — drop the key. If you need to preview a batch without writing, open an issue so it can be designed (no-commit cascade / constraint semantics) and reintroduced as a flag that actually holds. | + --- diff --git a/content/docs/references/api/contract.mdx b/content/docs/references/api/contract.mdx index 580eace1df..c7c110f075 100644 --- a/content/docs/references/api/contract.mdx +++ b/content/docs/references/api/contract.mdx @@ -346,6 +346,19 @@ const result = ApiErrorSchema.parse(data); | **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>; declaredCode?: string; message: string; userMessage?: string; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | +### Nested Shape: `BaseResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + --- @@ -386,6 +399,29 @@ const result = ApiErrorSchema.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ id?: string; success: boolean; errors?: object[]; index?: number; … }[]` | ✅ | Results for each item in the batch | +### Nested Shape: `BulkResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `BulkResponse.data[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | optional | Record ID if processed | +| **success** | `boolean` | ✅ | | +| **errors** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>; declaredCode?: string; message: string; userMessage?: string; … }[]` | optional | | +| **index** | `number` | optional | Index in original request | +| **data** | `any` | optional | Result data (e.g. created record) | + --- @@ -428,6 +464,19 @@ const result = ApiErrorSchema.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **id** | `string` | ✅ | ID of the deleted record | +### Nested Shape: `DeleteResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + --- @@ -455,6 +504,59 @@ const result = ApiErrorSchema.parse(data); | **distinct** | `never` | optional | [REMOVED] `query.distinct` was removed in @objectstack/spec 17 (#4286, ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded `total`/`hasMore` to a page-local estimate while still returning duplicate rows. Delete the key; `QueryBuilder.distinct()` was removed with it, and the count suppression is gone (`total` is truthful again). For unique values of one column use the SQL/memory drivers' `distinct(object, field)` door; for unique combinations, `groupBy`; for a deduplicated count, the `count_distinct` aggregation. | | **expand** | `Record` | optional | Recursive relation loading map. Keys are lookup/master_detail field names; values are nested QueryAST objects that control select (`fields`) and filter (`where`, AND-merged with the batch $in), plus further expansion on the related object. The engine resolves expand via batch $in queries (driver-agnostic) with a default max depth of 3; per-parent `limit`/`offset`/`orderBy` are NOT applied on this path. | +### Nested Shape: `ExportRequest.search` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **query** | `string` | ✅ | Search query text | +| **fields** | `string[]` | optional | Fields to search in (if not specified, searches all text fields) | +| **fuzzy** | `boolean` | optional (default: `false`) | [EXPERIMENTAL — not enforced] Fuzzy matching (tolerate typos). The ADR-0061 expansion reads only `query` + `fields`; no executor receives this flag (#4286). | +| **operator** | `Enum<'and' \| 'or'>` | optional (default: `"or"`) | [EXPERIMENTAL — not enforced] Logical operator between terms. The ADR-0061 expansion applies its own term semantics; no executor receives this flag (#4286). | +| **boost** | `Record` | optional | [EXPERIMENTAL — not enforced] Field-specific relevance boosting (field name -> boost factor). No executor scores results (#4286). | +| **minScore** | `number` | optional | [EXPERIMENTAL — not enforced] Minimum relevance score threshold. No executor scores results (#4286). | +| **language** | `string` | optional | [EXPERIMENTAL — not enforced] Language for text analysis (e.g., "en", "zh", "es"). No executor selects an analyzer (#4286). | +| **highlight** | `boolean` | optional (default: `false`) | [EXPERIMENTAL — not enforced] Search result highlighting. No executor emits highlights (#4286). | + +### Nested Shape: `ExportRequest.aggregations[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **function** | `Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>` | ✅ | Aggregation function | +| **field** | `string` | optional | Field to aggregate (optional for COUNT(*)) | +| **alias** | `string` | ✅ | Result column alias | +| **distinct** | `never` | optional | [REMOVED] `query.aggregations[].distinct` was removed in @objectstack/spec 17 (#6815, ADR-0049) — exactly ONE of the six faces that read an aggregation honoured it. The objectql in-memory fallback deduplicated the values before applying the function, while `driver-sql`, `driver-turso`, `driver-mongodb`, `driver-memory` and the service-analytics SQL builder all ignored it — so `{ function: 'sum', field: 'amount', distinct: true }` answered a DEDUPLICATED sum when the engine fell back in memory and an ordinary sum on every SQL datasource: one query, two numbers, chosen by which backend happened to serve it. Both answers are plausible, so nothing surfaced the divergence. Delete the key. For a deduplicated COUNT the live spelling is the `count_distinct` aggregation function, which every SQL face compiles to `COUNT(DISTINCT field)` (#6409) and the in-memory fallback computes identically. `SUM(DISTINCT …)` / `AVG(DISTINCT …)` get no replacement: no backend ever computed them here, and a per-row measure that needs deduplicating is a modelling problem to fix in the data, not a flag on the read. | +| **filter** | `any` | optional | Per-aggregation filter (SQL FILTER (WHERE …) semantics): narrows the source rows THIS aggregation reads, leaving sibling aggregations unfiltered. Enforced by engine.aggregate (#10576): lowered in memory for drivers without native conditional aggregation; a driver reached directly refuses rather than silently dropping it. | + +### Nested Shape: `ExportRequest.groupBy[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field to group by | +| **dateGranularity** | `Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>` | optional | Bucket date values into uniform periods (day/week/month/quarter/year) | +| **alias** | `string` | optional | Alias for the projected group value | + +### Nested Shape: `ExportRequest.expand[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **object** | `string` | ✅ | Object name (e.g. account) | +| **fields** | `string[]` | optional | Fields to retrieve — names of the queried object's OWN columns. A dotted path (`owner.name`) is not a projection: no driver resolves one, and the ingress refuses it with `400 INVALID_FIELD` (#7532). Related data is read with `expand`, whose nested QueryAST both filters (`where`) and selects (`fields`) the related record's columns. The projection must RETAIN the foreign-key column: `fields: ['title']` with `expand: 'project_id'` resolves nothing, because the relation is carried by that key — add `'project_id'` and it works. Where the value is wanted on the queried object itself, denormalise it onto that object (a stored field, written when the source changes), the same remedy the sort axis prescribes (#6924). | +| **where** | `any` | optional | Filtering criteria (WHERE) | +| **search** | `string \| { query: string; fields?: string[]; fuzzy: boolean; operator: Enum<'and' \| 'or'>; … }` | optional | Full-text search — the query text (canonical, ADR-0061 D1), or a structured FullTextSearch configuration | +| **searchFields** | `string[]` | optional | Narrow the search to these fields (server-intersected with the allowed searchable set — can only narrow, never widen; ADR-0061 D1) | +| **orderBy** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Sorting instructions (ORDER BY) | +| **limit** | `number` | optional | Max records to return (LIMIT) | +| **offset** | `number` | optional | Records to skip (OFFSET) | +| **top** | `number` | optional | Alias for limit (OData compatibility) | +| **cursor** | `never` | optional | [REMOVED] `query.cursor` was removed in @objectstack/spec 17 (#4286, ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; `QueryBuilder.cursor()` was removed with it. Express the keyset as an ordinary `where` predicate on your sort key — `where: { created_at: { $gt: last.created_at } }` with the matching `orderBy` — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. | +| **joins** | `never` | optional | [REMOVED] `query.joins` was removed in @objectstack/spec 17 (#4286, ADR-0049) — no engine or driver ever read it: a query carrying `joins` behaved exactly as if the key were absent, while its name squatted on the reserved REST parameter set. Delete the key. Related records are read through `expand` — `expand: { owner_id: { object: 'user', fields: ['name'] } }` — which the engine resolves via batch $in queries, and whose nested query selects the related record's own columns. Keep the foreign key in your own projection (`fields: ['title', 'owner_id']`): the relation is carried by that column, so projecting it away leaves expansion nothing to resolve. A dotted `fields` path is NOT a replacement — no driver ever resolved one and the ingress refuses it (`400 INVALID_FIELD`, #7532). | +| **aggregations** | `{ function: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>; field?: string; alias: string; filter?: any }[]` | optional | Aggregation functions | +| **groupBy** | `(string \| { field: string; dateGranularity?: Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>; alias?: string })[]` | optional | GROUP BY targets (strings or `{field, dateGranularity?}` objects for date bucketing) | +| **having** | `any` | optional | HAVING — filter over the AGGREGATED rows (aggregation aliases + groupBy projections); applied engine-side after aggregation | +| **windowFunctions** | `never` | optional | [REMOVED] `query.windowFunctions` was removed in @objectstack/spec 17 (#4286, ADR-0049) — `find()` never applied it: no engine or driver read the key on the query path, so every OVER clause it declared was silently dropped. Delete the key. Window functions are a SQL-driver capability behind `SqlDriver.findWithWindowFunctions(object, query)` (embedder-level; not on the `IDataDriver` contract or the REST surface); request-level analytics are `aggregations` + `groupBy`. | +| **distinct** | `never` | optional | [REMOVED] `query.distinct` was removed in @objectstack/spec 17 (#4286, ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded `total`/`hasMore` to a page-local estimate while still returning duplicate rows. Delete the key; `QueryBuilder.distinct()` was removed with it, and the count suppression is gone (`total` is truthful again). For unique values of one column use the SQL/memory drivers' `distinct(object, field)` door; for unique combinations, `groupBy`; for a deduplicated count, the `count_distinct` aggregation. | +| **expand** | `Record` | optional | Recursive relation loading map. Keys are lookup/master_detail field names; values are nested QueryAST objects that control select (`fields`) and filter (`where`, AND-merged with the batch $in), plus further expansion on the related object. The engine resolves expand via batch $in queries (driver-agnostic) with a default max depth of 3; per-parent `limit`/`offset`/`orderBy` are NOT applied on this path. | + --- @@ -481,6 +583,30 @@ const result = ApiErrorSchema.parse(data); | **data** | `Record[]` | ✅ | Array of matching records | | **pagination** | `{ total?: number; limit?: number; offset?: number; cursor?: string; … }` | ✅ | Pagination info | +### Nested Shape: `ListRecordResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `ListRecordResponse.pagination` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **total** | `number` | optional | Total matching records count | +| **limit** | `number` | optional | Page size | +| **offset** | `number` | optional | Page offset | +| **cursor** | `string` | optional | Cursor for next page | +| **nextCursor** | `string` | optional | Next cursor for pagination | +| **hasMore** | `boolean` | ✅ | Are there more pages? | + --- @@ -496,6 +622,19 @@ const result = ApiErrorSchema.parse(data); | **index** | `number` | optional | Index in original request | | **data** | `any` | optional | Result data (e.g. created record) | +### Nested Shape: `ModificationResult.errors[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + --- @@ -512,6 +651,27 @@ const result = ApiErrorSchema.parse(data); | **queryComplexityLimit** | `number` | optional | Maximum allowed query complexity score | | **enableQueryPlan** | `boolean` | optional (default: `false`) | Log query execution plans for debugging | +### Nested Shape: `QueryOptimizationConfig.dataLoader` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **maxBatchSize** | `integer` | optional (default: `100`) | Maximum number of keys per batch load | +| **batchScheduleFn** | `Enum<'microtask' \| 'timeout' \| 'manual'>` | optional (default: `"microtask"`) | Scheduling strategy for collecting batch keys | +| **cacheEnabled** | `boolean` | optional (default: `true`) | Enable per-request result caching | +| **cacheKeyFn** | `string` | optional | Name or identifier of the cache key function | +| **cacheTtl** | `number` | optional | Cache time-to-live in seconds (0 = no expiration) | +| **coalesceRequests** | `boolean` | optional (default: `true`) | Deduplicate identical requests within a batch window | +| **maxConcurrency** | `integer` | optional | Maximum parallel batch requests | + +### Nested Shape: `QueryOptimizationConfig.batchStrategy` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **strategy** | `Enum<'dataloader' \| 'windowed' \| 'prefetch'>` | ✅ | Batch loading strategy type | +| **windowMs** | `number` | optional | Collection window duration in milliseconds (for windowed strategy) | +| **prefetchDepth** | `integer` | optional | Depth of relation prefetching (for prefetch strategy) | +| **associationLoading** | `Enum<'lazy' \| 'eager' \| 'batch'>` | optional (default: `"batch"`) | How to load related associations | + --- @@ -535,6 +695,19 @@ Key-value map of record data | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `Record` | ✅ | The requested or modified record | +### Nested Shape: `SingleRecordResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + --- diff --git a/content/docs/references/api/discovery.mdx b/content/docs/references/api/discovery.mdx index f646c9e9aa..24b7191c58 100644 --- a/content/docs/references/api/discovery.mdx +++ b/content/docs/references/api/discovery.mdx @@ -88,6 +88,75 @@ const result = ApiRoutesSchema.parse(data); | **scoping** | `{ enabled: boolean; resolution: Enum<'required' \| 'optional' \| 'auto'>; scoped: boolean; environmentId?: string }` | optional | Environment-scoping posture, added by the REST discovery endpoint | | **metadata** | `Record` | optional | Custom metadata key-value pairs for extensibility | +### Nested Shape: `Discovery.routes` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **data** | `string` | ✅ | e.g. /api/v1/data | +| **metadata** | `string` | ✅ | e.g. /api/v1/meta | +| **discovery** | `string` | optional | e.g. /api/v1/discovery | +| **ui** | `string` | optional | e.g. /api/v1/ui | +| **auth** | `string` | optional | e.g. /api/v1/auth | +| **automation** | `string` | optional | e.g. /api/v1/automation | +| **storage** | `string` | optional | e.g. /api/v1/storage | +| **analytics** | `string` | optional | e.g. /api/v1/analytics | +| **packages** | `string` | optional | e.g. /api/v1/packages | +| **datasources** | `string` | optional | e.g. /api/v1/datasources — base for the datasources/:name/external/* federation-admin family; absent when no host mounts it | +| **email** | `string` | optional | e.g. /api/v1/email — base for the email/send endpoint; absent when no host mounts it | +| **approvals** | `string` | optional | e.g. /api/v1/approvals | +| **realtime** | `string` | optional | e.g. /api/v1/realtime | +| **notifications** | `string` | optional | e.g. /api/v1/notifications | +| **ai** | `string` | optional | e.g. /api/v1/ai | +| **i18n** | `string` | optional | e.g. /api/v1/i18n | +| **mcp** | `string` | optional | e.g. /api/v1/mcp — always the unscoped base; absent when MCP is disabled or unserveable | + +### Nested Shape: `Discovery.services[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | ✅ | | +| **status** | `Enum<'available' \| 'registered' \| 'unavailable' \| 'degraded' \| 'stub'>` | ✅ | available = fully operational, registered = route declared but handler unverified, unavailable = not installed, degraded = partial, stub = placeholder that returns 501 | +| **handlerReady** | `boolean` | optional | Whether the HTTP handler is confirmed to be mounted. Omitted = readiness unknown/unverified; true = handler mounted; false = handler missing or stub (likely 501). | +| **route** | `string` | optional | e.g. /api/v1/analytics | +| **provider** | `string` | optional | e.g. "objectql", "plugin-redis", "driver-memory" | +| **version** | `string` | optional | Semantic version of the service implementation (e.g. "3.0.6") | +| **message** | `string` | optional | e.g. "Install plugin-workflow to enable" | +| **rateLimit** | `{ requestsPerMinute?: integer; requestsPerHour?: integer; burstLimit?: integer; retryAfterMs?: integer }` | optional | Rate limit and quota info for this service | + +### Nested Shape: `Discovery.capabilities` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **comments** | `{ enabled: boolean; features?: Record; description?: string }` | ✅ | Whether the backend supports record comments / chatter (the `sys_comment` object served via the data API) | +| **automation** | `{ enabled: boolean; features?: Record; description?: string }` | ✅ | Whether the backend supports Automation CRUD (flows, triggers) | +| **cron** | `{ enabled: boolean; features?: Record; description?: string }` | ✅ | Whether the backend supports cron scheduling | +| **search** | `{ enabled: boolean; features?: Record; description?: string }` | ✅ | Whether the backend supports full-text search | +| **export** | `{ enabled: boolean; features?: Record; description?: string }` | ✅ | Whether the backend supports async export | +| **chunkedUpload** | `{ enabled: boolean; features?: Record; description?: string }` | ✅ | Whether the backend supports chunked (multipart) uploads | +| **transactionalBatch** | `{ enabled: boolean; features?: Record; description?: string }` | ✅ | Whether the backend exposes the atomic cross-object batch endpoint (POST `{basePath}`/batch, #1604/ADR-0034): all ops commit or roll back together in one transaction. Lets clients skip non-atomic client-side simulation instead of runtime-probing 404/405/501. True ⟺ the /batch route is mounted AND the runtime can honour a transaction. | +| **websockets** | `{ enabled: boolean; features?: Record; description?: string }` | ✅ | Whether the backend mounts a realtime push surface (WebSocket/SSE) clients can subscribe to. False while realtime is an in-process bus with no mounted HTTP/WS surface (ADR-0076 D12, #2462). | +| **files** | `{ enabled: boolean; features?: Record; description?: string }` | ✅ | Whether a file-storage surface (upload/download/attachments) is served | +| **analytics** | `{ enabled: boolean; features?: Record; description?: string }` | ✅ | Whether the backend serves the analytics / BI query surface | +| **ai** | `{ enabled: boolean; features?: Record; description?: string }` | ✅ | Whether the backend serves the AI surface (NLQ, chat, agents, suggest) | +| **notifications** | `{ enabled: boolean; features?: Record; description?: string }` | ✅ | Whether the backend serves the notification surface (inbox, delivery) | +| **i18n** | `{ enabled: boolean; features?: Record; description?: string }` | ✅ | Whether the backend serves the i18n surface (translations, locale negotiation) | + +### Nested Shape: `Discovery.schemaDiscovery` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **openapi** | `string` | optional | URL to OpenAPI (Swagger) specification (e.g., "/api/v1/openapi.json") | +| **jsonSchema** | `string` | optional | URL to JSON Schema definitions | + +### Nested Shape: `Discovery.scoping` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | ✅ | Whether environment-scoped routes are mounted at all | +| **resolution** | `Enum<'required' \| 'optional' \| 'auto'>` | ✅ | How the environment id is resolved when scoping is enabled (mirrors RestApiConfig.projectResolution) | +| **scoped** | `boolean` | ✅ | Whether THIS response was served from the environment-scoped mount | +| **environmentId** | `string` | optional | The resolved environment id — present only on a scoped mount | + --- @@ -134,6 +203,18 @@ Deployment posture a discovery response advertises. Deliberately three coarse bu | **totalMissing** | `integer` | ✅ | Routes missing a handler | | **routes** | `{ route: string; method: Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>; service: string; declared: boolean; … }[]` | ✅ | Per-route health entries | +### Nested Shape: `RouteHealthReport.routes[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **route** | `string` | ✅ | Route path pattern | +| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>` | ✅ | HTTP method (GET, POST, etc.) | +| **service** | `string` | ✅ | Target service name | +| **declared** | `boolean` | ✅ | Whether the route is declared in discovery/metadata | +| **handlerRegistered** | `boolean` | ✅ | Whether the HTTP handler is registered | +| **healthStatus** | `Enum<'pass' \| 'fail' \| 'missing' \| 'skip'>` | ✅ | pass = handler responds, fail = 501/503, missing = no handler (404), skip = not checked | +| **message** | `string` | optional | Diagnostic message | + --- @@ -152,6 +233,15 @@ Deployment posture a discovery response advertises. Deliberately three coarse bu | **message** | `string` | optional | e.g. "Install plugin-workflow to enable" | | **rateLimit** | `{ requestsPerMinute?: integer; requestsPerHour?: integer; burstLimit?: integer; retryAfterMs?: integer }` | optional | Rate limit and quota info for this service | +### Nested Shape: `ServiceInfo.rateLimit` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **requestsPerMinute** | `integer` | optional | Maximum requests per minute | +| **requestsPerHour** | `integer` | optional | Maximum requests per hour | +| **burstLimit** | `integer` | optional | Maximum burst request count | +| **retryAfterMs** | `integer` | optional | Suggested retry-after delay in milliseconds when rate-limited | + --- diff --git a/content/docs/references/api/dispatcher.mdx b/content/docs/references/api/dispatcher.mdx index 89fec1c587..fba66f32f4 100644 --- a/content/docs/references/api/dispatcher.mdx +++ b/content/docs/references/api/dispatcher.mdx @@ -51,6 +51,16 @@ const result = DispatcherConfigSchema.parse(data); | **fallback** | `Enum<'404' \| 'proxy' \| 'custom'>` | optional (default: `"404"`) | Behavior when no route matches | | **proxyTarget** | `string` | optional | Proxy target URL when fallback is "proxy" | +### Nested Shape: `DispatcherConfig.routes[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **prefix** | `string` | ✅ | URL path prefix for routing (e.g. /api/v1/data) | +| **service** | `Enum<'metadata' \| 'data' \| 'auth' \| 'storage' \| 'file-storage' \| 'search' \| 'cache' \| …>` | ✅ | Target core service name | +| **authRequired** | `boolean` | optional (default: `true`) | Whether authentication is required | +| **criticality** | `Enum<'required' \| 'core' \| 'optional'>` | optional (default: `"optional"`) | Service criticality level for unavailability handling | +| **permissions** | `string[]` | optional | Required permissions for this route namespace | + --- @@ -77,6 +87,18 @@ Route-resolution failure mode emitted in `error.code` | **success** | `false` | ✅ | | | **error** | `{ code: string; message: string; httpStatus?: integer; route?: string; … }` | ✅ | | +### Nested Shape: `DispatcherErrorResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `string` | ✅ | Machine-readable error code (e.g. ROUTE_NOT_FOUND, permission_denied) | +| **message** | `string` | ✅ | Human-readable error message | +| **httpStatus** | `integer` | optional | HTTP status code (404, 405, 501, 503, …) | +| **route** | `string` | optional | Requested route path | +| **service** | `string` | optional | Target service name, if resolvable | +| **hint** | `string` | optional | Actionable hint for the developer (e.g., "Install plugin-workflow") | +| **details** | `any` | optional | Additional error context | + --- diff --git a/content/docs/references/api/documentation.mdx b/content/docs/references/api/documentation.mdx index c1b06db88b..36fa8932c6 100644 --- a/content/docs/references/api/documentation.mdx +++ b/content/docs/references/api/documentation.mdx @@ -64,6 +64,17 @@ const result = ApiChangelogEntrySchema.parse(data); | **changes** | `{ added: string[]; changed: string[]; deprecated: string[]; removed: string[]; … }` | ✅ | Version changes | | **migrationGuide** | `string` | optional | Migration guide URL or text | +### Nested Shape: `ApiChangelogEntry.changes` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **added** | `string[]` | optional (default: `[]`) | New features | +| **changed** | `string[]` | optional (default: `[]`) | Changes | +| **deprecated** | `string[]` | optional (default: `[]`) | Deprecations | +| **removed** | `string[]` | optional (default: `[]`) | Removed features | +| **fixed** | `string[]` | optional (default: `[]`) | Bug fixes | +| **security** | `string[]` | optional (default: `[]`) | Security fixes | + --- @@ -91,6 +102,72 @@ const result = ApiChangelogEntrySchema.parse(data); | **securitySchemes** | `Record; scheme?: string; bearerFormat?: string; name?: string; … }>` | optional | Security scheme definitions | | **tags** | `{ name: string; description?: string; externalDocs?: object }[]` | optional | Global tag definitions | +### Nested Shape: `ApiDocumentationConfig.servers[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **url** | `string` | ✅ | Server base URL | +| **description** | `string` | optional | Server description | +| **variables** | `Record` | optional | URL template variables | + +### Nested Shape: `ApiDocumentationConfig.ui` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'swagger-ui' \| 'redoc' \| 'rapidoc' \| 'stoplight' \| 'scalar' \| 'graphiql' \| 'postman' \| 'custom'>` | ✅ | Testing UI implementation | +| **path** | `string` | optional (default: `"/api-docs"`) | URL path for documentation UI | +| **theme** | `Enum<'light' \| 'dark' \| 'auto'>` | optional (default: `"light"`) | UI color theme | +| **enableTryItOut** | `boolean` | optional (default: `true`) | Enable interactive API testing | +| **enableFilter** | `boolean` | optional (default: `true`) | Enable endpoint filtering | +| **enableCors** | `boolean` | optional (default: `true`) | Enable CORS for browser testing | +| **defaultModelsExpandDepth** | `integer` | optional (default: `1`) | Default expand depth for schemas (-1 = fully expand) | +| **displayRequestDuration** | `boolean` | optional (default: `true`) | Show request duration | +| **syntaxHighlighting** | `boolean` | optional (default: `true`) | Enable syntax highlighting | +| **customCssUrl** | `string` | optional | Custom CSS stylesheet URL | +| **customJsUrl** | `string` | optional | Custom JavaScript URL | +| **layout** | `{ showExtensions: boolean; showCommonExtensions: boolean; deepLinking: boolean; displayOperationId: boolean; … }` | optional | Layout configuration | + +### Nested Shape: `ApiDocumentationConfig.testCollections[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Collection name | +| **description** | `string` | optional | Collection description | +| **variables** | `Record` | optional (default: `{}`) | Shared variables | +| **requests** | `{ name: string; description?: string; method: Enum<'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE' \| 'HEAD' \| 'OPTIONS'>; url: string; … }[]` | ✅ | Test requests in this collection | +| **folders** | `{ name: string; description?: string; requests: object[] }[]` | optional | Request folders for organization | + +### Nested Shape: `ApiDocumentationConfig.changelog[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **version** | `string` | ✅ | API version | +| **date** | `string` | ✅ | Release date | +| **changes** | `{ added: string[]; changed: string[]; deprecated: string[]; removed: string[]; … }` | ✅ | Version changes | +| **migrationGuide** | `string` | optional | Migration guide URL or text | + +### Nested Shape: `ApiDocumentationConfig.codeTemplates[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **language** | `string` | ✅ | Target language/framework (e.g., typescript, python, curl) | +| **name** | `string` | ✅ | Template name | +| **template** | `string` | ✅ | Code template with placeholders | +| **variables** | `string[]` | optional | Required template variables | + +### Nested Shape: `ApiDocumentationConfig.securitySchemes[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'apiKey' \| 'http' \| 'oauth2' \| 'openIdConnect'>` | ✅ | Security type | +| **scheme** | `string` | optional | HTTP auth scheme (bearer, basic, etc.) | +| **bearerFormat** | `string` | optional | Bearer token format (e.g., JWT) | +| **name** | `string` | optional | API key parameter name | +| **in** | `Enum<'header' \| 'query' \| 'cookie'>` | optional | API key location | +| **flows** | `{ implicit?: any; password?: any; clientCredentials?: any; authorizationCode?: any }` | optional | OAuth2 flows | +| **openIdConnectUrl** | `string` | optional | OpenID Connect discovery URL | +| **description** | `string` | optional | Security scheme description | + --- @@ -106,6 +183,20 @@ const result = ApiChangelogEntrySchema.parse(data); | **requests** | `{ name: string; description?: string; method: Enum<'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE' \| 'HEAD' \| 'OPTIONS'>; url: string; … }[]` | ✅ | Test requests in this collection | | **folders** | `{ name: string; description?: string; requests: object[] }[]` | optional | Request folders for organization | +### Nested Shape: `ApiTestCollection.requests[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Test request name | +| **description** | `string` | optional | Request description | +| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE' \| 'HEAD' \| 'OPTIONS'>` | ✅ | HTTP method | +| **url** | `string` | ✅ | Request URL (can include variables) | +| **headers** | `Record` | optional (default: `{}`) | Request headers | +| **queryParams** | `Record` | optional (default: `{}`) | Query parameters | +| **body** | `any` | optional | Request body | +| **variables** | `Record` | optional (default: `{}`) | Template variables | +| **expectedResponse** | `{ statusCode: integer; body?: any }` | optional | Expected response for validation | + --- @@ -147,6 +238,19 @@ const result = ApiChangelogEntrySchema.parse(data); | **customJsUrl** | `string` | optional | Custom JavaScript URL | | **layout** | `{ showExtensions: boolean; showCommonExtensions: boolean; deepLinking: boolean; displayOperationId: boolean; … }` | optional | Layout configuration | +### Nested Shape: `ApiTestingUiConfig.layout` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **showExtensions** | `boolean` | optional (default: `false`) | Show vendor extensions | +| **showCommonExtensions** | `boolean` | optional (default: `false`) | Show common extensions | +| **deepLinking** | `boolean` | optional (default: `true`) | Enable deep linking | +| **displayOperationId** | `boolean` | optional (default: `false`) | Display operation IDs | +| **defaultModelRendering** | `Enum<'example' \| 'model'>` | optional (default: `"example"`) | Default model rendering mode | +| **defaultModelsExpandDepth** | `integer` | optional (default: `1`) | Models expand depth | +| **defaultModelExpandDepth** | `integer` | optional (default: `1`) | Single model expand depth | +| **docExpansion** | `Enum<'list' \| 'full' \| 'none'>` | optional (default: `"list"`) | Documentation expansion mode | + --- @@ -193,6 +297,29 @@ const result = ApiChangelogEntrySchema.parse(data); | **generatedAt** | `string` | ✅ | Generation timestamp | | **sourceApis** | `string[]` | ✅ | Source API IDs used for generation | +### Nested Shape: `GeneratedApiDocumentation.openApiSpec` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **openapi** | `string` | optional (default: `"3.0.0"`) | OpenAPI specification version | +| **info** | `{ title: string; version: string; description?: string; termsOfService?: string; … }` | ✅ | API metadata | +| **servers** | `{ url: string; description?: string; variables?: Record }[]` | optional (default: `[]`) | API servers | +| **paths** | `Record` | ✅ | API paths and operations | +| **components** | `{ schemas?: Record; responses?: Record; parameters?: Record; examples?: Record; … }` | optional | Reusable components | +| **security** | `Record[]` | optional | Global security requirements | +| **tags** | `{ name: string; description?: string; externalDocs?: object }[]` | optional | Tag definitions | +| **externalDocs** | `{ description?: string; url: string }` | optional | External documentation | + +### Nested Shape: `GeneratedApiDocumentation.testCollections[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Collection name | +| **description** | `string` | optional | Collection description | +| **variables** | `Record` | optional (default: `{}`) | Shared variables | +| **requests** | `{ name: string; description?: string; method: Enum<'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE' \| 'HEAD' \| 'OPTIONS'>; url: string; … }[]` | ✅ | Test requests in this collection | +| **folders** | `{ name: string; description?: string; requests: object[] }[]` | optional | Request folders for organization | + --- @@ -242,6 +369,25 @@ const result = ApiChangelogEntrySchema.parse(data); | **tags** | `{ name: string; description?: string; externalDocs?: object }[]` | optional | Tag definitions | | **externalDocs** | `{ description?: string; url: string }` | optional | External documentation | +### Nested Shape: `OpenApiSpec.info` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **title** | `string` | ✅ | API title | +| **version** | `string` | ✅ | API version | +| **description** | `string` | optional | API description | +| **termsOfService** | `string` | optional | Terms of service URL | +| **contact** | `{ name?: string; url?: string; email?: string }` | optional | | +| **license** | `{ name: string; url?: string }` | optional | | + +### Nested Shape: `OpenApiSpec.servers[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **url** | `string` | ✅ | Server base URL | +| **description** | `string` | optional | Server description | +| **variables** | `Record` | optional | URL template variables | + --- diff --git a/content/docs/references/api/endpoint.mdx b/content/docs/references/api/endpoint.mdx index fa80cfa515..92439d1342 100644 --- a/content/docs/references/api/endpoint.mdx +++ b/content/docs/references/api/endpoint.mdx @@ -51,6 +51,30 @@ const result = ApiEndpointSchema.parse(data); | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `ApiEndpoint.inputMapping[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **source** | `string` | ✅ | Source field/path | +| **target** | `string` | ✅ | Target field/path | +| **transform** | `string` | optional | Transformation function name — NOT EXECUTED in 17.x, and publish REJECTS the key: there is no transformation-function registry anywhere in the platform, so it stays in the frozen vocabulary and is refused rather than parsed and ignored (#5040 E7). A mapping entry moves and renames fields by dot path and nothing more — shape the value where it is produced instead (a flow endpoint whose flow computes it, or a formula field on the object) | + +### Nested Shape: `ApiEndpoint.outputMapping[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **source** | `string` | ✅ | Source field/path | +| **target** | `string` | ✅ | Target field/path | +| **transform** | `string` | optional | Transformation function name — NOT EXECUTED in 17.x, and publish REJECTS the key: there is no transformation-function registry anywhere in the platform, so it stays in the frozen vocabulary and is refused rather than parsed and ignored (#5040 E7). A mapping entry moves and renames fields by dot path and nothing more — shape the value where it is produced instead (a flow endpoint whose flow computes it, or a formula field on the object) | + +### Nested Shape: `ApiEndpoint.rateLimit` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `false`) | Enable rate limiting | +| **windowMs** | `integer` | optional (default: `60000`) | Time window in milliseconds | +| **maxRequests** | `integer` | optional (default: `100`) | Max requests per window | + --- diff --git a/content/docs/references/api/errors.mdx b/content/docs/references/api/errors.mdx index cccfa78b9e..f90d782dd8 100644 --- a/content/docs/references/api/errors.mdx +++ b/content/docs/references/api/errors.mdx @@ -110,6 +110,17 @@ const result = EnhancedApiErrorSchema.parse(data); * `INTEGRATION_ERROR` * `WEBHOOK_DELIVERY_FAILED` +### Nested Shape: `EnhancedApiError.fields[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field path (supports dot notation) | +| **code** | `Enum<'required' \| 'invalid_type' \| 'invalid_shape' \| 'unknown_field' \| …>` | ✅ | Which constraint the value violated (field-level catalog, ADR-0114) | +| **message** | `string` | ✅ | Human-readable error message, rendered in the caller’s locale | +| **label** | `string` | optional | Field display label in the caller’s locale | +| **value** | `any` | optional | The invalid value that was provided | +| **constraint** | `Record` | optional | The constraint that was violated, as discrete values (e.g. `{ maxLength: 512, actual: 3000 }`) | + --- @@ -140,6 +151,27 @@ const result = EnhancedApiErrorSchema.parse(data); | **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>; message: string; userMessage?: string; category?: Enum<'validation' \| 'authentication' \| 'authorization' \| 'not_found' \| 'conflict' \| …>; … }` | ✅ | Error details | | **meta** | `{ timestamp?: string; requestId?: string; traceId?: string }` | optional | Response metadata | +### Nested Shape: `ErrorResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Machine-readable error code | +| **message** | `string` | ✅ | Human-readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934) — see ApiErrorSchema.userMessage. Present only when the producer opted in at throw time; unmarked errors keep the generic consumer substitution (#3821). | +| **category** | `Enum<'validation' \| 'authentication' \| 'authorization' \| 'not_found' \| 'conflict' \| …>` | optional | Error category | +| **httpStatus** | `number` | optional | HTTP status code | +| **retryable** | `boolean` | optional (default: `false`) | Whether the request can be retried | +| **retryStrategy** | `Enum<'no_retry' \| 'retry_immediate' \| 'retry_backoff' \| 'retry_after'>` | optional | Recommended retry strategy | +| **retryAfter** | `number` | optional | Seconds to wait before retrying | +| **details** | `any` | optional | Additional error context | +| **fields** | `{ field: string; code: Enum<'required' \| 'invalid_type' \| 'invalid_shape' \| 'unknown_field' \| …>; message: string; label?: string; … }[]` | optional | One entry per offending value | +| **fieldErrors** | `never` | optional | [REMOVED] `EnhancedApiError.fieldErrors` was renamed to `fields` in @objectstack/spec 17 (ADR-0114 D4, #3977) — the array is unchanged, only the property name. Every producer already emitted `fields`; `fieldErrors` was declared and never emitted, so a reader keying on it was reading a field no server sent. | +| **timestamp** | `string` | optional | When the error occurred | +| **requestId** | `string` | optional | Request ID for tracking | +| **traceId** | `string` | optional | Distributed trace ID | +| **documentation** | `string` | optional | URL to error documentation | +| **helpText** | `string` | optional | Suggested actions to resolve the error | + --- diff --git a/content/docs/references/api/export.mdx b/content/docs/references/api/export.mdx index 907b605fb1..610ae527f8 100644 --- a/content/docs/references/api/export.mdx +++ b/content/docs/references/api/export.mdx @@ -47,6 +47,13 @@ const result = CreateExportJobRequestSchema.parse(data); | **encoding** | `string` | optional (default: `"utf-8"`) | Character encoding for the export file | | **templateId** | `string` | optional | Export template ID for predefined field mappings | +### Nested Shape: `CreateExportJobRequest.sort[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name to sort by | +| **direction** | `Enum<'asc' \| 'desc'>` | optional (default: `"asc"`) | Sort direction | + --- @@ -61,6 +68,28 @@ const result = CreateExportJobRequestSchema.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ jobId: string; status: Enum<'pending' \| 'processing' \| 'completed' \| 'failed' \| 'cancelled' \| 'expired'>; estimatedRecords?: integer; createdAt: string }` | ✅ | | +### Nested Shape: `CreateExportJobResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `CreateExportJobResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **jobId** | `string` | ✅ | Export job ID | +| **status** | `Enum<'pending' \| 'processing' \| 'completed' \| 'failed' \| 'cancelled' \| 'expired'>` | ✅ | Initial job status | +| **estimatedRecords** | `integer` | optional | Estimated total records | +| **createdAt** | `string` | ✅ | Job creation timestamp | + --- @@ -87,6 +116,17 @@ const result = CreateExportJobRequestSchema.parse(data); | **createMissingOptions** | `boolean` | optional (default: `false`) | Keep unmatched select values instead of failing the row | | **skipBlankMatchKey** | `boolean` | optional (default: `false`) | Skip rows whose matchFields are blank (default: upsert creates them, update skips them) | +### Nested Shape: `CreateImportJobRequest.mapping[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **sourceField** | `string` | ✅ | Field name in the source data (import) or object (export) | +| **targetField** | `string` | ✅ | Field name in the target object (import) or file column (export) | +| **targetLabel** | `string` | optional | Display label for the target column (export) | +| **transform** | `Enum<'none' \| 'uppercase' \| 'lowercase' \| 'trim' \| 'date_format' \| 'lookup'>` | optional (default: `"none"`) | Transformation to apply during mapping | +| **defaultValue** | `any` | optional | Default value if source field is null/empty | +| **required** | `boolean` | optional (default: `false`) | Whether this field is required (import validation) | + --- @@ -148,6 +188,17 @@ const result = CreateExportJobRequestSchema.parse(data); | **updatedAt** | `string` | optional | Last update timestamp | | **createdBy** | `string` | optional | User who created the template | +### Nested Shape: `ExportImportTemplate.mappings[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **sourceField** | `string` | ✅ | Field name in the source data (import) or object (export) | +| **targetField** | `string` | ✅ | Field name in the target object (import) or file column (export) | +| **targetLabel** | `string` | optional | Display label for the target column (export) | +| **transform** | `Enum<'none' \| 'uppercase' \| 'lowercase' \| 'trim' \| 'date_format' \| 'lookup'>` | optional (default: `"none"`) | Transformation to apply during mapping | +| **defaultValue** | `any` | optional | Default value if source field is null/empty | +| **required** | `boolean` | optional (default: `false`) | Whether this field is required (import validation) | + --- @@ -162,6 +213,36 @@ const result = CreateExportJobRequestSchema.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ jobId: string; status: Enum<'pending' \| 'processing' \| 'completed' \| 'failed' \| 'cancelled' \| 'expired'>; format: Enum<'csv' \| 'json' \| 'jsonl' \| 'xlsx' \| 'parquet'>; totalRecords?: integer; … }` | ✅ | | +### Nested Shape: `ExportJobProgress.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `ExportJobProgress.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **jobId** | `string` | ✅ | Export job ID | +| **status** | `Enum<'pending' \| 'processing' \| 'completed' \| 'failed' \| 'cancelled' \| 'expired'>` | ✅ | Current job status | +| **format** | `Enum<'csv' \| 'json' \| 'jsonl' \| 'xlsx' \| 'parquet'>` | ✅ | Export format | +| **totalRecords** | `integer` | optional | Total records to export | +| **processedRecords** | `integer` | ✅ | Records processed so far | +| **percentComplete** | `number` | ✅ | Export progress percentage | +| **fileSize** | `integer` | optional | Current file size in bytes | +| **downloadUrl** | `string` | optional | Presigned download URL (available when status is "completed") | +| **downloadExpiresAt** | `string` | optional | Download URL expiration timestamp | +| **error** | `{ code: string; message: string }` | optional | Error details if job failed | +| **startedAt** | `string` | optional | Processing start timestamp | +| **completedAt** | `string` | optional | Completion timestamp | + --- @@ -236,6 +317,31 @@ const result = CreateExportJobRequestSchema.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ jobId: string; downloadUrl: string; fileName: string; fileSize: integer; … }` | ✅ | | +### Nested Shape: `GetExportJobDownloadResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `GetExportJobDownloadResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **jobId** | `string` | ✅ | Export job ID | +| **downloadUrl** | `string` | ✅ | Presigned download URL | +| **fileName** | `string` | ✅ | Suggested file name | +| **fileSize** | `integer` | ✅ | File size in bytes | +| **format** | `Enum<'csv' \| 'json' \| 'jsonl' \| 'xlsx' \| 'parquet'>` | ✅ | Export file format | +| **expiresAt** | `string` | ✅ | Download URL expiration timestamp | +| **checksum** | `string` | optional | File checksum (SHA-256) | + --- @@ -294,6 +400,18 @@ const result = CreateExportJobRequestSchema.parse(data); | **results** | `{ row: integer; ok: boolean; action: Enum<'created' \| 'updated' \| 'skipped' \| 'failed'>; id?: string; … }[]` | ✅ | Capped sample of per-row outcomes (failures first) | | **resultsTruncated** | `boolean` | ✅ | Whether `results` is a capped sample of a larger set | +### Nested Shape: `ImportJobResults.results[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **row** | `integer` | ✅ | 1-based row number in the source data | +| **ok** | `boolean` | ✅ | Whether the row succeeded | +| **action** | `Enum<'created' \| 'updated' \| 'skipped' \| 'failed'>` | ✅ | What happened to the row | +| **id** | `string` | optional | Record id (created/updated rows) | +| **field** | `string` | optional | Field that caused a coercion/validation error | +| **code** | `string` | optional | Error code (failed rows) | +| **error** | `string` | optional | Human-readable error message (failed rows) | + --- @@ -377,6 +495,17 @@ Type: `{ sourceField: string; targetField: string; targetLabel?: string; transfo | **createMissingOptions** | `boolean` | optional (default: `false`) | Keep unmatched select values instead of failing the row | | **skipBlankMatchKey** | `boolean` | optional (default: `false`) | Skip rows whose matchFields are blank (default: upsert creates them, update skips them) | +### Nested Shape: `ImportRequest.mapping[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **sourceField** | `string` | ✅ | Field name in the source data (import) or object (export) | +| **targetField** | `string` | ✅ | Field name in the target object (import) or file column (export) | +| **targetLabel** | `string` | optional | Display label for the target column (export) | +| **transform** | `Enum<'none' \| 'uppercase' \| 'lowercase' \| 'trim' \| 'date_format' \| 'lookup'>` | optional (default: `"none"`) | Transformation to apply during mapping | +| **defaultValue** | `any` | optional | Default value if source field is null/empty | +| **required** | `boolean` | optional (default: `false`) | Whether this field is required (import validation) | + --- @@ -397,6 +526,18 @@ Type: `{ sourceField: string; targetField: string; targetLabel?: string; transfo | **skipped** | `integer` | ✅ | Rows skipped (no match in update mode, etc.) | | **results** | `{ row: integer; ok: boolean; action: Enum<'created' \| 'updated' \| 'skipped' \| 'failed'>; id?: string; … }[]` | ✅ | Per-row outcomes | +### Nested Shape: `ImportResponse.results[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **row** | `integer` | ✅ | 1-based row number in the source data | +| **ok** | `boolean` | ✅ | Whether the row succeeded | +| **action** | `Enum<'created' \| 'updated' \| 'skipped' \| 'failed'>` | ✅ | What happened to the row | +| **id** | `string` | optional | Record id (created/updated rows) | +| **field** | `string` | optional | Field that caused a coercion/validation error | +| **code** | `string` | optional | Error code (failed rows) | +| **error** | `string` | optional | Human-readable error message (failed rows) | + --- @@ -430,6 +571,13 @@ Type: `{ sourceField: string; targetField: string; targetLabel?: string; transfo | **dateFormat** | `string` | optional | Expected date format in import data (e.g., "YYYY-MM-DD") | | **nullValues** | `string[]` | optional | Strings to treat as null (e.g., ["", "N/A", "null"]) | +### Nested Shape: `ImportValidationConfig.deduplication` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **strategy** | `Enum<'skip' \| 'update' \| 'create_new' \| 'fail'>` | optional (default: `"skip"`) | How to handle duplicate records | +| **matchFields** | `string[]` | ✅ | Fields used to identify duplicates (e.g., "email", "external_id") | + --- @@ -455,6 +603,30 @@ Type: `{ sourceField: string; targetField: string; targetLabel?: string; transfo | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ totalRecords: integer; validRecords: integer; invalidRecords: integer; duplicateRecords: integer; … }` | ✅ | | +### Nested Shape: `ImportValidationResult.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `ImportValidationResult.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **totalRecords** | `integer` | ✅ | Total records in import file | +| **validRecords** | `integer` | ✅ | Records that passed validation | +| **invalidRecords** | `integer` | ✅ | Records that failed validation | +| **duplicateRecords** | `integer` | ✅ | Duplicate records detected | +| **errors** | `{ row: integer; field?: string; code: string; message: string }[]` | ✅ | List of validation errors | +| **preview** | `Record[]` | optional | Preview of first N valid records (for dry_run mode) | + --- @@ -494,6 +666,27 @@ Type: `{ sourceField: string; targetField: string; targetLabel?: string; transfo | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ jobs: object[]; nextCursor?: string; hasMore: boolean }` | ✅ | | +### Nested Shape: `ListExportJobsResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `ListExportJobsResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **jobs** | `{ jobId: string; object: string; status: Enum<'pending' \| 'processing' \| 'completed' \| 'failed' \| 'cancelled' \| 'expired'>; format: Enum<'csv' \| 'json' \| 'jsonl' \| 'xlsx' \| 'parquet'>; … }[]` | ✅ | List of export jobs | +| **nextCursor** | `string` | optional | Cursor for the next page | +| **hasMore** | `boolean` | ✅ | Whether more jobs are available | + --- @@ -519,6 +712,24 @@ Type: `{ sourceField: string; targetField: string; targetLabel?: string; transfo | :--- | :--- | :--- | :--- | | **jobs** | `{ jobId: string; object: string; status: Enum<'pending' \| 'running' \| 'succeeded' \| 'failed' \| 'cancelled'>; total: integer; … }[]` | ✅ | Import jobs, newest first | +### Nested Shape: `ListImportJobsResponse.jobs[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **jobId** | `string` | ✅ | Import job id | +| **object** | `string` | ✅ | Target object name | +| **status** | `Enum<'pending' \| 'running' \| 'succeeded' \| 'failed' \| 'cancelled'>` | ✅ | Job status | +| **total** | `integer` | ✅ | Total rows | +| **processed** | `integer` | ✅ | Rows processed | +| **created** | `integer` | ✅ | Rows created | +| **updated** | `integer` | ✅ | Rows updated | +| **skipped** | `integer` | ✅ | Rows skipped | +| **errors** | `integer` | ✅ | Rows failed | +| **createdAt** | `string` | ✅ | Job creation timestamp (ISO 8601) | +| **completedAt** | `string` | optional | Completion timestamp (ISO 8601) | +| **undoable** | `boolean` | ✅ | Whether this job can still be logically rolled back | +| **revertedAt** | `string` | optional | When the job was undone / rolled back (ISO 8601) | + --- @@ -538,6 +749,22 @@ Type: `{ sourceField: string; targetField: string; targetLabel?: string; transfo | **schedule** | `{ cronExpression: string \| object; timezone?: string }` | ✅ | Schedule timing configuration | | **delivery** | `{ method: Enum<'email' \| 'storage' \| 'webhook'>; recipients?: string[]; storagePath?: string; webhookUrl?: string }` | ✅ | Export delivery configuration | +### Nested Shape: `ScheduleExportRequest.schedule` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **cronExpression** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | ✅ | Cron expression for schedule | +| **timezone** | `string` | optional (default: `"UTC"`) | IANA timezone | + +### Nested Shape: `ScheduleExportRequest.delivery` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **method** | `Enum<'email' \| 'storage' \| 'webhook'>` | ✅ | How to deliver the export file | +| **recipients** | `string[]` | optional | Email recipients (for email delivery) | +| **storagePath** | `string` | optional | Storage path (for storage delivery) | +| **webhookUrl** | `string` | optional | Webhook URL (for webhook delivery) | + --- @@ -552,6 +779,29 @@ Type: `{ sourceField: string; targetField: string; targetLabel?: string; transfo | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ id: string; name: string; enabled: boolean; nextRunAt?: string; … }` | ✅ | | +### Nested Shape: `ScheduleExportResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `ScheduleExportResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Scheduled export ID | +| **name** | `string` | ✅ | Schedule name | +| **enabled** | `boolean` | ✅ | Whether the schedule is active | +| **nextRunAt** | `string` | optional | Next scheduled execution | +| **createdAt** | `string` | ✅ | Creation timestamp | + --- @@ -577,6 +827,22 @@ Type: `{ sourceField: string; targetField: string; targetLabel?: string; transfo | **createdAt** | `string` | optional | Creation timestamp | | **createdBy** | `string` | optional | User who created the schedule | +### Nested Shape: `ScheduledExport.schedule` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **cronExpression** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | ✅ | Cron expression for schedule | +| **timezone** | `string` | optional (default: `"UTC"`) | IANA timezone | + +### Nested Shape: `ScheduledExport.delivery` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **method** | `Enum<'email' \| 'storage' \| 'webhook'>` | ✅ | How to deliver the export file | +| **recipients** | `string[]` | optional | Email recipients (for email delivery) | +| **storagePath** | `string` | optional | Storage path (for storage delivery) | +| **webhookUrl** | `string` | optional | Webhook URL (for webhook delivery) | + --- diff --git a/content/docs/references/api/http-cache.mdx b/content/docs/references/api/http-cache.mdx index 6799c0c0cb..288e6e51bc 100644 --- a/content/docs/references/api/http-cache.mdx +++ b/content/docs/references/api/http-cache.mdx @@ -143,6 +143,15 @@ const result = CacheControlSchema.parse(data); | **ifModifiedSince** | `string` | optional | Timestamp for conditional request (If-Modified-Since header) | | **cacheControl** | `{ directives: Enum<'public' \| 'private' \| 'no-cache' \| 'no-store' \| 'must-revalidate' \| 'max-age'>[]; maxAge?: number; staleWhileRevalidate?: number; staleIfError?: number }` | optional | Client cache control preferences | +### Nested Shape: `MetadataCacheRequest.cacheControl` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **directives** | `Enum<'public' \| 'private' \| 'no-cache' \| 'no-store' \| 'must-revalidate' \| 'max-age'>[]` | ✅ | Cache control directives | +| **maxAge** | `number` | optional | Maximum cache age in seconds | +| **staleWhileRevalidate** | `number` | optional | Allow serving stale content while revalidating (seconds) | +| **staleIfError** | `number` | optional | Allow serving stale content on error (seconds) | + --- @@ -159,6 +168,22 @@ const result = CacheControlSchema.parse(data); | **notModified** | `boolean` | optional (default: `false`) | True if resource has not been modified (304 response) | | **version** | `string` | optional | Metadata version identifier | +### Nested Shape: `MetadataCacheResponse.etag` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **value** | `string` | ✅ | ETag value (hash or version identifier) | +| **weak** | `boolean` | optional (default: `false`) | Whether this is a weak ETag | + +### Nested Shape: `MetadataCacheResponse.cacheControl` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **directives** | `Enum<'public' \| 'private' \| 'no-cache' \| 'no-store' \| 'must-revalidate' \| 'max-age'>[]` | ✅ | Cache control directives | +| **maxAge** | `number` | optional | Maximum cache age in seconds | +| **staleWhileRevalidate** | `number` | optional | Allow serving stale content while revalidating (seconds) | +| **staleIfError** | `number` | optional | Allow serving stale content on error (seconds) | + --- diff --git a/content/docs/references/api/metadata.mdx b/content/docs/references/api/metadata.mdx index afc7c119c3..4e211f0db1 100644 --- a/content/docs/references/api/metadata.mdx +++ b/content/docs/references/api/metadata.mdx @@ -55,6 +55,54 @@ const result = AppDefinitionResponseSchema.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ name: string; label: string \| Record; description?: string \| Record; icon?: string; … }` | ✅ | Full App Configuration | +### Nested Shape: `AppDefinitionResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `AppDefinitionResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | App unique machine name (lowercase snake_case) | +| **label** | `string \| Record` | ✅ | App display label | +| **version** | `never` | optional | [REMOVED] `App.version` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — no consumer in framework or objectui). An app is versioned by its owning package: use `manifest.version`. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **description** | `string \| Record` | optional | App description | +| **icon** | `string` | optional | App icon used in the App Launcher | +| **branding** | `{ primaryColor?: string; accentColor?: string; logo?: string; favicon?: string }` | optional | App-specific branding | +| **active** | `boolean` | optional (default: `true`) | Whether the app is enabled | +| **isDefault** | `boolean` | optional (default: `false`) | Is default app | +| **hidden** | `boolean` | optional | Hide from the App Switcher; the shell surfaces hidden apps via the avatar menu instead (navigation only — never an access gate) | +| **_unpublished** | `boolean` | optional | Machine-managed publish gate (ADR-0045 §3) — true = unpublished, externally unobservable. Written by AI materialization, cleared by publish-drafts. Never authored. | +| **navigation** | `({ id: string; label: string \| Record; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| … +7 more)[]` | optional | Full navigation tree for the app sidebar | +| **areas** | `{ id: string; label: string \| Record; icon?: string; description?: string \| Record; … }[]` | optional | Navigation areas for partitioning navigation by business domain | +| **contextSelectors** | `{ id: string; label: string \| Record; icon?: string; optionsSource: object; … }[]` | optional | App-level scope dropdowns whose value is injected into nav items as `{}` template vars | +| **homePageId** | `never` | optional | [REMOVED] `app.homePageId` was removed in @objectstack/spec 17.0.0 (#4667, #4709, ADR-0049). objectui's console did read it before v17 (`resolveLandingRoute`), so this key had a consumer — it was retired because the capability is better expressed on the navigation item itself than as an ID cross-reference that silently falls back when it dangles. An app's landing page IS its first navigation item (by `order`), and the root landing follows `isDefault` routing. Delete the key; to change where an app opens, reorder `navigation` so the intended entry is first, and set `isDefault` on the app that should own the root landing. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **requiredPermissions** | `string[]` | optional | Permissions required to access this app | +| **objects** | `never` | optional | [REMOVED] `App.objects` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — never read; the spec itself labelled it "config file convenience"). Objects belong to the stack (`defineStack({ objects })`); an app reaches them through its navigation items. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **apis** | `never` | optional | [REMOVED] `App.apis` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — never read). Delete the key and declare the endpoint one level up, on the STACK: `defineStack({ apis })`. That surface EXECUTES from protocol 17 (#5040). Between #4936 and the executor landing it was refused wholesale — nothing mounted a declared path, so every key including `authRequired` parsed and gated nothing — and that blanket refusal is now narrowed to five per-endpoint publish gates (namespace, supported target, mapping, policy, uniqueness): an endpoint that passes them is mounted and serves traffic as soon as the stack is published. Two things to get right when you move it: the path must sit inside your own carve-out, `/api/v1/apps//` with an explicit `manifest.namespace` (ADR-0121 D1/D2), and `authRequired` defaults to `true` — an explicit `false` is the only thing that opens anonymous access, and ADR-0121 D6 then requires an armed `rateLimit: { enabled: true, windowMs, maxRequests }`. Read the `declarative-apis-endpoints-live` entry of the protocol upgrade guide first; it is a security review, not a rename. A route that genuinely needs handler CODE is mounted imperatively instead: resolve the `http.server` service from your plugin context and register the route on `kernel:ready` (NOT the manifest `contributes.routes` key — nothing reads it, so an entry there parses cleanly and serves nothing). Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **sharing** | `never` | optional | [REMOVED] `App.sharing` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit / ADR-0049 enforce-or-remove) — no public-app route ever read it, so it declared sharing that did not exist. Public access is granted per FORM VIEW (`FormView.sharing`, the public-data-collection surface). Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **embed** | `never` | optional | [REMOVED] `App.embed` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit / ADR-0049) — no iframe route ever read it. Embedding is a per-form-view surface (`FormView.sharing`), not an app-level switch. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **mobileNavigation** | `never` | optional | [REMOVED] `App.mobileNavigation` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — fully unimplemented; no renderer, including packages/mobile, ever read it). Delete the key; the block returns if/when a real mobile navigation ships. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **defaultAgent** | `string` | optional | Platform agent bound to this app's ambient chat ('ask' is the implicit default; 'build' for authoring surfaces) — ADR-0063 §1 | +| **aria** | `never` | optional | [REMOVED] `App.aria` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — no renderer read app-level ARIA attributes). Declare `aria` on the page component that renders the DOM node instead (`page.components[].aria`; `page.aria` and the list view `aria` are live too). Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this app. | +| **_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. | + --- @@ -69,6 +117,19 @@ const result = AppDefinitionResponseSchema.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ name: string; label: string; icon?: string; description?: string }[]` | ✅ | List of available concepts (Objects, Apps, Flows) | +### Nested Shape: `ConceptListResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + --- @@ -82,6 +143,14 @@ const result = AppDefinitionResponseSchema.parse(data); | **continueOnError** | `boolean` | optional (default: `false`) | Continue on individual failure | | **validate** | `boolean` | optional (default: `true`) | Validate before registering | +### Nested Shape: `MetadataBulkRegisterRequest.items[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `string` | ✅ | Metadata type | +| **name** | `string` | ✅ | Item name | +| **data** | `Record` | ✅ | Metadata payload | + --- @@ -96,6 +165,28 @@ const result = AppDefinitionResponseSchema.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ total: integer; succeeded: integer; failed: integer; errors?: object[] }` | ✅ | Bulk operation result | +### Nested Shape: `MetadataBulkResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `MetadataBulkResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **total** | `integer` | ✅ | Total items processed | +| **succeeded** | `integer` | ✅ | Successfully processed | +| **failed** | `integer` | ✅ | Failed items | +| **errors** | `{ type: string; name: string; error: string }[]` | optional | Per-item errors | + --- @@ -107,6 +198,13 @@ const result = AppDefinitionResponseSchema.parse(data); | :--- | :--- | :--- | :--- | | **items** | `{ type: string; name: string }[]` | ✅ | Items to unregister | +### Nested Shape: `MetadataBulkUnregisterRequest.items[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `string` | ✅ | Metadata type | +| **name** | `string` | ✅ | Item name | + --- @@ -121,6 +219,26 @@ const result = AppDefinitionResponseSchema.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ type: string; name: string }` | ✅ | | +### Nested Shape: `MetadataDeleteResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `MetadataDeleteResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `string` | ✅ | Metadata type | +| **name** | `string` | ✅ | Deleted item name | + --- @@ -135,6 +253,29 @@ const result = AppDefinitionResponseSchema.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ sourceType: string; sourceName: string; targetType: string; targetName: string; … }[]` | ✅ | Items this item depends on | +### Nested Shape: `MetadataDependenciesResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `MetadataDependenciesResponse.data[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **sourceType** | `string` | ✅ | Dependent metadata type | +| **sourceName** | `string` | ✅ | Dependent metadata name | +| **targetType** | `string` | ✅ | Referenced metadata type | +| **targetName** | `string` | ✅ | Referenced metadata name | +| **kind** | `Enum<'reference' \| 'extends' \| 'includes' \| 'triggers'>` | ✅ | How the dependency is formed | + --- @@ -149,6 +290,29 @@ const result = AppDefinitionResponseSchema.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ sourceType: string; sourceName: string; targetType: string; targetName: string; … }[]` | ✅ | Items that depend on this item | +### Nested Shape: `MetadataDependentsResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `MetadataDependentsResponse.data[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **sourceType** | `string` | ✅ | Dependent metadata type | +| **sourceName** | `string` | ✅ | Dependent metadata name | +| **targetType** | `string` | ✅ | Referenced metadata type | +| **targetName** | `string` | ✅ | Referenced metadata name | +| **kind** | `Enum<'reference' \| 'extends' \| 'includes' \| 'triggers'>` | ✅ | How the dependency is formed | + --- @@ -163,6 +327,19 @@ const result = AppDefinitionResponseSchema.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `Record` | optional | Effective metadata with all overlays applied | +### Nested Shape: `MetadataEffectiveResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + --- @@ -177,6 +354,25 @@ const result = AppDefinitionResponseSchema.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ exists: boolean }` | ✅ | | +### Nested Shape: `MetadataExistsResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `MetadataExistsResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **exists** | `boolean` | ✅ | Whether the item exists | + --- @@ -204,6 +400,19 @@ const result = AppDefinitionResponseSchema.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `any` | ✅ | Exported metadata bundle | +### Nested Shape: `MetadataExportResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + --- @@ -232,6 +441,19 @@ const result = AppDefinitionResponseSchema.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ total: integer; imported: integer; skipped: integer; failed: integer; … }` | ✅ | Import result | +### Nested Shape: `MetadataImportResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + --- @@ -246,6 +468,27 @@ const result = AppDefinitionResponseSchema.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ type: string; name: string; definition: Record }` | ✅ | Metadata item | +### Nested Shape: `MetadataItemResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `MetadataItemResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `string` | ✅ | Metadata type | +| **name** | `string` | ✅ | Item name | +| **definition** | `Record` | ✅ | Metadata definition payload | + --- @@ -260,6 +503,19 @@ const result = AppDefinitionResponseSchema.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `Record[]` | ✅ | Array of metadata definitions | +### Nested Shape: `MetadataListResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + --- @@ -274,6 +530,19 @@ const result = AppDefinitionResponseSchema.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `string[]` | ✅ | Array of metadata item names | +### Nested Shape: `MetadataNamesResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + --- @@ -288,6 +557,39 @@ const result = AppDefinitionResponseSchema.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ id: string; baseType: string; baseName: string; packageId?: string; … }` | optional | Overlay definition, undefined if none | +### Nested Shape: `MetadataOverlayResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `MetadataOverlayResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Overlay record ID (UUID) | +| **baseType** | `string` | ✅ | Metadata type being customized | +| **baseName** | `string` | ✅ | Metadata name being customized | +| **packageId** | `string` | optional | Package ID that delivered the base metadata | +| **packageVersion** | `string` | optional | Package version when overlay was created | +| **scope** | `Enum<'platform' \| 'user'>` | optional (default: `"platform"`) | Customization scope (platform=admin, user=personal) | +| **tenantId** | `string` | optional | Tenant identifier | +| **owner** | `string` | optional | Owner user ID for user-scope overlays | +| **patch** | `Record` | ✅ | JSON Merge Patch payload (changed fields only) | +| **changes** | `{ path: string; originalValue?: any; currentValue: any; changedBy?: string; … }[]` | optional | Field-level change tracking for conflict detection | +| **active** | `boolean` | optional (default: `true`) | Whether this overlay is active | +| **createdAt** | `string` | optional | | +| **createdBy** | `string` | optional | | +| **updatedAt** | `string` | optional | | +| **updatedBy** | `string` | optional | | + --- @@ -315,6 +617,16 @@ Overlay to save | **updatedAt** | `string` | optional | | | **updatedBy** | `string` | optional | | +### Nested Shape: `MetadataOverlaySaveRequest.changes[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **path** | `string` | ✅ | JSON path to the changed field | +| **originalValue** | `any` | optional | Original value from the package | +| **currentValue** | `any` | ✅ | Current customized value | +| **changedBy** | `string` | optional | User or admin who made this change | +| **changedAt** | `string` | optional | Timestamp of the change | + --- @@ -352,6 +664,28 @@ Metadata query with filtering, sorting, and pagination | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ items: object[]; total: integer; page: integer; pageSize: integer }` | ✅ | Paginated query result | +### Nested Shape: `MetadataQueryResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `MetadataQueryResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **items** | `{ type: string; name: string; namespace?: string; label?: string; … }[]` | ✅ | Matched metadata items | +| **total** | `integer` | ✅ | Total matching items | +| **page** | `integer` | ✅ | Current page | +| **pageSize** | `integer` | ✅ | Page size | + --- @@ -410,6 +744,31 @@ Metadata query with filtering, sorting, and pagination | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ type: string; label: string; description?: string; filePatterns: string[]; … }` | optional | Type info | +### Nested Shape: `MetadataTypeInfoResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `MetadataTypeInfoResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `string` | ✅ | Metadata type identifier | +| **label** | `string` | ✅ | Display label | +| **description** | `string` | optional | Description | +| **filePatterns** | `string[]` | ✅ | File glob patterns | +| **supportsOverlay** | `boolean` | ✅ | Overlay support | +| **domain** | `string` | ✅ | Protocol domain | +| **actions** | `{ name: string; label: string \| Record; description?: string \| Record; objectName?: string; … }[]` | optional | Declarative type-level actions (buttons) the metadata-admin UI renders for this type | + --- @@ -424,6 +783,19 @@ Metadata query with filtering, sorting, and pagination | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `string[]` | ✅ | Registered metadata type identifiers | +### Nested Shape: `MetadataTypesResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + --- @@ -450,6 +822,27 @@ Metadata query with filtering, sorting, and pagination | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ valid: boolean; errors?: object[]; warnings?: object[] }` | ✅ | Validation result | +### Nested Shape: `MetadataValidateResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `MetadataValidateResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **valid** | `boolean` | ✅ | Whether the metadata is valid | +| **errors** | `{ path: string; message: string; code?: string }[]` | optional | Validation errors | +| **warnings** | `{ path: string; message: string }[]` | optional | Validation warnings | + --- @@ -464,6 +857,67 @@ Metadata query with filtering, sorting, and pagination | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ name: string; label?: string; pluralLabel?: string; description?: string; … }` | ✅ | Full Object Schema | +### Nested Shape: `ObjectDefinitionResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `ObjectDefinitionResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Machine unique key (snake_case). Immutable. | +| **label** | `string` | optional | Human readable singular label (e.g. "Account") | +| **pluralLabel** | `string` | optional | Human readable plural label (e.g. "Accounts") | +| **description** | `string` | optional | Developer documentation / description | +| **icon** | `string` | optional | Icon name (Lucide/Material) for UI representation | +| **isSystem** | `boolean` | optional (default: `false`) | Is system object (protected from deletion; defaults its org-wide sharing to public when no sharingModel is set — plugin-sharing) | +| **managedBy** | `Enum<'platform' \| 'config' \| 'system-data' \| 'engine-owned' \| 'append-only' \| 'better-auth'>` | optional | Lifecycle bucket — platform (user CRUD) \| config (admin authored) \| system-data (platform-defined schema, admin/user-writable data) \| engine-owned (engine owns the lifecycle, no user writes) \| append-only (audit) \| better-auth (identity). UI clients honour the resolved affordance matrix. | +| **ownership** | `Enum<'user' \| 'business_unit' \| 'org' \| 'none'>` | optional | Record-ownership model: user (default — injects reassignable owner_id plus owning_business_unit_id) \| business_unit (unit-owned: owning_business_unit_id only, no owner_id) \| org \| none (no per-record owner, neither anchor). Distinct from the package own/extend contribution kind. | +| **userActions** | `{ create?: boolean \| object; import?: boolean \| object; edit?: boolean \| object; delete?: boolean \| object; … }` | optional | Per-object override of the resolved CRUD affordance matrix. | +| **systemFields** | `false \| { tenant?: boolean; audit?: boolean }` | optional | Opt out of, or selectively disable, registry-level system-field auto-injection. | +| **datasource** | `string` | optional (default: `"default"`) | Target Datasource ID. "default" is the primary DB. | +| **external** | `{ remoteName?: string; remoteSchema?: string; writable?: boolean; columnMap?: Record; … }` | optional | Remote table binding for federated (external) objects. | +| **fields** | `Record; description?: string; … }>` | ✅ | Field definitions map. Keys must be snake_case identifiers. | +| **indexes** | `{ name?: string; fields: string[]; unique?: boolean \| 'global' \| 'organization' }[]` | optional | Database performance indexes | +| **fieldGroups** | `{ key: string; label: string; icon?: string; description?: string; … }[]` | optional | Ordered list of field groups (array order = display order). See ObjectFieldGroupSchema. | +| **tenancy** | `{ enabled: boolean; tenantField?: string; organizationField?: string }` | optional | Multi-tenancy configuration for SaaS applications | +| **access** | `{ default?: Enum<'public' \| 'private'> }` | optional | [ADR-0066 D2] Object exposure posture (public-by-default vs private secure-by-default). | +| **requiredPermissions** | `string[] \| { read?: string[]; create?: string[]; update?: string[]; delete?: string[] }` | optional | [ADR-0066 D3/⑤] Capabilities required to access this object (AND-gate) — `string[]` gates all CRUD, or a `{read,create,update,delete}` map gates per operation. | +| **lifecycle** | `{ class: Enum<'record' \| 'audit' \| 'telemetry' \| 'transient' \| 'event'>; retention?: object; ttl?: object; storage?: object; … }` | optional | Data lifecycle contract (ADR-0057): class + retention/ttl/rotation/archive policies enforced by the platform LifecycleService. | +| **fileAccessDelegate** | `string` | optional | Kernel service that authorizes downloads of files owned by this object's media fields, instead of testing whether the caller can read the owning row. For objects whose access is mediated by a service (e.g. sys_approval_action → approvals). Fails closed. | +| **validations** | `any[]` | optional | Object-level validation rules | +| **activityMilestones** | `{ field: string; value: string; summary: string; type?: string }[]` | optional | Declarative semantic activity milestones — emit a templated timeline row when a field transitions into a value, no hook code (ADR-0052 §5b.2). | +| **nameField** | `string` | optional | [ADR-0079] Canonical primary title field — the stored field used as the record display name (e.g. "name", "title"). | +| **displayNameField** | `string` | optional | [DEPRECATED → nameField] Field to use as the record display name (e.g., "name", "title"). Accepted as an alias for nameField. | +| **titleFormat** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | [DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField. | +| **highlightFields** | `string[]` | optional | [ADR-0085] Ordered most-important fields; first entry wins where only one fits. Drives default columns, cards, previews, detail highlight strip. Renamed from compactLayout. | +| **stageField** | `string \| false` | optional | [ADR-0085] Lifecycle stage field (linear/ordered), or false to declare the status field non-linear and suppress stage heuristics. Absent = heuristic detection allowed. | +| **editMode** | `Enum<'modal' \| 'page'>` | optional | Edit-interaction intent for records of this object: 'modal' opens the edit form as a dialog over the current view; 'page' navigates to a dedicated full-page edit route. Absent = the renderer picks its own default (objectui defaults to modal). Cross-renderer intent, not pixel styling (#11408, #10144 family). | +| **listViews** | `Record; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: object \| … +3 more; … }>` | optional | Built-in named list views (segmented tabs) shipped with the object schema — "views" mode, dropdown userFilters allowed, no page-only tabs (ADR-0047) | +| **searchableFields** | `string[]` | optional | Fields the `$search` query matches against (ADR-0061). Canonical default for the record picker, list quick-search and global search; views may narrow it. When unset, search auto-defaults to the name/title field plus short-text fields. Entries must name a STORED column: a virtual `formula` field is computed on read and materializes no column, so searching it can never match and it is refused (#6674) — mirror the value onto a stored text field and declare that. | +| **enable** | `{ trackHistory?: boolean; searchable?: boolean; apiEnabled?: boolean; apiMethods?: Enum<'get' \| 'list' \| 'create' \| 'update' \| 'delete' \| 'bulk'>[]; … }` | optional | Enabled system features modules | +| **sharingModel** | `Enum<'private' \| 'public_read' \| 'public_read_write' \| 'controlled_by_parent'>` | optional | Org-Wide Default record visibility (OWD) for INTERNAL users. Canonical four only (legacy aliases removed, ADR-0090 D4): private (owner-only) \| public_read (everyone reads, owner writes) \| public_read_write (everyone reads+writes) \| controlled_by_parent (derived from the master record). A CUSTOM object that omits this resolves to private at runtime (ADR-0090 D1). | +| **externalSharingModel** | `Enum<'private' \| 'public_read' \| 'public_read_write' \| 'controlled_by_parent'>` | optional | [ADR-0090 D11] OWD for external (portal/partner) principals. Defaults to private; must be <= sharingModel in openness. | +| **publicSharing** | `{ enabled?: boolean; allowedAudiences?: Enum<'public' \| 'link_only' \| 'signed_in' \| 'email'>[]; allowedPermissions?: Enum<'view' \| 'comment' \| 'edit'>[]; maxExpiryDays?: integer; … }` | optional | Public share-link policy (Notion/Figma-style link sharing) | +| **actions** | `{ name: string; label: string \| Record; description?: string \| Record; objectName?: string; … }[]` | optional | Actions associated with this object (auto-populated from top-level actions via objectName) | +| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this object. | +| **_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. | + --- diff --git a/content/docs/references/api/odata.mdx b/content/docs/references/api/odata.mdx index 7107380cb9..0457b1843d 100644 --- a/content/docs/references/api/odata.mdx +++ b/content/docs/references/api/odata.mdx @@ -93,6 +93,14 @@ const result = ODataConfigSchema.parse(data); | **path** | `string` | optional (default: `"/odata"`) | OData endpoint path | | **metadata** | `{ namespace: string; entityTypes: object[]; entitySets: object[] }` | optional | OData metadata configuration | +### Nested Shape: `ODataConfig.metadata` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **namespace** | `string` | ✅ | Service namespace | +| **entityTypes** | `{ name: string; key: string[]; properties: object[]; navigationProperties?: object[] }[]` | ✅ | Entity types | +| **entitySets** | `{ name: string; entityType: string }[]` | ✅ | Entity sets | + --- @@ -104,6 +112,16 @@ const result = ODataConfigSchema.parse(data); | :--- | :--- | :--- | :--- | | **error** | `{ code: string; message: string; target?: string; details?: object[]; … }` | ✅ | | +### Nested Shape: `ODataError.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `string` | ✅ | Error code | +| **message** | `string` | ✅ | Error message | +| **target** | `string` | optional | Error target | +| **details** | `{ code: string; message: string; target?: string }[]` | optional | Error details | +| **innererror** | `Record` | optional | Inner error details | + --- @@ -153,6 +171,22 @@ const result = ODataConfigSchema.parse(data); | **entityTypes** | `{ name: string; key: string[]; properties: object[]; navigationProperties?: object[] }[]` | ✅ | Entity types | | **entitySets** | `{ name: string; entityType: string }[]` | ✅ | Entity sets | +### Nested Shape: `ODataMetadata.entityTypes[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Entity type name | +| **key** | `string[]` | ✅ | Key fields | +| **properties** | `{ name: string; type: string; nullable: boolean }[]` | ✅ | | +| **navigationProperties** | `{ name: string; type: string; partner?: string }[]` | optional | | + +### Nested Shape: `ODataMetadata.entitySets[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Entity set name | +| **entityType** | `string` | ✅ | Entity type | + --- diff --git a/content/docs/references/api/package-api.mdx b/content/docs/references/api/package-api.mdx index cf977b7923..3e83d72e60 100644 --- a/content/docs/references/api/package-api.mdx +++ b/content/docs/references/api/package-api.mdx @@ -61,6 +61,36 @@ Get installed package response | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Installed package details | +### Nested Shape: `GetInstalledPackageResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `GetInstalledPackageResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Full package manifest | +| **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | +| **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | +| **installedAt** | `string` | optional | Installation timestamp | +| **updatedAt** | `string` | optional | Last update timestamp | +| **installedVersion** | `string` | optional | Currently installed version for quick access | +| **previousVersion** | `string` | optional | Version before the last upgrade | +| **statusChangedAt** | `string` | optional | Status change timestamp | +| **errorMessage** | `string` | optional | Error message when status is error | +| **settings** | `Record` | optional | User-provided configuration settings | +| **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history | +| **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package | + --- @@ -93,6 +123,28 @@ List installed packages response | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ packages: object[]; total?: integer; nextCursor?: string; hasMore: boolean }` | ✅ | | +### Nested Shape: `ListInstalledPackagesResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `ListInstalledPackagesResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **packages** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }[]` | ✅ | Installed packages | +| **total** | `integer` | optional | Total matching packages | +| **nextCursor** | `string` | optional | Cursor for the next page | +| **hasMore** | `boolean` | ✅ | Whether more packages are available | + --- @@ -131,6 +183,45 @@ Install package request | **platformVersion** | `string` | optional | Current platform version for compatibility verification | | **artifactRef** | `{ url: string; sha256: string; size: integer; format?: Enum<'tgz' \| 'zip'>; … }` | optional | Artifact reference for marketplace installation | +### Nested Shape: `PackageInstallRequest.manifest` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique package identifier (reverse domain style) | +| **namespace** | `string` | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") | +| **defaultDatasource** | `string` | optional (default: `"default"`) | Default datasource for all objects in this package | +| **version** | `string` | ✅ | Package version (semantic versioning) | +| **type** | `Enum<'plugin' \| 'ui' \| 'driver' \| 'server' \| 'app' \| 'theme' \| 'agent' \| 'objectql' \| …>` | ✅ | Type of package | +| **scope** | `Enum<'cloud' \| 'system' \| 'project'>` | optional (default: `"project"`) | Deployment scope: cloud \| system \| project | +| **name** | `string` | ✅ | Human-readable package name | +| **description** | `string` | optional | Package description | +| **permissions** | `string[] \| { services?: string[]; hooks?: string[]; network?: string[]; fs?: string[] }` | optional | Required permissions: legacy string[] or structured plugin block (ADR-0025 §3.2) | +| **objects** | `string[]` | optional | Glob patterns for ObjectQL schemas files | +| **datasources** | `string[]` | optional | Glob patterns for Datasource definitions | +| **dependencies** | `Record` | optional | Package dependencies | +| **configuration** | `{ title?: string; properties: Record }` | optional | Plugin configuration settings | +| **contributes** | `{ kinds?: object[]; routes?: object[] }` | optional | Platform contributions | +| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Initial seed data (prefer top-level data field) | +| **capabilities** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability | +| **extensions** | `Record` | optional | Extension points and contributions | +| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: (object \| … +8 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages | +| **loading** | `never` | optional | [REMOVED] `manifest.loading` was removed in @objectstack/spec 17.0.0 (#4914, ADR-0049 enforce-or-remove) — the entire block (`strategy`, `preload`, `codeSplitting`, `dynamicImport`, `initialization`, `dependencyResolution`, `hotReload`, `caching`, `sandboxing`, `monitoring`) had no runtime reader in any repo, so authoring it configured nothing. Delete the key. Plugins are composed at boot — `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder`); the set is fixed until the process restarts. ⚠️ `loading.sandboxing` in particular never isolated anything: it did not run plugins in a process, vm, iframe or web-worker, and `allowedServices` gated no call. If you were relying on it for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced. | +| **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | +| **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) | +| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier (ADR-0025 §3.6) | +| **packaging** | `Enum<'bundled' \| 'manifest-deps'>` | optional | Dependency packaging strategy (ADR-0025 §3.3) | +| **integrity** | `Record` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) | + +### Nested Shape: `PackageInstallRequest.artifactRef` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **url** | `string` | ✅ | Artifact download URL | +| **sha256** | `string` | ✅ | SHA256 checksum | +| **size** | `integer` | ✅ | Artifact size in bytes | +| **format** | `Enum<'tgz' \| 'zip'>` | optional (default: `"tgz"`) | Artifact format | +| **uploadedAt** | `string` | ✅ | Upload timestamp | + --- @@ -147,6 +238,28 @@ Install package response | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ package: object; dependencyResolution?: object; namespaceConflicts?: object[]; message?: string }` | ✅ | | +### Nested Shape: `PackageInstallResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `PackageInstallResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Installed package details | +| **dependencyResolution** | `{ dependencies: object[]; canProceed: boolean; requiredActions: object[]; installOrder: string[]; … }` | optional | Dependency resolution result | +| **namespaceConflicts** | `{ type: 'namespace_conflict'; requestedNamespace: string; conflictingPackageId: string; conflictingPackageName: string; … }[]` | optional | Namespace conflicts detected | +| **message** | `string` | optional | Installation status message | + --- @@ -189,6 +302,27 @@ Rollback package response | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ success: boolean; restoredVersion?: string; message?: string }` | ✅ | | +### Nested Shape: `PackageRollbackResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `PackageRollbackResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **success** | `boolean` | ✅ | Whether the rollback succeeded | +| **restoredVersion** | `string` | optional | Restored version | +| **message** | `string` | optional | Rollback status message | + --- @@ -208,6 +342,35 @@ Upgrade package request | **dryRun** | `boolean` | optional (default: `false`) | Preview upgrade without making changes | | **skipValidation** | `boolean` | optional (default: `false`) | Skip pre-upgrade compatibility checks | +### Nested Shape: `PackageUpgradeRequest.manifest` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique package identifier (reverse domain style) | +| **namespace** | `string` | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") | +| **defaultDatasource** | `string` | optional (default: `"default"`) | Default datasource for all objects in this package | +| **version** | `string` | ✅ | Package version (semantic versioning) | +| **type** | `Enum<'plugin' \| 'ui' \| 'driver' \| 'server' \| 'app' \| 'theme' \| 'agent' \| 'objectql' \| …>` | ✅ | Type of package | +| **scope** | `Enum<'cloud' \| 'system' \| 'project'>` | optional (default: `"project"`) | Deployment scope: cloud \| system \| project | +| **name** | `string` | ✅ | Human-readable package name | +| **description** | `string` | optional | Package description | +| **permissions** | `string[] \| { services?: string[]; hooks?: string[]; network?: string[]; fs?: string[] }` | optional | Required permissions: legacy string[] or structured plugin block (ADR-0025 §3.2) | +| **objects** | `string[]` | optional | Glob patterns for ObjectQL schemas files | +| **datasources** | `string[]` | optional | Glob patterns for Datasource definitions | +| **dependencies** | `Record` | optional | Package dependencies | +| **configuration** | `{ title?: string; properties: Record }` | optional | Plugin configuration settings | +| **contributes** | `{ kinds?: object[]; routes?: object[] }` | optional | Platform contributions | +| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Initial seed data (prefer top-level data field) | +| **capabilities** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability | +| **extensions** | `Record` | optional | Extension points and contributions | +| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: (object \| … +8 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages | +| **loading** | `never` | optional | [REMOVED] `manifest.loading` was removed in @objectstack/spec 17.0.0 (#4914, ADR-0049 enforce-or-remove) — the entire block (`strategy`, `preload`, `codeSplitting`, `dynamicImport`, `initialization`, `dependencyResolution`, `hotReload`, `caching`, `sandboxing`, `monitoring`) had no runtime reader in any repo, so authoring it configured nothing. Delete the key. Plugins are composed at boot — `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder`); the set is fixed until the process restarts. ⚠️ `loading.sandboxing` in particular never isolated anything: it did not run plugins in a process, vm, iframe or web-worker, and `allowedServices` gated no call. If you were relying on it for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced. | +| **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | +| **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) | +| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier (ADR-0025 §3.6) | +| **packaging** | `Enum<'bundled' \| 'manifest-deps'>` | optional | Dependency packaging strategy (ADR-0025 §3.3) | +| **integrity** | `Record` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) | + --- @@ -224,6 +387,31 @@ Upgrade package response | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ success: boolean; phase: string; plan?: object; snapshotId?: string; … }` | ✅ | | +### Nested Shape: `PackageUpgradeResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `PackageUpgradeResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **success** | `boolean` | ✅ | Whether the upgrade succeeded | +| **phase** | `string` | ✅ | Current upgrade phase | +| **plan** | `{ packageId: string; fromVersion: string; toVersion: string; impactLevel: Enum<'none' \| 'low' \| 'medium' \| 'high' \| 'critical'>; … }` | optional | Upgrade plan that was executed | +| **snapshotId** | `string` | optional | Snapshot ID for rollback | +| **conflicts** | `{ path: string; baseValue: any; incomingValue: any; customValue: any }[]` | optional | Unresolved merge conflicts | +| **errorMessage** | `string` | optional | Error message if failed | +| **message** | `string` | optional | Human-readable status message | + --- @@ -238,6 +426,35 @@ Resolve dependencies request | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest to resolve dependencies for | | **platformVersion** | `string` | optional | Current platform version for compatibility filtering | +### Nested Shape: `ResolveDependenciesRequest.manifest` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique package identifier (reverse domain style) | +| **namespace** | `string` | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") | +| **defaultDatasource** | `string` | optional (default: `"default"`) | Default datasource for all objects in this package | +| **version** | `string` | ✅ | Package version (semantic versioning) | +| **type** | `Enum<'plugin' \| 'ui' \| 'driver' \| 'server' \| 'app' \| 'theme' \| 'agent' \| 'objectql' \| …>` | ✅ | Type of package | +| **scope** | `Enum<'cloud' \| 'system' \| 'project'>` | optional (default: `"project"`) | Deployment scope: cloud \| system \| project | +| **name** | `string` | ✅ | Human-readable package name | +| **description** | `string` | optional | Package description | +| **permissions** | `string[] \| { services?: string[]; hooks?: string[]; network?: string[]; fs?: string[] }` | optional | Required permissions: legacy string[] or structured plugin block (ADR-0025 §3.2) | +| **objects** | `string[]` | optional | Glob patterns for ObjectQL schemas files | +| **datasources** | `string[]` | optional | Glob patterns for Datasource definitions | +| **dependencies** | `Record` | optional | Package dependencies | +| **configuration** | `{ title?: string; properties: Record }` | optional | Plugin configuration settings | +| **contributes** | `{ kinds?: object[]; routes?: object[] }` | optional | Platform contributions | +| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Initial seed data (prefer top-level data field) | +| **capabilities** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability | +| **extensions** | `Record` | optional | Extension points and contributions | +| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: (object \| … +8 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages | +| **loading** | `never` | optional | [REMOVED] `manifest.loading` was removed in @objectstack/spec 17.0.0 (#4914, ADR-0049 enforce-or-remove) — the entire block (`strategy`, `preload`, `codeSplitting`, `dynamicImport`, `initialization`, `dependencyResolution`, `hotReload`, `caching`, `sandboxing`, `monitoring`) had no runtime reader in any repo, so authoring it configured nothing. Delete the key. Plugins are composed at boot — `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder`); the set is fixed until the process restarts. ⚠️ `loading.sandboxing` in particular never isolated anything: it did not run plugins in a process, vm, iframe or web-worker, and `allowedServices` gated no call. If you were relying on it for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced. | +| **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | +| **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) | +| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier (ADR-0025 §3.6) | +| **packaging** | `Enum<'bundled' \| 'manifest-deps'>` | optional | Dependency packaging strategy (ADR-0025 §3.3) | +| **integrity** | `Record` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) | + --- @@ -254,6 +471,29 @@ Resolve dependencies response | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ dependencies: object[]; canProceed: boolean; requiredActions: object[]; installOrder: string[]; … }` | ✅ | Dependency resolution result with topological sort | +### Nested Shape: `ResolveDependenciesResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `ResolveDependenciesResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **dependencies** | `{ packageId: string; requiredRange: string; resolvedVersion?: string; installedVersion?: string; … }[]` | ✅ | Resolution result for each dependency | +| **canProceed** | `boolean` | ✅ | Whether installation can proceed | +| **requiredActions** | `{ type: Enum<'install' \| 'upgrade' \| 'confirm_conflict'>; packageId: string; description: string }[]` | ✅ | Actions required before proceeding | +| **installOrder** | `string[]` | ✅ | Topologically sorted package IDs for installation | +| **circularDependencies** | `string[][]` | optional | Circular dependency chains detected (e.g. [["A", "B", "A"]]) | + --- @@ -281,6 +521,27 @@ Uninstall package response | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ packageId: string; success: boolean; message?: string }` | ✅ | | +### Nested Shape: `UninstallPackageApiResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `UninstallPackageApiResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **packageId** | `string` | ✅ | Uninstalled package ID | +| **success** | `boolean` | ✅ | Whether uninstall succeeded | +| **message** | `string` | optional | Uninstall status message | + --- @@ -297,6 +558,22 @@ Upload artifact request | **token** | `string` | optional | Publisher authentication token | | **releaseNotes** | `string` | optional | Release notes for this version | +### Nested Shape: `UploadArtifactRequest.artifact` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **formatVersion** | `string` | optional (default: `"1.0"`) | Artifact format version (e.g. "1.0") | +| **packageId** | `string` | ✅ | Package identifier from manifest | +| **version** | `string` | ✅ | Package version from manifest | +| **format** | `Enum<'tgz' \| 'zip'>` | optional (default: `"tgz"`) | Archive format of the artifact | +| **size** | `integer` | optional | Total artifact file size in bytes | +| **builtAt** | `string` | ✅ | ISO 8601 timestamp of when the artifact was built | +| **builtWith** | `string` | optional | Build tool identifier (e.g. "os-cli@3.2.0") | +| **files** | `{ path: string; size: integer; category?: Enum<'objects' \| 'views' \| 'pages' \| 'flows' \| 'dashboards' \| 'permissions' \| …> }[]` | optional | List of files contained in the artifact | +| **metadataCategories** | `Enum<'objects' \| 'views' \| 'pages' \| 'flows' \| 'dashboards' \| 'permissions' \| …>[]` | optional | Metadata categories included in this artifact | +| **checksums** | `{ algorithm: Enum<'sha256' \| 'sha384' \| 'sha512'>; files: Record }` | optional | SHA256 checksums for artifact integrity verification | +| **signature** | `{ algorithm: Enum<'RSA-SHA256' \| 'RSA-SHA384' \| 'RSA-SHA512' \| 'ECDSA-SHA256'>; publicKeyRef: string; signature: string; signedAt?: string; … }` | optional | Digital signature for artifact authenticity verification | + --- @@ -313,6 +590,28 @@ Upload artifact response | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ success: boolean; artifactRef?: object; submissionId?: string; message?: string }` | ✅ | | +### Nested Shape: `UploadArtifactResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `UploadArtifactResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **success** | `boolean` | ✅ | Whether the upload succeeded | +| **artifactRef** | `{ url: string; sha256: string; size: integer; format: Enum<'tgz' \| 'zip'>; … }` | optional | Artifact reference in the registry | +| **submissionId** | `string` | optional | Marketplace submission ID for review tracking | +| **message** | `string` | optional | Upload status message | + --- diff --git a/content/docs/references/api/plugin-rest-api.mdx b/content/docs/references/api/plugin-rest-api.mdx index 54dad0c2fb..46ffb56abd 100644 --- a/content/docs/references/api/plugin-rest-api.mdx +++ b/content/docs/references/api/plugin-rest-api.mdx @@ -132,6 +132,20 @@ const result = ErrorHandlingConfigSchema.parse(data); | **license** | `{ name: string; url?: string }` | optional | API license information | | **securitySchemes** | `Record; scheme?: string; bearerFormat?: string }>` | optional | Security scheme definitions | +### Nested Shape: `OpenApiGenerationConfig.servers[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **url** | `string` | ✅ | Server URL | +| **description** | `string` | optional | Server description | + +### Nested Shape: `OpenApiGenerationConfig.license` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | License name | +| **url** | `string` | optional | License URL | + --- @@ -216,6 +230,101 @@ const result = ErrorHandlingConfigSchema.parse(data); | **cors** | `{ enabled: boolean; origins?: string[]; methods?: Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>[]; credentials: boolean }` | optional | CORS configuration | | **performance** | `{ enableCompression: boolean; enableETag: boolean; enableCaching: boolean; defaultCacheTtl: integer }` | optional | Performance optimization settings | +### Nested Shape: `RestApiPluginConfig.routes[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **prefix** | `string` | ✅ | URL path prefix for this route group | +| **service** | `string` | ✅ | Core service name (metadata, data, auth, etc.) | +| **category** | `Enum<'discovery' \| 'metadata' \| 'data' \| 'batch' \| 'permission' \| 'analytics' \| …>` | ✅ | Primary category for this route group | +| **methods** | `string[]` | optional | Protocol method names implemented | +| **endpoints** | `{ method: Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>; path: string; handler: string; category: Enum<'discovery' \| 'metadata' \| 'data' \| 'batch' \| 'permission' \| 'analytics' \| …>; … }[]` | optional | Endpoint definitions | +| **middleware** | `{ name: string; type: Enum<'authentication' \| 'authorization' \| 'logging' \| 'validation' \| 'transformation' \| 'error' \| 'custom'>; enabled: boolean; order: integer; … }[]` | optional | Middleware stack for this route group | +| **authRequired** | `boolean` | optional (default: `true`) | Whether authentication is required by default | +| **documentation** | `{ title?: string; description?: string; tags?: string[] }` | optional | Documentation metadata for this route group | + +### Nested Shape: `RestApiPluginConfig.validation` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | Enable automatic request validation | +| **mode** | `Enum<'strict' \| 'permissive' \| 'strip'>` | optional (default: `"strict"`) | How to handle validation errors | +| **validateBody** | `boolean` | optional (default: `true`) | Validate request body against schema | +| **validateQuery** | `boolean` | optional (default: `true`) | Validate query string parameters | +| **validateParams** | `boolean` | optional (default: `true`) | Validate URL path parameters | +| **validateHeaders** | `boolean` | optional (default: `false`) | Validate request headers | +| **includeFieldErrors** | `boolean` | optional (default: `true`) | Include field-level error details in response | +| **errorPrefix** | `string` | optional | Custom prefix for validation error messages | +| **schemaRegistry** | `string` | optional | Schema registry name to use for validation | + +### Nested Shape: `RestApiPluginConfig.responseEnvelope` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | Enable automatic response envelope wrapping | +| **includeMetadata** | `boolean` | optional (default: `true`) | Include meta object in responses | +| **includeTimestamp** | `boolean` | optional (default: `true`) | Include timestamp in response metadata | +| **includeRequestId** | `boolean` | optional (default: `true`) | Include requestId in response metadata | +| **includeDuration** | `boolean` | optional (default: `false`) | Include request duration in ms | +| **includeTraceId** | `boolean` | optional (default: `false`) | Include distributed traceId | +| **customMetadata** | `Record` | optional | Additional metadata fields to include | +| **skipIfWrapped** | `boolean` | optional (default: `true`) | Skip wrapping if response already has success field | + +### Nested Shape: `RestApiPluginConfig.errorHandling` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | Enable standardized error handling | +| **includeStackTrace** | `boolean` | optional (default: `false`) | Include stack traces in error responses | +| **logErrors** | `boolean` | optional (default: `true`) | Log errors to system logger | +| **exposeInternalErrors** | `boolean` | optional (default: `false`) | Expose internal error details in responses | +| **includeRequestId** | `boolean` | optional (default: `true`) | Include requestId in error responses | +| **includeTimestamp** | `boolean` | optional (default: `true`) | Include timestamp in error responses | +| **includeDocumentation** | `boolean` | optional (default: `true`) | Include documentation URLs for errors | +| **documentationBaseUrl** | `string` | optional | Base URL for error documentation | +| **customErrorMessages** | `Record` | optional | Custom error messages by error code | +| **redactFields** | `string[]` | optional | Field names to redact from error details | + +### Nested Shape: `RestApiPluginConfig.openApi` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | Enable automatic OpenAPI documentation generation | +| **version** | `Enum<'3.0.0' \| '3.0.1' \| '3.0.2' \| '3.0.3' \| '3.1.0'>` | optional (default: `"3.0.3"`) | OpenAPI specification version | +| **title** | `string` | optional (default: `"ObjectStack API"`) | API title | +| **description** | `string` | optional | API description | +| **apiVersion** | `string` | optional (default: `"1.0.0"`) | API version | +| **outputPath** | `string` | optional (default: `"/api/docs/openapi.json"`) | URL path to serve OpenAPI JSON | +| **uiPath** | `string` | optional (default: `"/api/docs"`) | URL path to serve documentation UI | +| **uiFramework** | `Enum<'swagger-ui' \| 'redoc' \| 'rapidoc' \| 'elements'>` | optional (default: `"swagger-ui"`) | Documentation UI framework | +| **includeInternal** | `boolean` | optional (default: `false`) | Include internal endpoints in documentation | +| **generateSchemas** | `boolean` | optional (default: `true`) | Auto-generate schemas from Zod definitions | +| **includeExamples** | `boolean` | optional (default: `true`) | Include request/response examples | +| **servers** | `{ url: string; description?: string }[]` | optional | Server URLs for API | +| **contact** | `{ name?: string; url?: string; email?: string }` | optional | API contact information | +| **license** | `{ name: string; url?: string }` | optional | API license information | +| **securitySchemes** | `Record; scheme?: string; bearerFormat?: string }>` | optional | Security scheme definitions | + +### Nested Shape: `RestApiPluginConfig.globalMiddleware[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Middleware name (snake_case) | +| **type** | `Enum<'authentication' \| 'authorization' \| 'logging' \| 'validation' \| 'transformation' \| 'error' \| 'custom'>` | ✅ | Middleware type | +| **enabled** | `boolean` | optional (default: `true`) | Whether middleware is enabled | +| **order** | `integer` | optional (default: `100`) | Execution order priority | +| **config** | `Record` | optional | Middleware configuration object | +| **paths** | `{ include?: string[]; exclude?: string[] }` | optional | Path filtering | + +### Nested Shape: `RestApiPluginConfig.performance` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enableCompression** | `boolean` | optional (default: `true`) | Enable response compression | +| **enableETag** | `boolean` | optional (default: `true`) | Enable ETag generation | +| **enableCaching** | `boolean` | optional (default: `true`) | Enable HTTP caching | +| **defaultCacheTtl** | `integer` | optional (default: `300`) | Default cache TTL in seconds | + --- @@ -254,6 +363,46 @@ const result = ErrorHandlingConfigSchema.parse(data); | **authRequired** | `boolean` | optional (default: `true`) | Whether authentication is required by default | | **documentation** | `{ title?: string; description?: string; tags?: string[] }` | optional | Documentation metadata for this route group | +### Nested Shape: `RestApiRouteRegistration.endpoints[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>` | ✅ | HTTP method for this endpoint | +| **path** | `string` | ✅ | URL path pattern (e.g., /api/v1/data/:object/:id) | +| **handler** | `string` | ✅ | Protocol method name or handler identifier | +| **category** | `Enum<'discovery' \| 'metadata' \| 'data' \| 'batch' \| 'permission' \| 'analytics' \| …>` | ✅ | Route category | +| **public** | `boolean` | optional (default: `false`) | Is publicly accessible without authentication | +| **permissions** | `string[]` | optional | Required permissions (e.g., ["data.read", "object.account.read"]) | +| **summary** | `string` | optional | Short description for OpenAPI | +| **description** | `string` | optional | Detailed description for OpenAPI | +| **tags** | `string[]` | optional | OpenAPI tags for grouping | +| **requestSchema** | `string` | optional | Request schema name (for validation) | +| **responseSchema** | `string` | optional | Response schema name (for documentation) | +| **timeout** | `integer` | optional | Request timeout in milliseconds | +| **rateLimit** | `string` | optional | Rate limit policy name | +| **cacheable** | `boolean` | optional (default: `false`) | Whether response can be cached | +| **cacheTtl** | `integer` | optional | Cache TTL in seconds | +| **handlerStatus** | `Enum<'implemented' \| 'stub' \| 'planned'>` | optional | Handler implementation status: implemented (default if omitted), stub, or planned | + +### Nested Shape: `RestApiRouteRegistration.middleware[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Middleware name (snake_case) | +| **type** | `Enum<'authentication' \| 'authorization' \| 'logging' \| 'validation' \| 'transformation' \| 'error' \| 'custom'>` | ✅ | Middleware type | +| **enabled** | `boolean` | optional (default: `true`) | Whether middleware is enabled | +| **order** | `integer` | optional (default: `100`) | Execution order priority | +| **config** | `Record` | optional | Middleware configuration object | +| **paths** | `{ include?: string[]; exclude?: string[] }` | optional | Path filtering | + +### Nested Shape: `RestApiRouteRegistration.documentation` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **title** | `string` | optional | Route group title | +| **description** | `string` | optional | Route group description | +| **tags** | `string[]` | optional | OpenAPI tags | + --- @@ -284,6 +433,26 @@ const result = ErrorHandlingConfigSchema.parse(data); | **summary** | `{ total: integer; implemented: integer; stub: integer; planned: integer }` | ✅ | | | **entries** | `{ path: string; method: Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>; category: Enum<'discovery' \| 'metadata' \| 'data' \| 'batch' \| 'permission' \| 'analytics' \| …>; handlerStatus: Enum<'implemented' \| 'stub' \| 'planned'>; … }[]` | ✅ | Per-endpoint coverage entries | +### Nested Shape: `RouteCoverageReport.summary` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **total** | `integer` | ✅ | Total declared endpoints | +| **implemented** | `integer` | ✅ | Endpoints with real handlers | +| **stub** | `integer` | ✅ | Endpoints with stub handlers (501) | +| **planned** | `integer` | ✅ | Endpoints not yet implemented | + +### Nested Shape: `RouteCoverageReport.entries[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **path** | `string` | ✅ | Full URL path (e.g. /api/v1/analytics/query) | +| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>` | ✅ | HTTP method (GET, POST, etc.) | +| **category** | `Enum<'discovery' \| 'metadata' \| 'data' \| 'batch' \| 'permission' \| 'analytics' \| …>` | ✅ | Route category | +| **handlerStatus** | `Enum<'implemented' \| 'stub' \| 'planned'>` | ✅ | Handler status | +| **service** | `string` | ✅ | Target service name | +| **healthCheckPassed** | `boolean` | optional | Whether the health check probe succeeded | + --- diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index c3e4e11841..313071b761 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -45,6 +45,14 @@ const result = AiAgentCapabilitiesSchema.parse(data); | **context** | `Record` | optional | Agent context (app, object, record, …) | | **options** | `Record` | optional | Request options (model, temperature, …) | +### Nested Shape: `AiAgentChatRequest.messages[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **role** | `Enum<'system' \| 'user' \| 'assistant' \| 'tool'>` | ✅ | Message role | +| **content** | `any` | optional | Message content: a string, or an array of content parts | +| **parts** | `any[]` | optional | Vercel AI SDK v6 message parts (alternative to `content`) | + --- @@ -59,6 +67,15 @@ const result = AiAgentCapabilitiesSchema.parse(data); | **role** | `string` | ✅ | Agent role | | **capabilities** | `{ authoring: boolean; canvas: boolean; debug: boolean; resume: boolean }` | ✅ | Capability set implied by the agent surface | +### Nested Shape: `AiAgentSummary.capabilities` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **authoring** | `boolean` | ✅ | Authors app metadata (objects/views/flows) | +| **canvas** | `boolean` | ✅ | Drives the Live Canvas split view (ADR-0037) | +| **debug** | `boolean` | ✅ | Exposes the build-doctor debug drawer | +| **resume** | `boolean` | ✅ | Turns resume durable multi-step runs (ADR-0013) | + --- @@ -70,6 +87,15 @@ const result = AiAgentCapabilitiesSchema.parse(data); | :--- | :--- | :--- | :--- | | **agents** | `{ name: string; label: string; role: string; capabilities: object }[]` | ✅ | Agents this caller may chat with | +### Nested Shape: `AiAgentsResponse.agents[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Agent name — the `:agentName` path segment | +| **label** | `string` | ✅ | Display label | +| **role** | `string` | ✅ | Agent role | +| **capabilities** | `{ authoring: boolean; canvas: boolean; debug: boolean; resume: boolean }` | ✅ | Capability set implied by the agent surface | + --- @@ -89,6 +115,14 @@ const result = AiAgentCapabilitiesSchema.parse(data); | **turnId** | `string` | optional | Stable per-turn idempotency key (ADR-0013 D1) | | **options** | `Record` | optional | Legacy nested request options | +### Nested Shape: `AiChatRequest.messages[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **role** | `Enum<'system' \| 'user' \| 'assistant' \| 'tool'>` | ✅ | Message role | +| **content** | `any` | optional | Message content: a string, or an array of content parts | +| **parts** | `any[]` | optional | Vercel AI SDK v6 message parts (alternative to `content`) | + --- @@ -104,6 +138,14 @@ const result = AiAgentCapabilitiesSchema.parse(data); | **usage** | `{ promptTokens: number; completionTokens: number; totalTokens: number }` | optional | Token usage | | **conversationId** | `string` | optional | Conversation the turn was persisted into | +### Nested Shape: `AiChatResponse.usage` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **promptTokens** | `number` | ✅ | Tokens consumed by the prompt | +| **completionTokens** | `number` | ✅ | Tokens generated | +| **totalTokens** | `number` | ✅ | prompt + completion | + --- @@ -134,6 +176,14 @@ const result = AiAgentCapabilitiesSchema.parse(data); | **updatedAt** | `string` | ✅ | Last update timestamp (ISO 8601) | | **metadata** | `Record` | optional | Conversation metadata | +### Nested Shape: `AiConversation.messages[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **role** | `Enum<'system' \| 'user' \| 'assistant' \| 'tool'>` | ✅ | Message role | +| **content** | `any` | optional | Message content: a string, or an array of content parts | +| **parts** | `any[]` | optional | Vercel AI SDK v6 message parts (alternative to `content`) | + --- @@ -159,6 +209,14 @@ const result = AiAgentCapabilitiesSchema.parse(data); | **models** | `(string \| { id: string; label: string; default: boolean })[]` | ✅ | Models this environment offers | | **defaultModel** | `string` | optional | Default model id, when the service reports one | +### Nested Shape: `AiModelsResponse.models[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Model id | +| **label** | `string` | ✅ | Display label for the picker | +| **default** | `boolean` | ✅ | Whether this is the environment default | + --- @@ -246,6 +304,22 @@ const result = AiAgentCapabilitiesSchema.parse(data); | :--- | :--- | :--- | :--- | | **events** | `{ id: any; occurredAt: string; actor: string; source: string \| null; … }[]` | ✅ | Recent protection-audit events for the item, newest first. See the schema-level note for what an empty array means — and what it never means. | +### Nested Shape: `AuditMetaItemResponse.events[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `any` | ✅ | Row id of the audit event. Opaque to callers. | +| **occurredAt** | `string` | ✅ | When the attempt happened (ISO-8601 string). | +| **actor** | `string` | ✅ | Who attempted the operation. `system` when the row recorded no actor. | +| **source** | `string \| null` | ✅ | Which code path recorded the event (e.g. `protocol.deleteMetaItem`). `null` when the row recorded none. | +| **operation** | `Enum<'save' \| 'publish' \| 'rollback' \| 'delete' \| 'reset'>` | ✅ | Which metadata-protection door was attempted. | +| **outcome** | `Enum<'allowed' \| 'denied' \| 'forced'>` | ✅ | Whether the attempt went through, was refused, or overrode a lock (ADR-0010 §3.6). | +| **code** | `string` | ✅ | Machine-readable verdict code for the outcome (e.g. `item_locked`). Empty string when the row recorded none. | +| **lockState** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'> \| null` | ✅ | The lock verdict in force at the time of the attempt (ADR-0010 §3.3). `null` when no lock applied. | +| **lockOverridden** | `boolean` | ✅ | True when the attempt went through by overriding a lock (`outcome: "forced"` rows). | +| **requestId** | `string \| null` | ✅ | Correlation id of the originating request. `null` when the row recorded none. | +| **note** | `string \| null` | ✅ | Free-text note recorded with the event. `null` when the row recorded none. | + --- @@ -258,6 +332,32 @@ const result = AiAgentCapabilitiesSchema.parse(data); | **actions** | `{ type: string; version: string; name: string; description?: string; … }[]` | ✅ | Registered action descriptors (built-in + plugin) | | **total** | `integer` | ✅ | Number of descriptors returned (after any filters) | +### Nested Shape: `AutomationActionsResponse.actions[number]` + +Canonical cross-paradigm action/node descriptor (ADR-0018) + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `string` | ✅ | Registry action/node type (matches the executor type) | +| **version** | `string` | ✅ | Executor version (semver) | +| **name** | `string` | ✅ | Display label (or i18n key) | +| **description** | `string` | optional | Action description | +| **icon** | `string` | optional | Icon id resolved by the designer | +| **category** | `Enum<'logic' \| 'data' \| 'io' \| 'human' \| 'control' \| 'custom'>` | optional (default: `"custom"`) | Palette category | +| **paradigms** | `Enum<'flow' \| 'approval'>[]` | optional (default: `["flow"]`) | Authoring surfaces that may offer this action | +| **configSchema** | `any` | optional | JSON Schema for the node config (drives the designer form; undeclared keys are rejected at registration) | +| **supportsPause** | `boolean` | optional (default: `false`) | Supports async pause/resume | +| **supportsCancellation** | `boolean` | optional (default: `false`) | Supports cancellation | +| **supportsRetry** | `boolean` | optional (default: `true`) | Supports retry on failure | +| **needsOutbox** | `boolean` | optional (default: `false`) | Dispatch via service-messaging outbox (retry/idempotency/dead-letter) | +| **isAsync** | `never` | optional | [REMOVED] `ActionDescriptor.isAsync` was removed in @objectstack/spec 17 (#6748, ADR-0049) — no execution path ever read it, so declaring it never made a node suspend and omitting it never stopped one. Delete the key. The live mechanism is two-part: an executor suspends by RETURNING `suspend: true` from `execute()`, and its descriptor must declare `supportsPause: true` (plus the `resumeAuthority` its pauses need) or the engine refuses that suspension (#6667). Declaring `isAsync: true` alongside `supportsPause: true` was always redundant; declaring it alone was always inert. | +| **handlerContract** | `Enum<'none' \| 'pure'>` | optional (default: `"none"`) | Effect contract for author-supplied code this action invokes: 'none' (invokes none) or 'pure' (must not write — it returns a value and the flow graph persists it) | +| **resumeAuthority** | `Enum<'any' \| 'service'>` | optional | Who may resume a run this node suspended: 'any' (the generic resume route) or 'service' (only the owning service, e.g. approvals). Carries no schema default so an omission stays observable — and an omission is fail-CLOSED at run time, equivalent to 'service': a pausing node whose pause is open to the generic route must declare 'any' explicitly (#5561) | +| **maturity** | `Enum<'ga' \| 'beta' \| 'reserved'>` | optional (default: `"ga"`) | Runtime maturity: ga (shipped), beta, or reserved (contract only — designers grey this out) | +| **source** | `Enum<'builtin' \| 'plugin'>` | optional (default: `"plugin"`) | builtin = platform baseline; plugin = third-party contributed | +| **deprecated** | `boolean` | optional (default: `false`) | Deprecated alias kept for back-compat | +| **aliasOf** | `string` | optional | Canonical type this alias forwards to | + --- @@ -295,6 +395,14 @@ const result = AiAgentCapabilitiesSchema.parse(data); | **object** | `string` | ✅ | Object name | | **request** | `{ operation: Enum<'create' \| 'update' \| 'upsert' \| 'delete'>; records: object[]; options?: object }` | ✅ | Batch operation request | +### Nested Shape: `BatchDataRequest.request` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **operation** | `Enum<'create' \| 'update' \| 'upsert' \| 'delete'>` | ✅ | Type of batch operation | +| **records** | `{ id?: string; data?: Record; externalId?: string }[]` | ✅ | Array of records to process (server caps the count — see batch.maxBatchSize) | +| **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean }` | optional | Batch operation options | + --- @@ -313,6 +421,30 @@ const result = AiAgentCapabilitiesSchema.parse(data); | **failed** | `number` | ✅ | Number of records that failed | | **results** | `{ id?: string; success: boolean; errors?: object[]; data?: Record; … }[]` | ✅ | Detailed results for each record | +### Nested Shape: `BatchDataResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `BatchDataResponse.results[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | optional | Record ID if operation succeeded | +| **success** | `boolean` | ✅ | Whether this record was processed successfully | +| **errors** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>; declaredCode?: string; message: string; userMessage?: string; … }[]` | optional | Array of errors if operation failed. Branch on `errors[0].code` — an atomic batch that rolled back marks rows that were written then undone with code ROLLED_BACK and rows never reached with NOT_ATTEMPTED, while the causal row keeps its own error (#4793). A NON-atomic batch that stopped (the `continueOnError: false` default) marks its un-attempted tail with the same NOT_ATTEMPTED code — rows before the failure stay written and keep reporting success, since nothing was rolled back (#7539). | +| **data** | `Record` | optional | Full record data (if returnRecords=true) | +| **index** | `number` | optional | Index of the record in the request array | +| **droppedFields** | `{ object: string; fields: string[]; reason: Enum<'readonly' \| 'readonly_when' \| 'primary_key'> }[]` | optional | Write-observability (#3407/#3431/#3455): caller-supplied fields LEGALLY stripped from THIS row before it was written — static `readonly` (#2948) / TRUE `readonlyWhen` (#3042) on update, or the #3043 create-ingress strip. Per-row because a batch can drop different fields on different rows (`readonlyWhen` is record-state-dependent). Present ONLY when ≥1 field was dropped for this row; the row still succeeded (success unchanged). A single response header cannot express per-row drops, so this body field is the canonical bulk channel — REST does not emit `X-ObjectStack-Dropped-Fields` for batches. Optional — omit-when-empty keeps the shape backward-compatible. | + --- @@ -392,6 +524,16 @@ const result = AiAgentCapabilitiesSchema.parse(data); | **record** | `Record` | ✅ | The created record, including server-generated fields (created_at, owner). | | **droppedFields** | `{ object: string; fields: string[]; reason: Enum<'readonly' \| 'readonly_when' \| 'primary_key'> }[]` | optional | Write-observability (#3407/#3431): caller-supplied fields that were LEGALLY stripped before the record was written — a non-system create cannot seed a static `readonly` column (#3043 ingress strip), so those keys are dropped and the field re-derives its default. Present ONLY when ≥1 field was dropped; the create still succeeded without them (status/success semantics unchanged). REST additionally surfaces this as the `X-ObjectStack-Dropped-Fields` response header. Optional — omit-when-empty keeps the shape backward-compatible for existing clients. | +### Nested Shape: `CreateDataResponse.droppedFields[number]` + +A write-path strip event: caller-supplied fields legally dropped from the payload (#3407) + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **object** | `string` | ✅ | Object the write targeted (resolved object name) | +| **fields** | `string[]` | ✅ | Caller-supplied field names the engine removed from the write payload | +| **reason** | `Enum<'readonly' \| 'readonly_when' \| 'primary_key'>` | ✅ | Why the fields were dropped: static readonly (#2948), a TRUE readonlyWhen predicate (#3042), or the primary-key strip of a payload id the engine ruled is not an identifier (#6437) | + --- @@ -418,6 +560,16 @@ const result = AiAgentCapabilitiesSchema.parse(data); | **count** | `number` | ✅ | Number of records created | | **droppedFields** | `{ object: string; fields: string[]; reason: Enum<'readonly' \| 'readonly_when' \| 'primary_key'> }[]` | optional | Write-observability (#3407/#3431/#3455): caller-supplied `readonly` fields the #3043 create-ingress strip removed before the rows were written. AGGREGATED across the batch (one event per object/reason with the union of dropped field names) rather than per-row, because the insert-time strip is static-`readonly` only — schema-uniform, so every row drops the same set. Present ONLY when ≥1 field was dropped; the creates still succeeded without them (count/success unchanged). Optional — omit-when-empty keeps the shape backward-compatible. (The per-row `insertMany`/`batch` paths carry per-row `droppedFields` on each result instead — see BatchOperationResultSchema.) | +### Nested Shape: `CreateManyDataResponse.droppedFields[number]` + +A write-path strip event: caller-supplied fields legally dropped from the payload (#3407) + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **object** | `string` | ✅ | Object the write targeted (resolved object name) | +| **fields** | `string[]` | ✅ | Caller-supplied field names the engine removed from the write payload | +| **reason** | `Enum<'readonly' \| 'readonly_when' \| 'primary_key'>` | ✅ | Why the fields were dropped: static readonly (#2948), a TRUE readonlyWhen predicate (#3042), or the primary-key strip of a payload id the engine ruled is not an identifier (#6437) | + --- @@ -457,6 +609,15 @@ const result = AiAgentCapabilitiesSchema.parse(data); | **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean }` | optional | Delete options | | **object** | `string` | ✅ | Object name | +### Nested Shape: `DeleteManyDataRequest.options` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **atomic** | `boolean` | optional (default: `false`) | Opt-in all-or-nothing. When explicitly true the whole batch runs inside ONE engine transaction: the first failure rolls back every prior write, and the response reports zero successes — each row carries `errors[0].code` ROLLED_BACK (written, then undone), the causal row its own error, and rows never reached NOT_ATTEMPTED. A runtime that cannot roll back REFUSES the request (501 NOT_IMPLEMENTED) rather than silently degrading to best-effort — probe `capabilities.transactionalBatch` on /discovery first. Takes precedence over continueOnError. Default false: sequential best-effort. | +| **returnRecords** | `boolean` | optional (default: `false`) | If true, return full record data in response | +| **continueOnError** | `boolean` | optional (default: `false`) | If true (and atomic=false), continue processing remaining records after errors. Default false: the first failure ENDS the run — records before it stay written (nothing is rolled back on this arm), and every record after it is reported `errors[0].code` NOT_ATTEMPTED rather than omitted, so `results` always covers all `total` records and `succeeded + failed === total` (#7539). | +| **validateOnly** | `never` | optional | [REMOVED] `options.validateOnly` was removed from BatchOptions in @objectstack/spec (#4052). It was never implemented: the batch surfaces persisted regardless, so a "dry-run" would have silently executed. There is no dry-run today — drop the key. If you need to preview a batch without writing, open an issue so it can be designed (no-commit cascade / constraint semantics) and reintroduced as a flag that actually holds. | + --- @@ -475,6 +636,30 @@ const result = AiAgentCapabilitiesSchema.parse(data); | **failed** | `number` | ✅ | Number of records that failed | | **results** | `{ id?: string; success: boolean; errors?: object[]; data?: Record; … }[]` | ✅ | Detailed results for each record | +### Nested Shape: `DeleteManyDataResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `DeleteManyDataResponse.results[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | optional | Record ID if operation succeeded | +| **success** | `boolean` | ✅ | Whether this record was processed successfully | +| **errors** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>; declaredCode?: string; message: string; userMessage?: string; … }[]` | optional | Array of errors if operation failed. Branch on `errors[0].code` — an atomic batch that rolled back marks rows that were written then undone with code ROLLED_BACK and rows never reached with NOT_ATTEMPTED, while the causal row keeps its own error (#4793). A NON-atomic batch that stopped (the `continueOnError: false` default) marks its un-attempted tail with the same NOT_ATTEMPTED code — rows before the failure stay written and keep reporting success, since nothing was rolled back (#7539). | +| **data** | `Record` | optional | Full record data (if returnRecords=true) | +| **index** | `number` | optional | Index of the record in the request array | +| **droppedFields** | `{ object: string; fields: string[]; reason: Enum<'readonly' \| 'readonly_when' \| 'primary_key'> }[]` | optional | Write-observability (#3407/#3431/#3455): caller-supplied fields LEGALLY stripped from THIS row before it was written — static `readonly` (#2948) / TRUE `readonlyWhen` (#3042) on update, or the #3043 create-ingress strip. Per-row because a batch can drop different fields on different rows (`readonlyWhen` is record-state-dependent). Present ONLY when ≥1 field was dropped for this row; the row still succeeded (success unchanged). A single response header cannot express per-row drops, so this body field is the canonical bulk channel — REST does not emit `X-ObjectStack-Dropped-Fields` for batches. Optional — omit-when-empty keeps the shape backward-compatible. | + --- @@ -532,6 +717,23 @@ Disable package response | **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Disabled package details | | **message** | `string` | optional | Disable status message | +### Nested Shape: `DisablePackageResponse.package` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Full package manifest | +| **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | +| **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | +| **installedAt** | `string` | optional | Installation timestamp | +| **updatedAt** | `string` | optional | Last update timestamp | +| **installedVersion** | `string` | optional | Currently installed version for quick access | +| **previousVersion** | `string` | optional | Version before the last upgrade | +| **statusChangedAt** | `string` | optional | Status change timestamp | +| **errorMessage** | `string` | optional | Error message when status is error | +| **settings** | `Record` | optional | User-provided configuration settings | +| **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history | +| **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package | + --- @@ -559,6 +761,23 @@ Enable package response | **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Enabled package details | | **message** | `string` | optional | Enable status message | +### Nested Shape: `EnablePackageResponse.package` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Full package manifest | +| **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | +| **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | +| **installedAt** | `string` | optional | Installation timestamp | +| **updatedAt** | `string` | optional | Last update timestamp | +| **installedVersion** | `string` | optional | Currently installed version for quick access | +| **previousVersion** | `string` | optional | Version before the last upgrade | +| **statusChangedAt** | `string` | optional | Status change timestamp | +| **errorMessage** | `string` | optional | Error message when status is error | +| **settings** | `Record` | optional | User-provided configuration settings | +| **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history | +| **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package | + --- @@ -571,6 +790,28 @@ Enable package response | **object** | `string` | ✅ | The unique machine name of the object to query (e.g. "account"). | | **query** | `{ object: string; fields?: string[]; where?: any; search?: string \| object; … }` | optional | Structured query definition (filter, sort, select, pagination). | +### Nested Shape: `FindDataRequest.query` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **object** | `string` | ✅ | Object name (e.g. account) | +| **fields** | `string[]` | optional | Fields to retrieve — names of the queried object's OWN columns. A dotted path (`owner.name`) is not a projection: no driver resolves one, and the ingress refuses it with `400 INVALID_FIELD` (#7532). Related data is read with `expand`, whose nested QueryAST both filters (`where`) and selects (`fields`) the related record's columns. The projection must RETAIN the foreign-key column: `fields: ['title']` with `expand: 'project_id'` resolves nothing, because the relation is carried by that key — add `'project_id'` and it works. Where the value is wanted on the queried object itself, denormalise it onto that object (a stored field, written when the source changes), the same remedy the sort axis prescribes (#6924). | +| **where** | `any` | optional | Filtering criteria (WHERE) | +| **search** | `string \| { query: string; fields?: string[]; fuzzy: boolean; operator: Enum<'and' \| 'or'>; … }` | optional | Full-text search — the query text (canonical, ADR-0061 D1), or a structured FullTextSearch configuration | +| **searchFields** | `string[]` | optional | Narrow the search to these fields (server-intersected with the allowed searchable set — can only narrow, never widen; ADR-0061 D1) | +| **orderBy** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Sorting instructions (ORDER BY) | +| **limit** | `number` | optional | Max records to return (LIMIT) | +| **offset** | `number` | optional | Records to skip (OFFSET) | +| **top** | `number` | optional | Alias for limit (OData compatibility) | +| **cursor** | `never` | optional | [REMOVED] `query.cursor` was removed in @objectstack/spec 17 (#4286, ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; `QueryBuilder.cursor()` was removed with it. Express the keyset as an ordinary `where` predicate on your sort key — `where: { created_at: { $gt: last.created_at } }` with the matching `orderBy` — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. | +| **joins** | `never` | optional | [REMOVED] `query.joins` was removed in @objectstack/spec 17 (#4286, ADR-0049) — no engine or driver ever read it: a query carrying `joins` behaved exactly as if the key were absent, while its name squatted on the reserved REST parameter set. Delete the key. Related records are read through `expand` — `expand: { owner_id: { object: 'user', fields: ['name'] } }` — which the engine resolves via batch $in queries, and whose nested query selects the related record's own columns. Keep the foreign key in your own projection (`fields: ['title', 'owner_id']`): the relation is carried by that column, so projecting it away leaves expansion nothing to resolve. A dotted `fields` path is NOT a replacement — no driver ever resolved one and the ingress refuses it (`400 INVALID_FIELD`, #7532). | +| **aggregations** | `{ function: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>; field?: string; alias: string; filter?: any }[]` | optional | Aggregation functions | +| **groupBy** | `(string \| { field: string; dateGranularity?: Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>; alias?: string })[]` | optional | GROUP BY targets (strings or `{field, dateGranularity?}` objects for date bucketing) | +| **having** | `any` | optional | HAVING — filter over the AGGREGATED rows (aggregation aliases + groupBy projections); applied engine-side after aggregation | +| **windowFunctions** | `never` | optional | [REMOVED] `query.windowFunctions` was removed in @objectstack/spec 17 (#4286, ADR-0049) — `find()` never applied it: no engine or driver read the key on the query path, so every OVER clause it declared was silently dropped. Delete the key. Window functions are a SQL-driver capability behind `SqlDriver.findWithWindowFunctions(object, query)` (embedder-level; not on the `IDataDriver` contract or the REST surface); request-level analytics are `aggregations` + `groupBy`. | +| **distinct** | `never` | optional | [REMOVED] `query.distinct` was removed in @objectstack/spec 17 (#4286, ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded `total`/`hasMore` to a page-local estimate while still returning duplicate rows. Delete the key; `QueryBuilder.distinct()` was removed with it, and the count suppression is gone (`total` is truthful again). For unique values of one column use the SQL/memory drivers' `distinct(object, field)` door; for unique combinations, `groupBy`; for a deduplicated count, the `count_distinct` aggregation. | +| **expand** | `Record` | optional | Recursive relation loading map. Keys are lookup/master_detail field names; values are nested QueryAST objects that control select (`fields`) and filter (`where`, AND-merged with the batch $in), plus further expansion on the related object. The engine resolves expand via batch $in queries (driver-agnostic) with a default max depth of 3; per-parent `limit`/`offset`/`orderBy` are NOT applied on this path. | + --- @@ -644,6 +885,75 @@ Enable package response | **metadata** | `Record` | optional | Custom metadata key-value pairs for extensibility | | **apiName** | `string` | optional | API name (deprecated — use `name`; removed in protocol 18) | +### Nested Shape: `GetDiscoveryResponse.routes` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **data** | `string` | ✅ | e.g. /api/v1/data | +| **metadata** | `string` | ✅ | e.g. /api/v1/meta | +| **discovery** | `string` | optional | e.g. /api/v1/discovery | +| **ui** | `string` | optional | e.g. /api/v1/ui | +| **auth** | `string` | optional | e.g. /api/v1/auth | +| **automation** | `string` | optional | e.g. /api/v1/automation | +| **storage** | `string` | optional | e.g. /api/v1/storage | +| **analytics** | `string` | optional | e.g. /api/v1/analytics | +| **packages** | `string` | optional | e.g. /api/v1/packages | +| **datasources** | `string` | optional | e.g. /api/v1/datasources — base for the datasources/:name/external/* federation-admin family; absent when no host mounts it | +| **email** | `string` | optional | e.g. /api/v1/email — base for the email/send endpoint; absent when no host mounts it | +| **approvals** | `string` | optional | e.g. /api/v1/approvals | +| **realtime** | `string` | optional | e.g. /api/v1/realtime | +| **notifications** | `string` | optional | e.g. /api/v1/notifications | +| **ai** | `string` | optional | e.g. /api/v1/ai | +| **i18n** | `string` | optional | e.g. /api/v1/i18n | +| **mcp** | `string` | optional | e.g. /api/v1/mcp — always the unscoped base; absent when MCP is disabled or unserveable | + +### Nested Shape: `GetDiscoveryResponse.services[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | ✅ | | +| **status** | `Enum<'available' \| 'registered' \| 'unavailable' \| 'degraded' \| 'stub'>` | ✅ | available = fully operational, registered = route declared but handler unverified, unavailable = not installed, degraded = partial, stub = placeholder that returns 501 | +| **handlerReady** | `boolean` | optional | Whether the HTTP handler is confirmed to be mounted. Omitted = readiness unknown/unverified; true = handler mounted; false = handler missing or stub (likely 501). | +| **route** | `string` | optional | e.g. /api/v1/analytics | +| **provider** | `string` | optional | e.g. "objectql", "plugin-redis", "driver-memory" | +| **version** | `string` | optional | Semantic version of the service implementation (e.g. "3.0.6") | +| **message** | `string` | optional | e.g. "Install plugin-workflow to enable" | +| **rateLimit** | `{ requestsPerMinute?: integer; requestsPerHour?: integer; burstLimit?: integer; retryAfterMs?: integer }` | optional | Rate limit and quota info for this service | + +### Nested Shape: `GetDiscoveryResponse.capabilities` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **comments** | `{ enabled: boolean; features?: Record; description?: string }` | ✅ | Whether the backend supports record comments / chatter (the `sys_comment` object served via the data API) | +| **automation** | `{ enabled: boolean; features?: Record; description?: string }` | ✅ | Whether the backend supports Automation CRUD (flows, triggers) | +| **cron** | `{ enabled: boolean; features?: Record; description?: string }` | ✅ | Whether the backend supports cron scheduling | +| **search** | `{ enabled: boolean; features?: Record; description?: string }` | ✅ | Whether the backend supports full-text search | +| **export** | `{ enabled: boolean; features?: Record; description?: string }` | ✅ | Whether the backend supports async export | +| **chunkedUpload** | `{ enabled: boolean; features?: Record; description?: string }` | ✅ | Whether the backend supports chunked (multipart) uploads | +| **transactionalBatch** | `{ enabled: boolean; features?: Record; description?: string }` | ✅ | Whether the backend exposes the atomic cross-object batch endpoint (POST `{basePath}`/batch, #1604/ADR-0034): all ops commit or roll back together in one transaction. Lets clients skip non-atomic client-side simulation instead of runtime-probing 404/405/501. True ⟺ the /batch route is mounted AND the runtime can honour a transaction. | +| **websockets** | `{ enabled: boolean; features?: Record; description?: string }` | ✅ | Whether the backend mounts a realtime push surface (WebSocket/SSE) clients can subscribe to. False while realtime is an in-process bus with no mounted HTTP/WS surface (ADR-0076 D12, #2462). | +| **files** | `{ enabled: boolean; features?: Record; description?: string }` | ✅ | Whether a file-storage surface (upload/download/attachments) is served | +| **analytics** | `{ enabled: boolean; features?: Record; description?: string }` | ✅ | Whether the backend serves the analytics / BI query surface | +| **ai** | `{ enabled: boolean; features?: Record; description?: string }` | ✅ | Whether the backend serves the AI surface (NLQ, chat, agents, suggest) | +| **notifications** | `{ enabled: boolean; features?: Record; description?: string }` | ✅ | Whether the backend serves the notification surface (inbox, delivery) | +| **i18n** | `{ enabled: boolean; features?: Record; description?: string }` | ✅ | Whether the backend serves the i18n surface (translations, locale negotiation) | + +### Nested Shape: `GetDiscoveryResponse.schemaDiscovery` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **openapi** | `string` | optional | URL to OpenAPI (Swagger) specification (e.g., "/api/v1/openapi.json") | +| **jsonSchema** | `string` | optional | URL to JSON Schema definitions | + +### Nested Shape: `GetDiscoveryResponse.scoping` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | ✅ | Whether environment-scoped routes are mounted at all | +| **resolution** | `Enum<'required' \| 'optional' \| 'auto'>` | ✅ | How the environment id is resolved when scoping is enabled (mirrors RestApiConfig.projectResolution) | +| **scoped** | `boolean` | ✅ | Whether THIS response was served from the environment-scoped mount | +| **environmentId** | `string` | optional | The resolved environment id — present only on a scoped mount | + --- @@ -666,6 +976,24 @@ Enable package response | **objects** | `Record` | ✅ | Effective object permissions keyed by object name | | **systemPermissions** | `string[]` | ✅ | Effective system-level permissions | +### Nested Shape: `GetEffectivePermissionsResponse.objects[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **allowCreate** | `boolean` | optional (default: `false`) | Create permission | +| **allowRead** | `boolean` | optional (default: `false`) | Read permission | +| **allowEdit** | `boolean` | optional (default: `false`) | Edit permission | +| **allowDelete** | `boolean` | optional (default: `false`) | Delete permission | +| **allowExport** | `boolean` | optional | [#3544] User-level export axis over read (opt-in grant). true = export granted (still bounded by read); unset/false = no export. Merged most-permissively like the CRUD bits; NOT implied by viewAllRecords/modifyAllRecords. | +| **allowTransfer** | `boolean` | optional (default: `false`) | [RBAC-gated; ENFORCED now via insert/update owner_id guard, #3004] Change record ownership (assign/reassign/disown owner_id) | +| **allowRestore** | `boolean` | optional (default: `false`) | [RBAC-gated; operation pending M2] Restore from trash (Undelete) | +| **allowPurge** | `boolean` | optional (default: `false`) | [RBAC-gated; operation pending M2] Permanently delete (Hard Delete/GDPR) | +| **viewAllRecords** | `boolean` | optional (default: `false`) | View All Data (Bypass Sharing) | +| **modifyAllRecords** | `boolean` | optional (default: `false`) | Modify All Data (Bypass Sharing) — bypasses sharing rules and ownership on the objects record sharing enforces on; on an object with NO owner field sharing abstains, so the platform created_by write floor still applies (#6698). | +| **readScope** | `Enum<'own' \| 'own_and_reports' \| 'unit' \| 'unit_and_below' \| 'org'>` | optional | [ADR-0057 D1] Read depth: own\|unit\|unit_and_below\|org | +| **writeScope** | `Enum<'own' \| 'own_and_reports' \| 'unit' \| 'unit_and_below' \| 'org'>` | optional | [ADR-0057 D1] Write depth: own\|unit\|unit_and_below\|org | +| **apiOperations** | `Enum<'get' \| 'list' \| 'create' \| 'update' \| 'delete' \| 'upsert' \| 'bulk' \| …>[]` | optional | Server-resolved effective API operations for this object (#3391). Present only when the object tightens exposure via apiMethods; absent = default-allow. The frontend renders this effective set, never the raw whitelist. Vocabulary is the EFFECTIVE ApiOperation set (six primitives + eight derived verbs, #3543), not the authored six-value ApiMethod enum. | + --- @@ -691,6 +1019,14 @@ Enable package response | **locale** | `string` | ✅ | Locale code | | **labels** | `Record }>` | ✅ | Field labels keyed by field name | +### Nested Shape: `GetFieldLabelsResponse.labels[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string` | ✅ | Translated field label | +| **help** | `string` | optional | Translated help text | +| **options** | `Record` | optional | Translated option labels | + --- @@ -712,6 +1048,14 @@ Enable package response | :--- | :--- | :--- | :--- | | **locales** | `{ code: string; label: string; isDefault: boolean }[]` | ✅ | Available locales | +### Nested Shape: `GetLocalesResponse.locales[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `string` | ✅ | BCP-47 locale code (e.g., en-US, zh-CN) | +| **label** | `string` | ✅ | Locale label. Equals `code` on every serving surface today — the client names locales for its UI (#7634) | +| **isDefault** | `boolean` | optional (default: `false`) | Whether this is the default locale | + --- @@ -727,6 +1071,14 @@ Enable package response | **locale** | `string` | optional | Resolved response locale. Folded into the ETag so a language switch never returns a stale-locale 304 — metadata is translated *after* the cache validator check (issue #1319). | | **organizationId** | `string` | optional | Organization (tenant) scope for the read. Selects the org partition in the ADR-0005 overlay read order — org overlay wins over env-wide overlay wins over packaged artifact — exactly as on the uncached read (#9454). Also folded into the ETag, so a scope switch never returns a stale 304 from another scope's cached representation. Absent = environment-wide read. | +### Nested Shape: `GetMetaItemCachedRequest.cacheRequest` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ifNoneMatch** | `string` | optional | ETag value for conditional request (If-None-Match header) | +| **ifModifiedSince** | `string` | optional | Timestamp for conditional request (If-Modified-Since header) | +| **cacheControl** | `{ directives: Enum<'public' \| 'private' \| 'no-cache' \| 'no-store' \| 'must-revalidate' \| 'max-age'>[]; maxAge?: number; staleWhileRevalidate?: number; staleIfError?: number }` | optional | Client cache control preferences | + --- @@ -743,6 +1095,22 @@ Enable package response | **notModified** | `boolean` | optional (default: `false`) | True if resource has not been modified (304 response) | | **version** | `string` | optional | Metadata version identifier | +### Nested Shape: `GetMetaItemCachedResponse.etag` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **value** | `string` | ✅ | ETag value (hash or version identifier) | +| **weak** | `boolean` | optional (default: `false`) | Whether this is a weak ETag | + +### Nested Shape: `GetMetaItemCachedResponse.cacheControl` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **directives** | `Enum<'public' \| 'private' \| 'no-cache' \| 'no-store' \| 'must-revalidate' \| 'max-age'>[]` | ✅ | Cache control directives | +| **maxAge** | `number` | optional | Maximum cache age in seconds | +| **staleWhileRevalidate** | `number` | optional | Allow serving stale content while revalidating (seconds) | +| **staleIfError** | `number` | optional | Allow serving stale content on error (seconds) | + --- @@ -784,6 +1152,14 @@ Enable package response | **deletable** | `boolean` | ✅ | Whether deleting the overlay is permitted. Always present on this path. | | **resettable** | `boolean` | ✅ | Whether the item can be reset to its packaged default. Always present on this path. | +### Nested Shape: `GetMetaItemLayeredResponse._diagnostics` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **valid** | `boolean` | ✅ | Whether the metadata is valid | +| **errors** | `{ path: string; message: string; code?: string }[]` | optional | Validation errors | +| **warnings** | `{ path: string; message: string }[]` | optional | Validation warnings | + --- @@ -824,6 +1200,12 @@ Enable package response | **deletable** | `boolean` | optional | Whether deleting the overlay is permitted — false iff `lock` is `no-delete` or `full`. | | **resettable** | `boolean` | optional | Whether the item can be reset to its packaged default — true iff it is artifact-backed, i.e. there is a baseline to reset TO. | +### Nested Shape: `GetMetaItemResponse.sortability` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **fields** | `Record` | ✅ | Verdict per sortable-addressable column, keyed by field name. The domain is the served field map plus the always-provisioned `id`; a name absent from this map (an unknown field, a dotted path, an unprovisioned audit column) has no platform sort behind it and must get no sort affordance. | + --- @@ -872,6 +1254,24 @@ Enable package response | **types** | `string[]` | ✅ | Available metadata type names (e.g., "object", "plugin", "view") | | **entries** | `{ type: string; label: string; description?: string; filePatterns: string[]; … }[]` | optional | Enriched per-type registry entries (Phase 3a) | +### Nested Shape: `GetMetaTypesResponse.entries[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `string` | ✅ | Singular type identifier | +| **label** | `string` | ✅ | Human-readable label | +| **description** | `string` | optional | Brief description | +| **filePatterns** | `string[]` | ✅ | Glob patterns used to discover artifacts of this type | +| **supportsOverlay** | `boolean` | ✅ | Loader can merge per-org overlays on top of artifact | +| **allowOrgOverride** | `boolean` | ✅ | Per-org overlay writes accepted at runtime (may be env-elevated) | +| **allowRuntimeCreate** | `boolean` | ✅ | New artifacts of this type can be created via runtime API | +| **supportsVersioning** | `boolean` | ✅ | History is tracked for this type | +| **executionPinned** | `boolean` | ✅ | Runtime transactions pin a specific historical version_hash (ADR-0009) | +| **loadOrder** | `integer` | ✅ | Loading priority (lower = earlier) | +| **domain** | `Enum<'data' \| 'ui' \| 'automation' \| 'system' \| 'security' \| 'ai'>` | ✅ | Protocol domain | +| **overrideSource** | `Enum<'registry' \| 'env'>` | ✅ | Whether allowOrgOverride is set in the static registry or via OS_METADATA_WRITABLE env var | +| **createSeed** | `any` | optional | Authoritative minimal valid create seed for this type — Studio/CLI/API derive create defaults from it (single source of truth in @objectstack/spec). Absent for canvas-create types whose shape is built interactively. | + --- @@ -893,6 +1293,16 @@ Enable package response | :--- | :--- | :--- | :--- | | **preferences** | `{ email: boolean; push: boolean; inApp: boolean; digest: Enum<'none' \| 'daily' \| 'weekly'>; … }` | ✅ | Current notification preferences | +### Nested Shape: `GetNotificationPreferencesResponse.preferences` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **email** | `boolean` | optional (default: `true`) | Receive email notifications | +| **push** | `boolean` | optional (default: `true`) | Receive push notifications | +| **inApp** | `boolean` | optional (default: `true`) | Receive in-app notifications | +| **digest** | `Enum<'none' \| 'daily' \| 'weekly'>` | optional (default: `"none"`) | Email digest frequency | +| **channels** | `Record` | optional | Per-channel notification preferences | + --- @@ -917,6 +1327,30 @@ Enable package response | **permissions** | `{ allowCreate: boolean; allowRead: boolean; allowEdit: boolean; allowDelete: boolean; … }` | ✅ | Object-level permissions | | **fieldPermissions** | `Record` | optional | Field-level permissions keyed by field name | +### Nested Shape: `GetObjectPermissionsResponse.permissions` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **allowCreate** | `boolean` | optional (default: `false`) | Create permission | +| **allowRead** | `boolean` | optional (default: `false`) | Read permission | +| **allowEdit** | `boolean` | optional (default: `false`) | Edit permission | +| **allowDelete** | `boolean` | optional (default: `false`) | Delete permission | +| **allowExport** | `boolean` | optional | [#3544] User-level export axis over read (opt-in grant). true = export granted (still bounded by read); unset/false = no export. Merged most-permissively like the CRUD bits; NOT implied by viewAllRecords/modifyAllRecords. | +| **allowTransfer** | `boolean` | optional (default: `false`) | [RBAC-gated; ENFORCED now via insert/update owner_id guard, #3004] Change record ownership (assign/reassign/disown owner_id) | +| **allowRestore** | `boolean` | optional (default: `false`) | [RBAC-gated; operation pending M2] Restore from trash (Undelete) | +| **allowPurge** | `boolean` | optional (default: `false`) | [RBAC-gated; operation pending M2] Permanently delete (Hard Delete/GDPR) | +| **viewAllRecords** | `boolean` | optional (default: `false`) | View All Data (Bypass Sharing) | +| **modifyAllRecords** | `boolean` | optional (default: `false`) | Modify All Data (Bypass Sharing) — bypasses sharing rules and ownership on the objects record sharing enforces on; on an object with NO owner field sharing abstains, so the platform created_by write floor still applies (#6698). | +| **readScope** | `Enum<'own' \| 'own_and_reports' \| 'unit' \| 'unit_and_below' \| 'org'>` | optional | [ADR-0057 D1] Read depth: own\|unit\|unit_and_below\|org | +| **writeScope** | `Enum<'own' \| 'own_and_reports' \| 'unit' \| 'unit_and_below' \| 'org'>` | optional | [ADR-0057 D1] Write depth: own\|unit\|unit_and_below\|org | + +### Nested Shape: `GetObjectPermissionsResponse.fieldPermissions[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **readable** | `boolean` | optional (default: `true`) | Field read access | +| **editable** | `boolean` | optional (default: `false`) | Field edit access | + --- @@ -943,6 +1377,23 @@ Get package response | :--- | :--- | :--- | :--- | | **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Package details | +### Nested Shape: `GetPackageResponse.package` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Full package manifest | +| **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | +| **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | +| **installedAt** | `string` | optional | Installation timestamp | +| **updatedAt** | `string` | optional | Last update timestamp | +| **installedVersion** | `string` | optional | Currently installed version for quick access | +| **previousVersion** | `string` | optional | Version before the last upgrade | +| **statusChangedAt** | `string` | optional | Status change timestamp | +| **errorMessage** | `string` | optional | Error message when status is error | +| **settings** | `Record` | optional | User-provided configuration settings | +| **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history | +| **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package | + --- @@ -966,6 +1417,15 @@ Get package response | **channel** | `string` | ✅ | Channel name | | **members** | `{ userId: string; status: Enum<'online' \| 'away' \| 'busy' \| 'offline'>; lastSeen: string; metadata?: Record }[]` | ✅ | Active members and their presence state | +### Nested Shape: `GetPresenceResponse.members[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **userId** | `string` | ✅ | User identifier | +| **status** | `Enum<'online' \| 'away' \| 'busy' \| 'offline'>` | ✅ | Current presence status | +| **lastSeen** | `string` | ✅ | ISO 8601 datetime of last activity | +| **metadata** | `Record` | optional | Custom presence data (e.g., current page, custom status) | + --- @@ -989,6 +1449,21 @@ Get package response | **locale** | `string` | ✅ | Locale code | | **translations** | `{ objects?: Record; apps?: Record; messages?: Record; globalActions?: Record; … }` | ✅ | Translation data | +### Nested Shape: `GetTranslationsResponse.translations` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **objects** | `Record; … }>` | optional | Object translations keyed by object name | +| **apps** | `Record }>` | optional | App translations keyed by app name | +| **messages** | `Record` | optional | UI message translations keyed by message ID | +| **globalActions** | `Record` | optional | Global action translations keyed by action name | +| **dashboards** | `Record; widgets?: Record }>` | optional | Dashboard translations keyed by dashboard name | +| **pages** | `Record` | optional | Page translations keyed by page name | +| **flows** | `Record }>` | optional | Screen-flow translations keyed by flow name | +| **settings** | `Record; keys?: Record; … }>` | optional | Settings manifest translations keyed by namespace | +| **metadataForms** | `Record; fields?: Record }>` | optional | Translations for metadata-type configuration forms keyed by metadata type | +| **settingsCommon** | `{ sourceLabels?: object }` | optional | Cross-namespace Settings UI strings | + --- @@ -1026,6 +1501,182 @@ Get package response | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `GetUiViewResponse.list` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional | Internal view name (lowercase snake_case) | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **type** | `Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>` | optional (default: `"grid"`) | | +| **data** | `{ provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source configuration (defaults to "object" provider) | +| **columns** | `string[] \| { field: string; label?: string \| Record; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | ✅ | Fields to display as columns | +| **filter** | `{ field: string; operator?: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Filter criteria (JSON Rules) | +| **sort** | `string \| { field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | | +| **searchableFields** | `string[]` | optional | Fields enabled for search | +| **filterableFields** | `string[]` | optional | Legacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters | +| **resizable** | `boolean` | optional | Enable column resizing | +| **compactToolbar** | `boolean` | optional | Collapse Group/Color/Density/Hide-fields into a single View settings popover | +| **selection** | `{ type?: Enum<'none' \| 'single' \| 'multiple'> }` | optional | Row selection configuration | +| **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; view?: string; preventNavigation?: boolean; openNewTab?: boolean; … }` | optional | Configuration for item click navigation (page, drawer, modal, etc.) | +| **pagination** | `{ pageSize?: integer; pageSizeOptions?: integer[] }` | optional | Pagination configuration | +| **kanban** | `{ groupByField: string; summarizeField?: string; columns: string[] }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout | +| **calendar** | `{ startDateField: string; endDateField?: string; titleField: string; colorField?: string }` | optional | Calendar configuration — applies when the view renders as a calendar layout | +| **gantt** | `{ startDateField: string; endDateField: string; titleField: string; progressField?: string; … } & Record` | optional | Gantt-timeline configuration — applies when the view renders as a gantt layout | +| **gallery** | `{ coverField?: string; coverFit?: Enum<'cover' \| 'contain'>; cardSize?: Enum<'small' \| 'medium' \| 'large'>; titleField?: string; … }` | optional | Gallery/card view configuration | +| **timeline** | `{ startDateField: string; endDateField?: string; titleField: string; groupByField?: string; … }` | optional | Timeline view configuration | +| **chart** | `{ chartType?: Enum<'bar' \| 'line' \| 'pie' \| 'area' \| 'scatter'>; dataset: string; dimensions?: string[]; values: string[] }` | optional | List chart view configuration | +| **map** | `{ latitudeField?: string; longitudeField?: string; locationField?: string; titleField?: string; … }` | optional | Map configuration — applies when the view renders as a map layout | +| **tree** | `{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer } & Record` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout | +| **description** | `string \| Record` | optional | View description for documentation/tooltips | +| **sharing** | `{ type?: Enum<'personal' \| 'collaborative'>; lockedBy?: string }` | optional | View sharing and access configuration | +| **rowHeight** | `Enum<'compact' \| 'short' \| 'medium' \| 'tall' \| 'extra_tall'>` | optional | Row height / density setting | +| **grouping** | `{ fields: object[] }` | optional | Group records by one or more fields | +| **rowColor** | `{ field: string; colors?: Record }` | optional | Color rows based on field value | +| **hiddenFields** | `string[]` | optional | Fields to hide in this specific view | +| **fieldOrder** | `string[]` | optional | Explicit field display order for this view | +| **rowActions** | `string[]` | optional | Actions available for individual row items | +| **bulkActions** | `string[]` | optional | Actions available when multiple rows are selected | +| **bulkActionDefs** | `{ name: string; label?: string; icon?: string; variant?: Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'outline'>; … }[]` | optional | Rich bulk action definitions (schema-driven, executed via BulkActionDialog). Use a def for a mass data-plane mutation ('update' with a `patch` / 'delete') that no action expresses, or for an `operation: 'custom'` + `execution: 'aggregate'` entry (objectui#3139) that dispatches the action it NAMES once for the whole selection — the renderer injects `params._selectedIds: string[]` (read that on the server, not `recordId`) so a single call can produce one aggregate artifact (zip of QR codes, merged PDF, batch print). Aggregate results are all-or-nothing: a handler that cannot cover the whole selection must reject, and per-row retry is replaced by re-running the action. `batchSize` does not apply (the call is never chunked); set `maxRecords` on defs whose server work is expensive. For the PER-RECORD dispatch use `bulkActions: ['']` instead — the bare-string form, promoted with the action's own label, params and `visible`; a 'custom' def without `execution: 'aggregate'` has no dispatcher and is refused at parse time (#4457). Toolbar url/api actions can also interpolate the current selection via `${ctx.selection.ids}` / `${ctx.selection.count}`. | +| **conditionalFormatting** | `{ condition: string \| object; style: Record }[]` | optional | Conditional formatting rules for list rows | +| **inlineEdit** | `boolean` | optional | Allow inline editing of records directly in the list view | +| **exportOptions** | `Enum<'csv' \| 'xlsx' \| 'json'>[] \| { formats?: Enum<'csv' \| 'xlsx' \| 'json'>[]; maxRecords?: integer; includeHeaders?: boolean; fileNamePrefix?: string; … }` | optional | Export configuration for the list toolbar export menu: `{ formats?, maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }`. A bare format array is the legacy spelling and lifts to `{ formats: [...] }` at parse. | +| **userActions** | `{ sort?: boolean; search?: boolean; filter?: boolean; refresh?: boolean; … }` | optional | User action toggles for the view toolbar | +| **appearance** | `{ showDescription?: boolean; allowedVisualizations?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>[] }` | optional | Appearance and visualization configuration | +| **tabs** | `{ name: string; label?: string \| Record; icon?: string; view?: string; … }[]` | optional | Tab definitions for multi-tab view interface | +| **addRecord** | `{ enabled?: boolean; position?: Enum<'top' \| 'bottom' \| 'both'>; mode?: Enum<'inline' \| 'form' \| 'modal'>; formView?: string }` | optional | Add record entry point configuration | +| **showRecordCount** | `boolean` | optional | Show record count at the bottom of the list | +| **allowPrinting** | `boolean` | optional | Allow users to print the view | +| **emptyState** | `{ title?: string \| Record; message?: string \| Record; icon?: string }` | optional | Empty state configuration when no records found | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes for the list view | +| **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **striped** | `never` | optional | [REMOVED] `view.striped` 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, so authoring it was a parse-clean no-op. There is no authorable striped-rows switch; delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **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. | +| **userFilters** | `{ element?: Enum<'dropdown' \| 'toggle'>; fields?: object[] }` | optional | | + +### Nested Shape: `GetUiViewResponse.form` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'simple' \| 'tabbed' \| 'wizard' \| 'split' \| 'drawer' \| 'modal'>` | optional (default: `"simple"`) | | +| **layout** | `Enum<'vertical' \| 'horizontal' \| 'inline' \| 'grid'>` | optional | Field layout direction | +| **columns** | `integer` | optional | Number of columns for the form body | +| **title** | `string` | optional | Form title | +| **description** | `string` | optional | Form description | +| **defaultTab** | `string` | optional | Initially active tab (tabbed forms) | +| **tabPosition** | `Enum<'top' \| 'bottom' \| 'left' \| 'right'>` | optional | Tab strip position (tabbed forms) | +| **allowSkip** | `boolean` | optional | Allow skipping steps (wizard forms) | +| **showStepIndicator** | `boolean` | optional | Show the step indicator (wizard forms) | +| **splitDirection** | `Enum<'horizontal' \| 'vertical'>` | optional | Split orientation (split forms) | +| **splitSize** | `number` | optional | Primary split panel size, % (split forms) | +| **splitResizable** | `boolean` | optional | Whether the split is resizable (split forms) | +| **drawerSide** | `Enum<'top' \| 'bottom' \| 'left' \| 'right'>` | optional | Drawer side (drawer forms) | +| **drawerWidth** | `string` | optional | [DEPRECATED → size buckets] Drawer width, e.g. "480px". A pixel width cannot be chosen without knowing the client viewport — the renderer derives it. | +| **modalSize** | `Enum<'sm' \| 'default' \| 'lg' \| 'xl' \| 'full'>` | optional | Modal size (modal forms) | +| **data** | `{ provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source configuration (defaults to "object" provider) | +| **sections** | `{ name?: string; label?: string \| Record; description?: string; collapsible?: boolean; … }[]` | optional | | +| **groups** | `{ name?: string; label?: string \| Record; description?: string; collapsible?: boolean; … }[]` | optional | [LEGACY ALIAS → `sections`] Accepted for back-compat and folded onto `sections` at parse; `sections` wins when both are present. Prefer `sections`. | +| **subforms** | `{ childObject: string; relationshipField?: string; columns?: any[]; amountField?: string; … }[]` | optional | Inline master-detail child collections | +| **defaultSort** | `never` | optional | [REMOVED] `form.defaultSort` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — nothing read it: a related list inside a form sorts by its own list view's `sort`. Delete the key and set the sort on the related list view instead. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **sharing** | `{ enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … }` | optional | Public sharing configuration for this form | +| **submitBehavior** | `{ kind: 'thank-you'; title?: string; message?: string } \| { kind: 'redirect'; url: string; delayMs?: integer } \| { kind: 'continue' } \| { kind: 'next-record' }` | optional | Post-submit behavior. On the `redirect` arm, `url` is relative-only and interpolates only declared record fields as `{{record.field_name}}`, URL-escaped (ruled 2026-08-11, #7496). | +| **buttons** | `{ submit?: object; cancel?: object; reset?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm (framework#1894 / #2998). | +| **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: `GetUiViewResponse.listViews[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional | Internal view name (lowercase snake_case) | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **type** | `Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>` | optional (default: `"grid"`) | | +| **data** | `{ provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source configuration (defaults to "object" provider) | +| **columns** | `string[] \| { field: string; label?: string \| Record; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | ✅ | Fields to display as columns | +| **filter** | `{ field: string; operator?: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Filter criteria (JSON Rules) | +| **sort** | `string \| { field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | | +| **searchableFields** | `string[]` | optional | Fields enabled for search | +| **filterableFields** | `string[]` | optional | Legacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters | +| **resizable** | `boolean` | optional | Enable column resizing | +| **compactToolbar** | `boolean` | optional | Collapse Group/Color/Density/Hide-fields into a single View settings popover | +| **selection** | `{ type?: Enum<'none' \| 'single' \| 'multiple'> }` | optional | Row selection configuration | +| **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; view?: string; preventNavigation?: boolean; openNewTab?: boolean; … }` | optional | Configuration for item click navigation (page, drawer, modal, etc.) | +| **pagination** | `{ pageSize?: integer; pageSizeOptions?: integer[] }` | optional | Pagination configuration | +| **kanban** | `{ groupByField: string; summarizeField?: string; columns: string[] }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout | +| **calendar** | `{ startDateField: string; endDateField?: string; titleField: string; colorField?: string }` | optional | Calendar configuration — applies when the view renders as a calendar layout | +| **gantt** | `{ startDateField: string; endDateField: string; titleField: string; progressField?: string; … } & Record` | optional | Gantt-timeline configuration — applies when the view renders as a gantt layout | +| **gallery** | `{ coverField?: string; coverFit?: Enum<'cover' \| 'contain'>; cardSize?: Enum<'small' \| 'medium' \| 'large'>; titleField?: string; … }` | optional | Gallery/card view configuration | +| **timeline** | `{ startDateField: string; endDateField?: string; titleField: string; groupByField?: string; … }` | optional | Timeline view configuration | +| **chart** | `{ chartType?: Enum<'bar' \| 'line' \| 'pie' \| 'area' \| 'scatter'>; dataset: string; dimensions?: string[]; values: string[] }` | optional | List chart view configuration | +| **map** | `{ latitudeField?: string; longitudeField?: string; locationField?: string; titleField?: string; … }` | optional | Map configuration — applies when the view renders as a map layout | +| **tree** | `{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer } & Record` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout | +| **description** | `string \| Record` | optional | View description for documentation/tooltips | +| **sharing** | `{ type?: Enum<'personal' \| 'collaborative'>; lockedBy?: string }` | optional | View sharing and access configuration | +| **rowHeight** | `Enum<'compact' \| 'short' \| 'medium' \| 'tall' \| 'extra_tall'>` | optional | Row height / density setting | +| **grouping** | `{ fields: object[] }` | optional | Group records by one or more fields | +| **rowColor** | `{ field: string; colors?: Record }` | optional | Color rows based on field value | +| **hiddenFields** | `string[]` | optional | Fields to hide in this specific view | +| **fieldOrder** | `string[]` | optional | Explicit field display order for this view | +| **rowActions** | `string[]` | optional | Actions available for individual row items | +| **bulkActions** | `string[]` | optional | Actions available when multiple rows are selected | +| **bulkActionDefs** | `{ name: string; label?: string; icon?: string; variant?: Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'outline'>; … }[]` | optional | Rich bulk action definitions (schema-driven, executed via BulkActionDialog). Use a def for a mass data-plane mutation ('update' with a `patch` / 'delete') that no action expresses, or for an `operation: 'custom'` + `execution: 'aggregate'` entry (objectui#3139) that dispatches the action it NAMES once for the whole selection — the renderer injects `params._selectedIds: string[]` (read that on the server, not `recordId`) so a single call can produce one aggregate artifact (zip of QR codes, merged PDF, batch print). Aggregate results are all-or-nothing: a handler that cannot cover the whole selection must reject, and per-row retry is replaced by re-running the action. `batchSize` does not apply (the call is never chunked); set `maxRecords` on defs whose server work is expensive. For the PER-RECORD dispatch use `bulkActions: ['']` instead — the bare-string form, promoted with the action's own label, params and `visible`; a 'custom' def without `execution: 'aggregate'` has no dispatcher and is refused at parse time (#4457). Toolbar url/api actions can also interpolate the current selection via `${ctx.selection.ids}` / `${ctx.selection.count}`. | +| **conditionalFormatting** | `{ condition: string \| object; style: Record }[]` | optional | Conditional formatting rules for list rows | +| **inlineEdit** | `boolean` | optional | Allow inline editing of records directly in the list view | +| **exportOptions** | `Enum<'csv' \| 'xlsx' \| 'json'>[] \| { formats?: Enum<'csv' \| 'xlsx' \| 'json'>[]; maxRecords?: integer; includeHeaders?: boolean; fileNamePrefix?: string; … }` | optional | Export configuration for the list toolbar export menu: `{ formats?, maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }`. A bare format array is the legacy spelling and lifts to `{ formats: [...] }` at parse. | +| **userActions** | `{ sort?: boolean; search?: boolean; filter?: boolean; refresh?: boolean; … }` | optional | User action toggles for the view toolbar | +| **appearance** | `{ showDescription?: boolean; allowedVisualizations?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>[] }` | optional | Appearance and visualization configuration | +| **tabs** | `{ name: string; label?: string \| Record; icon?: string; view?: string; … }[]` | optional | Tab definitions for multi-tab view interface | +| **addRecord** | `{ enabled?: boolean; position?: Enum<'top' \| 'bottom' \| 'both'>; mode?: Enum<'inline' \| 'form' \| 'modal'>; formView?: string }` | optional | Add record entry point configuration | +| **showRecordCount** | `boolean` | optional | Show record count at the bottom of the list | +| **allowPrinting** | `boolean` | optional | Allow users to print the view | +| **emptyState** | `{ title?: string \| Record; message?: string \| Record; icon?: string }` | optional | Empty state configuration when no records found | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes for the list view | +| **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **striped** | `never` | optional | [REMOVED] `view.striped` 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, so authoring it was a parse-clean no-op. There is no authorable striped-rows switch; delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **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. | +| **userFilters** | `{ element?: Enum<'dropdown' \| 'toggle'>; fields?: object[] }` | optional | | + +### Nested Shape: `GetUiViewResponse.formViews[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'simple' \| 'tabbed' \| 'wizard' \| 'split' \| 'drawer' \| 'modal'>` | optional (default: `"simple"`) | | +| **layout** | `Enum<'vertical' \| 'horizontal' \| 'inline' \| 'grid'>` | optional | Field layout direction | +| **columns** | `integer` | optional | Number of columns for the form body | +| **title** | `string` | optional | Form title | +| **description** | `string` | optional | Form description | +| **defaultTab** | `string` | optional | Initially active tab (tabbed forms) | +| **tabPosition** | `Enum<'top' \| 'bottom' \| 'left' \| 'right'>` | optional | Tab strip position (tabbed forms) | +| **allowSkip** | `boolean` | optional | Allow skipping steps (wizard forms) | +| **showStepIndicator** | `boolean` | optional | Show the step indicator (wizard forms) | +| **splitDirection** | `Enum<'horizontal' \| 'vertical'>` | optional | Split orientation (split forms) | +| **splitSize** | `number` | optional | Primary split panel size, % (split forms) | +| **splitResizable** | `boolean` | optional | Whether the split is resizable (split forms) | +| **drawerSide** | `Enum<'top' \| 'bottom' \| 'left' \| 'right'>` | optional | Drawer side (drawer forms) | +| **drawerWidth** | `string` | optional | [DEPRECATED → size buckets] Drawer width, e.g. "480px". A pixel width cannot be chosen without knowing the client viewport — the renderer derives it. | +| **modalSize** | `Enum<'sm' \| 'default' \| 'lg' \| 'xl' \| 'full'>` | optional | Modal size (modal forms) | +| **data** | `{ provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source configuration (defaults to "object" provider) | +| **sections** | `{ name?: string; label?: string \| Record; description?: string; collapsible?: boolean; … }[]` | optional | | +| **groups** | `{ name?: string; label?: string \| Record; description?: string; collapsible?: boolean; … }[]` | optional | [LEGACY ALIAS → `sections`] Accepted for back-compat and folded onto `sections` at parse; `sections` wins when both are present. Prefer `sections`. | +| **subforms** | `{ childObject: string; relationshipField?: string; columns?: any[]; amountField?: string; … }[]` | optional | Inline master-detail child collections | +| **defaultSort** | `never` | optional | [REMOVED] `form.defaultSort` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — nothing read it: a related list inside a form sorts by its own list view's `sort`. Delete the key and set the sort on the related list view instead. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **sharing** | `{ enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … }` | optional | Public sharing configuration for this form | +| **submitBehavior** | `{ kind: 'thank-you'; title?: string; message?: string } \| { kind: 'redirect'; url: string; delayMs?: integer } \| { kind: 'continue' } \| { kind: 'next-record' }` | optional | Post-submit behavior. On the `redirect` arm, `url` is relative-only and interpolates only declared record fields as `{{record.field_name}}`, URL-escaped (ruled 2026-08-11, #7496). | +| **buttons** | `{ submit?: object; cancel?: object; reset?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm (framework#1894 / #2998). | +| **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: `GetUiViewResponse.protection` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | ✅ | Lock policy — none \| no-overlay \| no-delete \| full. | +| **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | +| **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | + --- @@ -1063,6 +1714,35 @@ Install package request | **enableOnInstall** | `boolean` | optional (default: `true`) | Whether to enable immediately after install | | **platformVersion** | `string` | optional | Current platform version for compatibility verification | +### Nested Shape: `InstallPackageRequest.manifest` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique package identifier (reverse domain style) | +| **namespace** | `string` | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") | +| **defaultDatasource** | `string` | optional (default: `"default"`) | Default datasource for all objects in this package | +| **version** | `string` | ✅ | Package version (semantic versioning) | +| **type** | `Enum<'plugin' \| 'ui' \| 'driver' \| 'server' \| 'app' \| 'theme' \| 'agent' \| 'objectql' \| …>` | ✅ | Type of package | +| **scope** | `Enum<'cloud' \| 'system' \| 'project'>` | optional (default: `"project"`) | Deployment scope: cloud \| system \| project | +| **name** | `string` | ✅ | Human-readable package name | +| **description** | `string` | optional | Package description | +| **permissions** | `string[] \| { services?: string[]; hooks?: string[]; network?: string[]; fs?: string[] }` | optional | Required permissions: legacy string[] or structured plugin block (ADR-0025 §3.2) | +| **objects** | `string[]` | optional | Glob patterns for ObjectQL schemas files | +| **datasources** | `string[]` | optional | Glob patterns for Datasource definitions | +| **dependencies** | `Record` | optional | Package dependencies | +| **configuration** | `{ title?: string; properties: Record }` | optional | Plugin configuration settings | +| **contributes** | `{ kinds?: object[]; routes?: object[] }` | optional | Platform contributions | +| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Initial seed data (prefer top-level data field) | +| **capabilities** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability | +| **extensions** | `Record` | optional | Extension points and contributions | +| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: (object \| … +8 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages | +| **loading** | `never` | optional | [REMOVED] `manifest.loading` was removed in @objectstack/spec 17.0.0 (#4914, ADR-0049 enforce-or-remove) — the entire block (`strategy`, `preload`, `codeSplitting`, `dynamicImport`, `initialization`, `dependencyResolution`, `hotReload`, `caching`, `sandboxing`, `monitoring`) had no runtime reader in any repo, so authoring it configured nothing. Delete the key. Plugins are composed at boot — `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder`); the set is fixed until the process restarts. ⚠️ `loading.sandboxing` in particular never isolated anything: it did not run plugins in a process, vm, iframe or web-worker, and `allowedServices` gated no call. If you were relying on it for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced. | +| **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | +| **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) | +| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier (ADR-0025 §3.6) | +| **packaging** | `Enum<'bundled' \| 'manifest-deps'>` | optional | Dependency packaging strategy (ADR-0025 §3.3) | +| **integrity** | `Record` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) | + --- @@ -1078,6 +1758,33 @@ Install package response | **message** | `string` | optional | Installation status message | | **dependencyResolution** | `{ dependencies: object[]; canProceed: boolean; requiredActions: object[]; installOrder: string[]; … }` | optional | Dependency resolution result from install analysis | +### Nested Shape: `InstallPackageResponse.package` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Full package manifest | +| **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | +| **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | +| **installedAt** | `string` | optional | Installation timestamp | +| **updatedAt** | `string` | optional | Last update timestamp | +| **installedVersion** | `string` | optional | Currently installed version for quick access | +| **previousVersion** | `string` | optional | Version before the last upgrade | +| **statusChangedAt** | `string` | optional | Status change timestamp | +| **errorMessage** | `string` | optional | Error message when status is error | +| **settings** | `Record` | optional | User-provided configuration settings | +| **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history | +| **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package | + +### Nested Shape: `InstallPackageResponse.dependencyResolution` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **dependencies** | `{ packageId: string; requiredRange: string; resolvedVersion?: string; installedVersion?: string; … }[]` | ✅ | Resolution result for each dependency | +| **canProceed** | `boolean` | ✅ | Whether installation can proceed | +| **requiredActions** | `{ type: Enum<'install' \| 'upgrade' \| 'confirm_conflict'>; packageId: string; description: string }[]` | ✅ | Actions required before proceeding | +| **installOrder** | `string[]` | ✅ | Topologically sorted package IDs for installation | +| **circularDependencies** | `string[][]` | optional | Circular dependency chains detected (e.g. [["A", "B", "A"]]) | + --- @@ -1102,6 +1809,19 @@ Install package response | :--- | :--- | :--- | :--- | | **conversations** | `{ id: string; title?: string; agentId?: string; userId?: string; … }[]` | ✅ | Matching conversations | +### Nested Shape: `ListAiConversationsResponse.conversations[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Conversation id | +| **title** | `string` | optional | Title / summary | +| **agentId** | `string` | optional | Agent this conversation is bound to | +| **userId** | `string` | optional | Owning user | +| **messages** | `({ role: Enum<'system' \| 'user' \| 'assistant' \| 'tool'>; content?: any; parts?: any[] } & Record)[]` | ✅ | Message history | +| **createdAt** | `string` | ✅ | Creation timestamp (ISO 8601) | +| **updatedAt** | `string` | ✅ | Last update timestamp (ISO 8601) | +| **metadata** | `Record` | optional | Conversation metadata | + --- @@ -1127,6 +1847,26 @@ Install package response | **items** | `{ id: string; object_name: string; action_name: string; tool_name: string; … }[]` | ✅ | Queued actions, newest first | | **total** | `number` | ✅ | Number of rows returned (not a total across pages) | +### Nested Shape: `ListAiPendingActionsResponse.items[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Pending action id | +| **object_name** | `string` | ✅ | Object the action targets | +| **action_name** | `string` | ✅ | Action name | +| **tool_name** | `string` | ✅ | Tool that would execute it | +| **tool_input** | `string` | ✅ | Serialized tool input | +| **status** | `Enum<'pending' \| 'approved' \| 'executed' \| 'failed' \| 'rejected'>` | ✅ | Lifecycle status | +| **result** | `string` | optional | Serialized result, once executed | +| **error** | `string` | optional | Failure message, when status is failed | +| **rejection_reason** | `string` | optional | Reason given at rejection | +| **conversation_id** | `string` | optional | Conversation that proposed it | +| **message_id** | `string` | optional | Message that proposed it | +| **proposed_by** | `string` | optional | Actor that proposed it | +| **decided_by** | `string` | optional | Actor that approved or rejected it | +| **proposed_at** | `string` | ✅ | Proposal timestamp (ISO 8601) | +| **decided_at** | `string` | optional | Decision timestamp (ISO 8601) | + --- @@ -1154,6 +1894,19 @@ Install package response | **unreadCount** | `number` | ✅ | Total number of unread notifications | | **cursor** | `never` | optional | [REMOVED] `cursor` was removed from GET /api/v1/notifications in @objectstack/spec 17 (#6361, ADR-0049) — it was declared on the request AND the response and honoured on neither: the server reads only `read`/`type`/`limit`, and no emit site ever wrote the response key, so a caller paginating by it re-read the first window forever with no error and no 400. Delete the key; the `cursor` argument of `client.notifications.list()` was removed with it. This route is NOT paginated — it answers the newest `limit` notifications and stops, so ask for a bigger window (`limit`, clamped by the server into 1..200) instead of a next page. A first-class inbox cursor, if ever built, will be a response-minted opaque token, not this key. | +### Nested Shape: `ListNotificationsResponse.notifications[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Notification ID | +| **type** | `string` | ✅ | Notification type | +| **title** | `string` | ✅ | Notification title | +| **body** | `string` | ✅ | Notification body text | +| **read** | `boolean` | optional (default: `false`) | Whether notification has been read | +| **data** | `Record` | optional | Additional notification data | +| **actionUrl** | `string` | optional | URL to navigate to when clicked | +| **createdAt** | `string` | ✅ | When notification was created | + --- @@ -1183,6 +1936,25 @@ List packages response | **packages** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }[]` | ✅ | List of installed packages | | **total** | `number` | ✅ | Total package count | +### Nested Shape: `ListPackagesResponse.packages[number]` + +Installed package with runtime lifecycle state + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Full package manifest | +| **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | +| **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | +| **installedAt** | `string` | optional | Installation timestamp | +| **updatedAt** | `string` | optional | Last update timestamp | +| **installedVersion** | `string` | optional | Currently installed version for quick access | +| **previousVersion** | `string` | optional | Version before the last upgrade | +| **statusChangedAt** | `string` | optional | Status change timestamp | +| **errorMessage** | `string` | optional | Error message when status is error | +| **settings** | `Record` | optional | User-provided configuration settings | +| **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history | +| **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package | + --- @@ -1261,6 +2033,14 @@ List packages response | **digest** | `Enum<'none' \| 'daily' \| 'weekly'>` | optional (default: `"none"`) | Email digest frequency | | **channels** | `Record` | optional | Per-channel notification preferences | +### Nested Shape: `NotificationPreferences.channels[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | Whether this channel is enabled | +| **email** | `boolean` | optional | Override email setting | +| **push** | `boolean` | optional | Override push setting | + --- @@ -1295,6 +2075,43 @@ List packages response | **advisories** | `{ rule: string; path: string; where: string; message: string; … }[]` | optional | Non-gating findings from the #4463 runtime authoring gate — the same shared author-time rules `os validate` / `os build` / `os lint` run, applied to the DRAFT body this promotion carried to `active` (#9176, the same key `SaveMetaItemResponseSchema` carries, because the gate runs on both write doors by #4463 D1). The promotion SUCCEEDED; these are what the gate has to say about it anyway. Present ONLY when at least one advisory was raised — an empty array is never emitted, so a clean publish's response bytes are unchanged and absence means "nothing to report", never "the gate did not run". Advisory by construction: every entry has `severity` `warning` or `info`, because an `error` finding refuses the promotion and arrives as the 422 `invalid_metadata` envelope instead of here. A caller that ignores this key behaves exactly as before. This door is the one Studio's designer takes on every edit (draft save, then publish), and a Studio / MCP / AI author has no CLI at all — which is the gap #4463 exists to close. | | **message** | `string` | optional | Human-readable receipt, e.g. `Published draft — type=view, name=cases [seq=3]`. The producer sets it on every publish today; it stays optional to match the producer's own signature and its `SaveMetaItemResponse` twin, and because an absent human-readable string strips no data — the failure mode #5745 exists to prevent. | +### Nested Shape: `PublishMetaItemResponse.seedApplied` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **success** | `boolean` | ✅ | False when the seed rows did not fully land. The publish itself still succeeded — check this rather than assuming data went live. | +| **inserted** | `integer` | ✅ | Rows created by the externalId-keyed upsert. | +| **updated** | `integer` | ✅ | Rows updated by the externalId-keyed upsert. | +| **error** | `string` | optional | Single failure message, present when the seed apply threw before the loader ran (including "no readable seed bodies"). | +| **errors** | `any[]` | optional | Per-record failures reported by the seed loader. Present only when the loader ran and returned a non-empty error list. | + +### Nested Shape: `PublishMetaItemResponse.materializeApplied` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **success** | `boolean` | ✅ | False when the materializer threw or reported failure; the publish still succeeded. | +| **inserted** | `integer` | ✅ | Data-plane rows created by the materializer. | +| **updated** | `integer` | ✅ | Data-plane rows updated by the materializer. | +| **error** | `string` | optional | Materializer failure message, present only when `success` is false. | + +### Nested Shape: `PublishMetaItemResponse.projectionApplied` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **success** | `boolean` | ✅ | False when the projector threw; the metadata promotion itself still succeeded. | +| **error** | `string` | optional | Projector failure message, present only when `success` is false. | + +### Nested Shape: `PublishMetaItemResponse.advisories[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **rule** | `string` | ✅ | Stable diagnostic rule id (`flow-multi-write-unfiltered`, `approval-expression-invalid`, …). Machine-readable and stable across releases — the key a renderer groups or suppresses by. | +| **path** | `string` | ✅ | Config path inside the SUBMITTED body (`flows[0].nodes[1].config.multi`), so an editor can jump to the offending key. May be empty when the finding is about the item as a whole. For the collection-resident write types (`object` / `permission` / `book`) the TOP-LEVEL collection entry is keyed by NAME (`objects.acme_invoice.sharingModel`), never by an array index — the gate evaluates against a private per-write snapshot whose indexes no caller can resolve (#10064). Every other write type is the sole member of its own collection, so its `[0]` is trivially stable and stays positional (`flows[0]...`), as do nested positions inside one named item (`objects.acme_invoice.indexes[1]`), which index the author's own document. | +| **where** | `string` | ✅ | Human-readable location — `flow "leave_approval" · node "approve"`. Prose for a person; use `path` for anything mechanical. | +| **message** | `string` | ✅ | What is wrong, in the rule author's own words. | +| **hint** | `string` | ✅ | How to fix it. | +| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | ✅ | How the gate treated this finding. `error` means the write was REFUSED (these appear on the 422, never on a 2xx); `warning` / `info` are advisory — the write succeeded and the finding is FYI. | + --- @@ -1318,6 +2135,45 @@ List packages response | **unhideError** | `string` | optional | Present when the ADR-0045 visibility flip failed (wholly or partway): the drafts ARE published, but apps still stored `_unpublished: true` stay externally unobservable. Client-facing text only — undeclared driver text is withheld per ADR-0112 (#8516); the full cause is in the server log. The route is idempotent: re-run it once the cause is resolved. | | **rebindError** | `string` | optional | Present when the post-publish `metadata:reloaded` announce failed: everything is published and stored, but boot-cached consumers keep the pre-publish view until re-run or restart (a newly published record-triggered flow does not bind its trigger). Client-facing text only, same ADR-0112 withhold as `unhideError` (#8516). | +### Nested Shape: `PublishPackageDraftsResponse.published[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `string` | ✅ | Metadata type of the promoted draft (canonical singular). | +| **name** | `string` | ✅ | Item name of the promoted draft. | +| **version** | `string` | ✅ | Content hash of the just-promoted body — the same ADR-0008 optimistic-concurrency token the single-item doors return: echo it back as `If-Match` on the next write to this item. Opaque to callers; currently `sha256:<64 hex chars>`, but the format is not part of this contract. | +| **advisories** | `{ rule: string; path: string; where: string; message: string; … }[]` | optional | Non-gating findings the #4463 runtime authoring gate raised against THIS draft's promotion (#9343 — the same element shape and the same omitted-when-empty discipline as `PublishMetaItemResponseSchema.advisories`, riding each element rather than a parallel top-level map). Present ONLY when at least one finding was raised — an empty array is never emitted, so an advisory-free batch's response bytes are unchanged. Advisory by construction: every entry is `warning`/`info`, because an `error` finding refuses the promotion and — the batch being all-or-nothing — aborts the whole batch as `failed[]` instead; `failed[]` elements never carry this key. | + +### Nested Shape: `PublishPackageDraftsResponse.failed[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `string` | ✅ | Metadata type of the item that did not publish. | +| **name** | `string` | ✅ | Item name. | +| **error** | `string` | ✅ | What refused it. On a rollback, the causal item carries its real error and every sibling carries the all-or-nothing explanation. A refusal that produced structured findings states a one-sentence HEADLINE here (what failed, where, which rules, how many); the per-path detail rides `issues[]` instead of being restated in this string (#10524 — consumers rendering both channels were showing every finding twice). | +| **code** | `string` | optional | Machine code for the refusal class (SCREAMING_SNAKE, ADR-0112 vocabulary) — e.g. a pre-flight violation code, or BATCH_ABORTED on the non-causal items of a rolled-back batch. | +| **issues** | `{ rule: string; path: string; where: string; message: string; … }[]` | optional | The structured findings behind the refusal, when the refusing error carried them — today the #4463 author-time gate's INVALID_METADATA refusal on the causal item. The producer has emitted this key since #8333; declaring it (#10524) is what lets a typed consumer read it back, and what lets `error` stay a headline without losing the per-path detail. Same element shape as `published[].advisories` and the single-item 422's `issues[]` (#4717 — one dialect, declared once). Present ONLY on the causal item and only when the refusal produced structured findings; BATCH_ABORTED siblings never carry it. Absent means "this refusal carried no structured findings", never "no problems". | + +### Nested Shape: `PublishPackageDraftsResponse.seedApplied` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **success** | `boolean` | ✅ | False when the seed rows did not fully land. The publish itself still succeeded — check this rather than assuming data went live. | +| **inserted** | `integer` | optional | Rows created by the externalId-keyed upsert. Optional ONLY because the route-level fallback producer (custom protocols that do not self-apply) reports early failures without counters; the in-batch producer always emits both counters. | +| **updated** | `integer` | optional | Rows updated by the externalId-keyed upsert. Same optionality rationale as `inserted`. | +| **error** | `string` | optional | Single failure message, present when the apply failed before the loader ran (including "no readable seed bodies"). When the failure is the seed bodies' own schema refusal, this is a one-sentence headline and the per-path detail rides `issues[]` (#10524). | +| **errors** | `any[]` | optional | Per-record failures reported by the seed loader, plus any seed-body read failures. May be present and empty on a clean load. | +| **issues** | `{ path: string; message: string; code?: string }[]` | optional | Structured spec-validation findings behind `error`, present when the apply was refused by the seed bodies' own schema — the declared 422 `seedRequestValidationError` mints (#8443). The per-path detail lives HERE, once; `error` stays a one-sentence headline (#10524). Absent on non-validation failures (driver faults, unreadable bodies), whose whole story is `error` / `errors[]`. | + +### Nested Shape: `PublishPackageDraftsResponse.materializeApplied` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **success** | `boolean` | ✅ | False when any item's materializer failed; the publish still succeeded. | +| **inserted** | `integer` | ✅ | Data-plane rows created across the batch. | +| **updated** | `integer` | ✅ | Data-plane rows updated across the batch. | +| **failures** | `{ type: string; name: string; error: string }[]` | ✅ | Each item whose data-plane projection did NOT land — named per-item, unlike the single-item door's scalar `error`, because one aggregate boolean over N items would hide WHICH ones never went live. | + --- @@ -1497,6 +2353,24 @@ List packages response | **advisories** | `{ rule: string; path: string; where: string; message: string; … }[]` | optional | Non-gating findings from the #4463 runtime authoring gate — the same shared author-time rules `os validate` / `os build` / `os lint` run, applied to this body on its way to `active`. The write SUCCEEDED; these are what the gate has to say about it anyway (#4717, closing #4463 D3). Present ONLY when at least one advisory was raised — an empty array is never emitted, so a clean save's response bytes are unchanged and absence means "nothing to report", never "the gate did not run". Advisory by construction: every entry has `severity` `warning` or `info`, because an `error` finding refuses the write and arrives as the 422 `invalid_metadata` envelope instead of here. A caller that ignores this key behaves exactly as before. Runtime-only: the CLI surfaces the same findings on its own stdout, and a Studio / MCP / AI author has no CLI at all, which is the gap #4463 exists to close. The gate runs on both write doors (#4463 D1), and both report: `POST /meta/:type/:name/publish` carries the same key on `PublishMetaItemResponseSchema` (#9176). | | **message** | `string` | optional | | +### Nested Shape: `SaveMetaItemResponse.projectionApplied` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **success** | `boolean` | ✅ | False when the projector threw; the metadata write itself still succeeded. | +| **error** | `string` | optional | Projector failure message, present only when `success` is false. | + +### Nested Shape: `SaveMetaItemResponse.advisories[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **rule** | `string` | ✅ | Stable diagnostic rule id (`flow-multi-write-unfiltered`, `approval-expression-invalid`, …). Machine-readable and stable across releases — the key a renderer groups or suppresses by. | +| **path** | `string` | ✅ | Config path inside the SUBMITTED body (`flows[0].nodes[1].config.multi`), so an editor can jump to the offending key. May be empty when the finding is about the item as a whole. For the collection-resident write types (`object` / `permission` / `book`) the TOP-LEVEL collection entry is keyed by NAME (`objects.acme_invoice.sharingModel`), never by an array index — the gate evaluates against a private per-write snapshot whose indexes no caller can resolve (#10064). Every other write type is the sole member of its own collection, so its `[0]` is trivially stable and stays positional (`flows[0]...`), as do nested positions inside one named item (`objects.acme_invoice.indexes[1]`), which index the author's own document. | +| **where** | `string` | ✅ | Human-readable location — `flow "leave_approval" · node "approve"`. Prose for a person; use `path` for anything mechanical. | +| **message** | `string` | ✅ | What is wrong, in the rule author's own words. | +| **hint** | `string` | ✅ | How to fix it. | +| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | ✅ | How the gate treated this finding. `error` means the write was REFUSED (these appear on the 422, never on a 2xx); `warning` / `info` are advisory — the write succeeded and the finding is FYI. | + --- @@ -1527,6 +2401,16 @@ List packages response | **totalHits** | `number` | ✅ | Number of hits returned — equals `hits.length`. NOT a deployment-wide total-match count: matches beyond `limit` / `perObject` are not counted. | | **truncated** | `boolean` | ✅ | True when the sweep stopped at the overall `limit` — more matches may exist beyond the returned set. | +### Nested Shape: `SearchAllResponse.hits[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **object** | `string` | ✅ | Name of the object the hit belongs to. | +| **id** | `string` | ✅ | ID of the matched record. | +| **title** | `string` | ✅ | Display title for the hit, resolved in order: the object's `titleFormat` template → the declared primary-title pointer (`nameField`, ADR-0079; deprecated alias `displayNameField` still honored) → conventional name fields → first/last name → the record ID as a string. | +| **snippet** | `string` | optional | Excerpt cut around the first matched term in a searchable text column, ellipsized at both ends when truncated. ABSENT when no source column literally contains a term (e.g. a pinyin companion match, #7643) — absence is a correct answer, not a miss. | +| **record** | `Record` | ✅ | The matched record as the engine's find path returns it (row-level security applied, internal fields already stripped). Object-specific — no cross-object field shape is promised beyond "a record of the named object". | + --- @@ -1539,6 +2423,15 @@ List packages response | **channel** | `string` | ✅ | Channel to set presence in | | **state** | `{ userId: string; status: Enum<'online' \| 'away' \| 'busy' \| 'offline'>; lastSeen: string; metadata?: Record }` | ✅ | Presence state to set | +### Nested Shape: `SetPresenceRequest.state` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **userId** | `string` | ✅ | User identifier | +| **status** | `Enum<'online' \| 'away' \| 'busy' \| 'offline'>` | ✅ | Current presence status | +| **lastSeen** | `string` | ✅ | ISO 8601 datetime of last activity | +| **metadata** | `Record` | optional | Custom presence data (e.g., current page, custom status) | + --- @@ -1640,6 +2533,16 @@ Uninstall package response | **record** | `Record` | ✅ | Updated record | | **droppedFields** | `{ object: string; fields: string[]; reason: Enum<'readonly' \| 'readonly_when' \| 'primary_key'> }[]` | optional | Write-observability (#3407/#3431): caller-supplied fields the engine LEGALLY stripped from the write before persisting — static `readonly` (#2948) or a TRUE `readonlyWhen` predicate (#3042). Present ONLY when ≥1 field was dropped; the update still succeeded without them (status/success semantics unchanged — stripping is legitimate, not an error). REST additionally surfaces this as the `X-ObjectStack-Dropped-Fields` response header. Optional — omit-when-empty keeps the shape backward-compatible for existing clients that only read `record`. | +### Nested Shape: `UpdateDataResponse.droppedFields[number]` + +A write-path strip event: caller-supplied fields legally dropped from the payload (#3407) + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **object** | `string` | ✅ | Object the write targeted (resolved object name) | +| **fields** | `string[]` | ✅ | Caller-supplied field names the engine removed from the write payload | +| **reason** | `Enum<'readonly' \| 'readonly_when' \| 'primary_key'>` | ✅ | Why the fields were dropped: static readonly (#2948), a TRUE readonlyWhen predicate (#3042), or the primary-key strip of a payload id the engine ruled is not an identifier (#6437) | + --- @@ -1653,6 +2556,22 @@ Uninstall package response | **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean }` | optional | Update options | | **object** | `string` | ✅ | Object name | +### Nested Shape: `UpdateManyDataRequest.records[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Record ID | +| **data** | `Record` | ✅ | Fields to update | + +### Nested Shape: `UpdateManyDataRequest.options` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **atomic** | `boolean` | optional (default: `false`) | Opt-in all-or-nothing. When explicitly true the whole batch runs inside ONE engine transaction: the first failure rolls back every prior write, and the response reports zero successes — each row carries `errors[0].code` ROLLED_BACK (written, then undone), the causal row its own error, and rows never reached NOT_ATTEMPTED. A runtime that cannot roll back REFUSES the request (501 NOT_IMPLEMENTED) rather than silently degrading to best-effort — probe `capabilities.transactionalBatch` on /discovery first. Takes precedence over continueOnError. Default false: sequential best-effort. | +| **returnRecords** | `boolean` | optional (default: `false`) | If true, return full record data in response | +| **continueOnError** | `boolean` | optional (default: `false`) | If true (and atomic=false), continue processing remaining records after errors. Default false: the first failure ENDS the run — records before it stay written (nothing is rolled back on this arm), and every record after it is reported `errors[0].code` NOT_ATTEMPTED rather than omitted, so `results` always covers all `total` records and `succeeded + failed === total` (#7539). | +| **validateOnly** | `never` | optional | [REMOVED] `options.validateOnly` was removed from BatchOptions in @objectstack/spec (#4052). It was never implemented: the batch surfaces persisted regardless, so a "dry-run" would have silently executed. There is no dry-run today — drop the key. If you need to preview a batch without writing, open an issue so it can be designed (no-commit cascade / constraint semantics) and reintroduced as a flag that actually holds. | + --- @@ -1671,6 +2590,30 @@ Uninstall package response | **failed** | `number` | ✅ | Number of records that failed | | **results** | `{ id?: string; success: boolean; errors?: object[]; data?: Record; … }[]` | ✅ | Detailed results for each record | +### Nested Shape: `UpdateManyDataResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `UpdateManyDataResponse.results[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | optional | Record ID if operation succeeded | +| **success** | `boolean` | ✅ | Whether this record was processed successfully | +| **errors** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>; declaredCode?: string; message: string; userMessage?: string; … }[]` | optional | Array of errors if operation failed. Branch on `errors[0].code` — an atomic batch that rolled back marks rows that were written then undone with code ROLLED_BACK and rows never reached with NOT_ATTEMPTED, while the causal row keeps its own error (#4793). A NON-atomic batch that stopped (the `continueOnError: false` default) marks its un-attempted tail with the same NOT_ATTEMPTED code — rows before the failure stay written and keep reporting success, since nothing was rolled back (#7539). | +| **data** | `Record` | optional | Full record data (if returnRecords=true) | +| **index** | `number` | optional | Index of the record in the request array | +| **droppedFields** | `{ object: string; fields: string[]; reason: Enum<'readonly' \| 'readonly_when' \| 'primary_key'> }[]` | optional | Write-observability (#3407/#3431/#3455): caller-supplied fields LEGALLY stripped from THIS row before it was written — static `readonly` (#2948) / TRUE `readonlyWhen` (#3042) on update, or the #3043 create-ingress strip. Per-row because a batch can drop different fields on different rows (`readonlyWhen` is record-state-dependent). Present ONLY when ≥1 field was dropped for this row; the row still succeeded (success unchanged). A single response header cannot express per-row drops, so this body field is the canonical bulk channel — REST does not emit `X-ObjectStack-Dropped-Fields` for batches. Optional — omit-when-empty keeps the shape backward-compatible. | + --- @@ -1682,6 +2625,16 @@ Uninstall package response | :--- | :--- | :--- | :--- | | **preferences** | `{ email?: boolean; push?: boolean; inApp?: boolean; digest?: Enum<'none' \| 'daily' \| 'weekly'>; … }` | ✅ | Preferences to update | +### Nested Shape: `UpdateNotificationPreferencesRequest.preferences` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **email** | `boolean` | optional (default: `true`) | Receive email notifications | +| **push** | `boolean` | optional (default: `true`) | Receive push notifications | +| **inApp** | `boolean` | optional (default: `true`) | Receive in-app notifications | +| **digest** | `Enum<'none' \| 'daily' \| 'weekly'>` | optional (default: `"none"`) | Email digest frequency | +| **channels** | `Record` | optional | Per-channel notification preferences | + --- @@ -1693,6 +2646,16 @@ Uninstall package response | :--- | :--- | :--- | :--- | | **preferences** | `{ email: boolean; push: boolean; inApp: boolean; digest: Enum<'none' \| 'daily' \| 'weekly'>; … }` | ✅ | Updated notification preferences | +### Nested Shape: `UpdateNotificationPreferencesResponse.preferences` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **email** | `boolean` | optional (default: `true`) | Receive email notifications | +| **push** | `boolean` | optional (default: `true`) | Receive push notifications | +| **inApp** | `boolean` | optional (default: `true`) | Receive in-app notifications | +| **digest** | `Enum<'none' \| 'daily' \| 'weekly'>` | optional (default: `"none"`) | Email digest frequency | +| **channels** | `Record` | optional | Per-channel notification preferences | + --- @@ -1734,6 +2697,21 @@ Uninstall package response | **results** | `{ valid: boolean; errors: object[]; warnings: object[] }[]` | ✅ | Per-row verdicts, in submission order. | | **posture** | `{ valueShapeStrict: boolean; mediaValueShapeStrict: boolean }` | ✅ | The ADR-0104 posture the verdict was reached under — reported because it is the difference between "this row is fine" and "this row is fine HERE". The same row can be an error on a self-certified deployment and an admitted warning on an un-migrated one, and a caller explaining a verdict needs to know which it got. An unconditionally-strict preview was considered and rejected (#4633 option B): it would fail rows on every un-migrated deployment that the write would have accepted. | +### Nested Shape: `ValidateDataResponse.results[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **valid** | `boolean` | ✅ | True when this row would be accepted by the write path. | +| **errors** | `{ field: string; code: string; message: string }[]` | ✅ | Findings that would REJECT this row. Empty when valid. | +| **warnings** | `{ field: string; code: string; message: string }[]` | ✅ | Findings the target deployment ADMITS rather than rejects — today, ADR-0104 value shapes under a warn-first posture. The row is valid; the write would store it and log the same complaint. | + +### Nested Shape: `ValidateDataResponse.posture` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **valueShapeStrict** | `boolean` | ✅ | True when this deployment rejects non-conforming value shapes (ADR-0104 self-certified). | +| **mediaValueShapeStrict** | `boolean` | ✅ | The same, for media field value shapes. | + --- diff --git a/content/docs/references/api/query-adapter.mdx b/content/docs/references/api/query-adapter.mdx index d068fcd67a..d1b33dc80a 100644 --- a/content/docs/references/api/query-adapter.mdx +++ b/content/docs/references/api/query-adapter.mdx @@ -47,6 +47,13 @@ const result = ODataQueryAdapterSchema.parse(data); | **stringFunctions** | `Enum<'contains' \| 'startswith' \| 'endswith' \| 'tolower' \| 'toupper' \| 'trim' \| 'concat' \| 'substring' \| 'length'>[]` | optional | Supported OData string functions | | **expand** | `{ enabled: boolean; maxDepth: integer }` | optional | $expand configuration | +### Nested Shape: `ODataQueryAdapter.expand` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | Enable $expand support | +| **maxDepth** | `integer` | optional (default: `3`) | Maximum expand depth | + --- @@ -73,6 +80,32 @@ const result = ODataQueryAdapterSchema.parse(data); | **rest** | `{ filterStyle: Enum<'bracket' \| 'dot' \| 'flat' \| 'rsql'>; pagination?: object; sorting?: object; fieldsParam: string }` | optional | REST query adapter configuration | | **odata** | `{ version: Enum<'v2' \| 'v4'>; usePrefix: boolean; stringFunctions?: Enum<'contains' \| 'startswith' \| 'endswith' \| 'tolower' \| 'toupper' \| 'trim' \| …>[]; expand?: object }` | optional | OData query adapter configuration | +### Nested Shape: `QueryAdapterConfig.operatorMappings[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **operator** | `string` | ✅ | Unified DSL operator | +| **rest** | `string` | optional | REST query parameter template | +| **odata** | `string` | optional | OData $filter expression template | + +### Nested Shape: `QueryAdapterConfig.rest` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **filterStyle** | `Enum<'bracket' \| 'dot' \| 'flat' \| 'rsql'>` | optional (default: `"bracket"`) | REST filter parameter encoding style | +| **pagination** | `{ limitParam: string; offsetParam: string; cursorParam: string; pageParam: string }` | optional | Pagination parameter name mappings | +| **sorting** | `{ param: string; format: Enum<'comma' \| 'array' \| 'pipe'> }` | optional | Sort parameter mapping | +| **fieldsParam** | `string` | optional (default: `"fields"`) | Field selection parameter name | + +### Nested Shape: `QueryAdapterConfig.odata` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **version** | `Enum<'v2' \| 'v4'>` | optional (default: `"v4"`) | OData version | +| **usePrefix** | `boolean` | optional (default: `true`) | Use $ prefix for system query options ($filter vs filter) | +| **stringFunctions** | `Enum<'contains' \| 'startswith' \| 'endswith' \| 'tolower' \| 'toupper' \| 'trim' \| …>[]` | optional | Supported OData string functions | +| **expand** | `{ enabled: boolean; maxDepth: integer }` | optional | $expand configuration | + --- @@ -97,6 +130,22 @@ const result = ODataQueryAdapterSchema.parse(data); | **sorting** | `{ param: string; format: Enum<'comma' \| 'array' \| 'pipe'> }` | optional | Sort parameter mapping | | **fieldsParam** | `string` | optional (default: `"fields"`) | Field selection parameter name | +### Nested Shape: `RestQueryAdapter.pagination` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **limitParam** | `string` | optional (default: `"limit"`) | Page size parameter name | +| **offsetParam** | `string` | optional (default: `"offset"`) | Offset parameter name | +| **cursorParam** | `string` | optional (default: `"cursor"`) | Cursor parameter name | +| **pageParam** | `string` | optional (default: `"page"`) | Page number parameter name | + +### Nested Shape: `RestQueryAdapter.sorting` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **param** | `string` | optional (default: `"sort"`) | Sort parameter name | +| **format** | `Enum<'comma' \| 'array' \| 'pipe'>` | optional (default: `"comma"`) | Sort parameter encoding format | + --- diff --git a/content/docs/references/api/realtime.mdx b/content/docs/references/api/realtime.mdx index e85893f1c8..79581bc7ae 100644 --- a/content/docs/references/api/realtime.mdx +++ b/content/docs/references/api/realtime.mdx @@ -31,6 +31,15 @@ const result = RealtimeConfigSchema.parse(data); | **transport** | `Enum<'websocket' \| 'sse' \| 'polling'>` | optional (default: `"websocket"`) | Transport protocol | | **subscriptions** | `{ id: string; events: object[]; transport: Enum<'websocket' \| 'sse' \| 'polling'>; channel?: string }[]` | optional | Default subscriptions | +### Nested Shape: `RealtimeConfig.subscriptions[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique subscription identifier | +| **events** | `{ type: Enum<'record.created' \| 'record.updated' \| 'record.deleted' \| 'field.changed'>; object?: string; filters?: any }[]` | ✅ | Array of events to subscribe to | +| **transport** | `Enum<'websocket' \| 'sse' \| 'polling'>` | ✅ | Transport protocol to use | +| **channel** | `string` | optional | Optional channel name for grouping subscriptions | + --- @@ -91,6 +100,14 @@ Realtime event type (not yet enforced — the runtime emits data.record.* event | **transport** | `Enum<'websocket' \| 'sse' \| 'polling'>` | ✅ | Transport protocol to use | | **channel** | `string` | optional | Optional channel name for grouping subscriptions | +### Nested Shape: `Subscription.events[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'record.created' \| 'record.updated' \| 'record.deleted' \| 'field.changed'>` | ✅ | Type of event to subscribe to | +| **object** | `string` | optional | Object name to subscribe to | +| **filters** | `any` | optional | Filter conditions | + --- diff --git a/content/docs/references/api/rest-server.mdx b/content/docs/references/api/rest-server.mdx index 3a863cd934..9eed88945f 100644 --- a/content/docs/references/api/rest-server.mdx +++ b/content/docs/references/api/rest-server.mdx @@ -49,6 +49,15 @@ const result = BatchEndpointsConfigSchema.parse(data); | **operations** | `{ createMany: boolean; updateMany: boolean; deleteMany: boolean; upsertMany: boolean }` | optional | Enable/disable specific batch operations | | **defaultAtomic** | `boolean` | optional (default: `true`) | Default atomic/transaction mode for batch operations | +### Nested Shape: `BatchEndpointsConfig.operations` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **createMany** | `boolean` | optional (default: `true`) | Enable POST /data/:object/createMany | +| **updateMany** | `boolean` | optional (default: `true`) | Enable POST /data/:object/updateMany | +| **deleteMany** | `boolean` | optional (default: `true`) | Enable POST /data/:object/deleteMany | +| **upsertMany** | `boolean` | optional (default: `true`) | Enable POST /data/:object/upsertMany | + --- @@ -77,6 +86,25 @@ const result = BatchEndpointsConfigSchema.parse(data); | **dataPrefix** | `string` | optional (default: `"/data"`) | URL prefix for data endpoints | | **objectParamStyle** | `Enum<'path' \| 'query'>` | optional (default: `"path"`) | How object name is passed (path param or query param) | +### Nested Shape: `CrudEndpointsConfig.operations` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **create** | `boolean` | optional (default: `true`) | Enable create operation | +| **read** | `boolean` | optional (default: `true`) | Enable read operation | +| **update** | `boolean` | optional (default: `true`) | Enable update operation | +| **delete** | `boolean` | optional (default: `true`) | Enable delete operation | +| **list** | `boolean` | optional (default: `true`) | Enable list operation | + +### Nested Shape: `CrudEndpointsConfig.patterns[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>` | ✅ | HTTP method | +| **path** | `string` | ✅ | URL path pattern | +| **summary** | `string` | optional | Operation summary | +| **description** | `string` | optional | Operation description | + --- @@ -104,6 +132,42 @@ const result = BatchEndpointsConfigSchema.parse(data); | **byObject** | `Record; path: string; object: string; … }[]>` | optional | Endpoints grouped by object | | **byOperation** | `Record; path: string; object: string; … }[]>` | optional | Endpoints grouped by operation | +### Nested Shape: `EndpointRegistry.endpoints[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique endpoint identifier | +| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>` | ✅ | HTTP method | +| **path** | `string` | ✅ | Full URL path | +| **object** | `string` | ✅ | Object name (snake_case) | +| **operation** | `Enum<'create' \| 'read' \| 'update' \| 'delete' \| 'list'> \| string` | ✅ | Operation type | +| **handler** | `string` | ✅ | Handler function identifier | +| **metadata** | `{ summary?: string; description?: string; tags?: string[]; deprecated?: boolean }` | optional | | + +### Nested Shape: `EndpointRegistry.byObject[string][number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique endpoint identifier | +| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>` | ✅ | HTTP method | +| **path** | `string` | ✅ | Full URL path | +| **object** | `string` | ✅ | Object name (snake_case) | +| **operation** | `Enum<'create' \| 'read' \| 'update' \| 'delete' \| 'list'> \| string` | ✅ | Operation type | +| **handler** | `string` | ✅ | Handler function identifier | +| **metadata** | `{ summary?: string; description?: string; tags?: string[]; deprecated?: boolean }` | optional | | + +### Nested Shape: `EndpointRegistry.byOperation[string][number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique endpoint identifier | +| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>` | ✅ | HTTP method | +| **path** | `string` | ✅ | Full URL path | +| **object** | `string` | ✅ | Object name (snake_case) | +| **operation** | `Enum<'create' \| 'read' \| 'update' \| 'delete' \| 'list'> \| string` | ✅ | Operation type | +| **handler** | `string` | ✅ | Handler function identifier | +| **metadata** | `{ summary?: string; description?: string; tags?: string[]; deprecated?: boolean }` | optional | | + --- @@ -136,6 +200,15 @@ const result = BatchEndpointsConfigSchema.parse(data); | **maskObjectFields** | `boolean` | optional (default: `true`) | [ADR-0106 D8] Mask served object schemas to the caller's readable fields | | **endpoints** | `{ types: boolean; items: boolean; item: boolean; schema: boolean }` | optional | Enable/disable specific endpoints | +### Nested Shape: `MetadataEndpointsConfig.endpoints` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **types** | `boolean` | optional (default: `true`) | GET /meta - List all metadata types | +| **items** | `boolean` | optional (default: `true`) | GET /meta/:type - List items of type | +| **item** | `boolean` | optional (default: `true`) | GET /meta/:type/:name - Get specific item | +| **schema** | `boolean` | optional (default: `true`) | GET /meta/:type/:name/schema - Get JSON schema | + --- @@ -160,6 +233,26 @@ const result = BatchEndpointsConfigSchema.parse(data); | **documentation** | `{ enabled: boolean; title: string; description?: string; version?: string; … }` | optional | OpenAPI/Swagger documentation config | | **responseFormat** | `{ envelope: boolean; includeMetadata: boolean; includePagination: boolean }` | optional | Response format options | +### Nested Shape: `RestApiConfig.documentation` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | Enable API documentation | +| **title** | `string` | optional (default: `"ObjectStack API"`) | API documentation title | +| **description** | `string` | optional | API description | +| **version** | `string` | optional | Documentation version | +| **termsOfService** | `string` | optional | Terms of service URL | +| **contact** | `{ name?: string; url?: string; email?: string }` | optional | | +| **license** | `{ name: string; url?: string }` | optional | | + +### Nested Shape: `RestApiConfig.responseFormat` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **envelope** | `boolean` | optional (default: `true`) | Wrap responses in standard envelope | +| **includeMetadata** | `boolean` | optional (default: `true`) | Include response metadata (timestamp, requestId) | +| **includePagination** | `boolean` | optional (default: `true`) | Include pagination info in list responses | + --- @@ -176,6 +269,62 @@ const result = BatchEndpointsConfigSchema.parse(data); | **routes** | `{ includeObjects?: string[]; excludeObjects?: string[]; nameTransform: Enum<'none' \| 'plural' \| 'kebab-case' \| 'camelCase'>; overrides?: Record }` | optional | Route generation configuration | | **openApi31** | `never` | optional | [REMOVED] `RestServerConfig.openApi31` was removed in @objectstack/spec 17 (#4579, ADR-0049) — no runtime ever read it: the REST server forwards only `api`/`crud`/`metadata`/`batch`/`routes`, and the served /openapi.json is the pre-generated contract enriched with the live server URL and the registered objects, so webhook/callback definitions declared here never appeared in it. Delete the key. Config-driven OpenAPI 3.1 webhooks/callbacks documentation is a new capability and must arrive via the enforce route of ADR-0049 (a new ADR), not by re-declaring the key; for a real outbound webhook use `Webhook` from `@objectstack/spec/automation`. | +### Nested Shape: `RestServerConfig.api` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **version** | `string` | optional (default: `"v1"`) | API version (e.g., v1, v2, 2024-01) | +| **basePath** | `string` | optional (default: `"/api"`) | Base URL path for API | +| **apiPath** | `string` | optional | Full API path (defaults to `{basePath}`/`{version}`) | +| **enableCrud** | `boolean` | optional (default: `true`) | Enable automatic CRUD endpoint generation | +| **enableMetadata** | `boolean` | optional (default: `true`) | Enable metadata API endpoints | +| **enableUi** | `boolean` | optional (default: `true`) | Enable UI API endpoints (Views, Menus, Layouts) | +| **enableBatch** | `boolean` | optional (default: `true`) | Enable batch operation endpoints | +| **enableDiscovery** | `boolean` | optional (default: `true`) | Enable API discovery endpoint | +| **enableOpenApi** | `boolean` | optional (default: `true`) | Enable OpenAPI 3.1 spec & docs viewer endpoints | +| **enableProjectScoping** | `boolean` | optional (default: `false`) | Enable project-scoped routing for data/meta/AI APIs | +| **projectResolution** | `Enum<'required' \| 'optional' \| 'auto'>` | optional (default: `"auto"`) | Project ID resolution strategy | +| **requireAuth** | `never` | optional | [REMOVED] `api.requireAuth` was removed in @objectstack/spec 17 (#3963). Anonymous access to object data is now always denied — auth is a kernel concern, not a deployment posture. Delete the key. To publish something publicly, declare it: a public form view (`sharing.allowAnonymous`), a share link, or `book.audience: 'public'` — each derives its own narrow authorization instead of opening the whole data plane. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **documentation** | `{ enabled: boolean; title: string; description?: string; version?: string; … }` | optional | OpenAPI/Swagger documentation config | +| **responseFormat** | `{ envelope: boolean; includeMetadata: boolean; includePagination: boolean }` | optional | Response format options | + +### Nested Shape: `RestServerConfig.crud` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **operations** | `{ create: boolean; read: boolean; update: boolean; delete: boolean; … }` | optional | Enable/disable operations | +| **patterns** | `Record; path: string; summary?: string; description?: string }>` | optional | Custom URL patterns for operations | +| **dataPrefix** | `string` | optional (default: `"/data"`) | URL prefix for data endpoints | +| **objectParamStyle** | `Enum<'path' \| 'query'>` | optional (default: `"path"`) | How object name is passed (path param or query param) | + +### Nested Shape: `RestServerConfig.metadata` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **prefix** | `string` | optional (default: `"/meta"`) | URL prefix for metadata endpoints | +| **enableCache** | `boolean` | optional (default: `true`) | Enable HTTP cache headers (ETag, Last-Modified) | +| **cacheTtl** | `integer` | optional (default: `3600`) | Cache TTL in seconds | +| **maskObjectFields** | `boolean` | optional (default: `true`) | [ADR-0106 D8] Mask served object schemas to the caller's readable fields | +| **endpoints** | `{ types: boolean; items: boolean; item: boolean; schema: boolean }` | optional | Enable/disable specific endpoints | + +### Nested Shape: `RestServerConfig.batch` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **maxBatchSize** | `integer` | optional (default: `200`) | Maximum records per batch operation | +| **enableBatchEndpoint** | `boolean` | optional (default: `true`) | Enable POST /data/:object/batch endpoint | +| **operations** | `{ createMany: boolean; updateMany: boolean; deleteMany: boolean; upsertMany: boolean }` | optional | Enable/disable specific batch operations | +| **defaultAtomic** | `boolean` | optional (default: `true`) | Default atomic/transaction mode for batch operations | + +### Nested Shape: `RestServerConfig.routes` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **includeObjects** | `string[]` | optional | Specific objects to generate routes for (empty = all) | +| **excludeObjects** | `string[]` | optional | Objects to exclude from route generation | +| **nameTransform** | `Enum<'none' \| 'plural' \| 'kebab-case' \| 'camelCase'>` | optional (default: `"none"`) | Transform object names in URLs | +| **overrides** | `Record }>` | optional | Per-object route customization | + --- @@ -190,6 +339,14 @@ const result = BatchEndpointsConfigSchema.parse(data); | **nameTransform** | `Enum<'none' \| 'plural' \| 'kebab-case' \| 'camelCase'>` | optional (default: `"none"`) | Transform object names in URLs | | **overrides** | `Record }>` | optional | Per-object route customization | +### Nested Shape: `RouteGenerationConfig.overrides[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | Enable/disable routes for this object | +| **basePath** | `string` | optional | Custom base path | +| **operations** | `Record` | optional | Enable/disable specific operations | + --- diff --git a/content/docs/references/api/router.mdx b/content/docs/references/api/router.mdx index 6ac13095a1..5b0e1e6219 100644 --- a/content/docs/references/api/router.mdx +++ b/content/docs/references/api/router.mdx @@ -95,6 +95,41 @@ HTTP method — the full routing vocabulary (`api/*` endpoints, router and REST- | **cors** | `{ enabled: boolean; origins: string \| string[]; methods?: Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>[]; credentials: boolean; … }` | optional | | | **staticMounts** | `{ path: string; directory: string; cacheControl?: string }[]` | optional | | +### Nested Shape: `RouterConfig.mounts` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **data** | `string` | optional (default: `"/data"`) | Data Protocol (CRUD) | +| **metadata** | `string` | optional (default: `"/meta"`) | Metadata Protocol (Schemas) | +| **auth** | `string` | optional (default: `"/auth"`) | Auth Protocol | +| **automation** | `string` | optional (default: `"/automation"`) | Automation Protocol | +| **storage** | `string` | optional (default: `"/storage"`) | Storage Protocol | +| **analytics** | `string` | optional (default: `"/analytics"`) | Analytics Protocol | +| **ui** | `string` | optional (default: `"/ui"`) | UI Metadata Protocol (Views, Layouts) | +| **realtime** | `string` | optional (default: `"/realtime"`) | Realtime/WebSocket Protocol | +| **notifications** | `string` | optional (default: `"/notifications"`) | Notification Protocol | +| **ai** | `string` | optional (default: `"/ai"`) | AI Engine Protocol (NLQ, Chat, Suggest) | +| **i18n** | `string` | optional (default: `"/i18n"`) | Internationalization Protocol | +| **packages** | `string` | optional (default: `"/packages"`) | Package Management Protocol | + +### Nested Shape: `RouterConfig.cors` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | Enable CORS | +| **origins** | `string \| string[]` | optional (default: `"*"`) | Allowed origins (* for all) | +| **methods** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>[]` | optional | Allowed HTTP methods | +| **credentials** | `boolean` | optional (default: `false`) | Allow credentials (cookies, authorization headers) | +| **maxAge** | `integer` | optional | Preflight cache duration in seconds | + +### Nested Shape: `RouterConfig.staticMounts[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **path** | `string` | ✅ | URL path to serve from | +| **directory** | `string` | ✅ | Physical directory to serve | +| **cacheControl** | `string` | optional | Cache-Control header value | + --- diff --git a/content/docs/references/api/sortability.mdx b/content/docs/references/api/sortability.mdx index e265ff4117..9550e49365 100644 --- a/content/docs/references/api/sortability.mdx +++ b/content/docs/references/api/sortability.mdx @@ -129,6 +129,14 @@ const result = FieldSortabilitySchema.parse(data); | :--- | :--- | :--- | :--- | | **fields** | `Record` | ✅ | Verdict per sortable-addressable column, keyed by field name. The domain is the served field map plus the always-provisioned `id`; a name absent from this map (an unknown field, a dotted path, an unprovisioned audit column) has no platform sort behind it and must get no sort affordance. | +### Nested Shape: `ObjectSortability.fields[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **sortable** | `boolean` | ✅ | Whether the platform honors an ORDER BY over this field. `false` means the runtime REFUSES the sort (`400 INVALID_SORT`) — render no sort affordance. `true` means the sort is accepted; see `caveat` for the one accepted-but-degradable case. A derived verdict: do not recompute it from field `type` client-side. | +| **reason** | `'virtual-type'` | optional | Present exactly when `sortable` is false: the field's type is virtual (computed on read, no stored column), so no driver materialises anything to ORDER BY. | +| **caveat** | `'unprovisioned-anchor'` | optional | Present only with `sortable: true`: the field is a platform-injected anchor on an ADR-0015 `external` object with no storage provisioned behind it. The runtime accepts the sort, but when the remote table carries no such column the ORDER BY is silently dropped (asc === desc under a 200). Consumers may choose a conservative affordance for these entries. | + --- diff --git a/content/docs/references/api/storage.mdx b/content/docs/references/api/storage.mdx index 050269d40b..5996ef0f14 100644 --- a/content/docs/references/api/storage.mdx +++ b/content/docs/references/api/storage.mdx @@ -36,6 +36,13 @@ const result = CompleteChunkedUploadRequestSchema.parse(data); | **uploadId** | `string` | ✅ | Multipart upload session ID | | **parts** | `{ chunkIndex: integer; eTag: string }[]` | ✅ | Ordered list of uploaded parts for assembly | +### Nested Shape: `CompleteChunkedUploadRequest.parts[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **chunkIndex** | `integer` | ✅ | Chunk index | +| **eTag** | `string` | ✅ | ETag returned from chunk upload | + --- @@ -50,6 +57,30 @@ const result = CompleteChunkedUploadRequestSchema.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ fileId: string; key: string; size: integer; mimeType: string; … }` | ✅ | | +### Nested Shape: `CompleteChunkedUploadResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `CompleteChunkedUploadResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **fileId** | `string` | ✅ | Final file ID | +| **key** | `string` | ✅ | Storage key/path of the assembled file | +| **size** | `integer` | ✅ | Total file size in bytes | +| **mimeType** | `string` | ✅ | File MIME type | +| **eTag** | `string` | optional | Final ETag of the assembled file | +| **url** | `string` | optional | Download URL for the assembled file | + --- @@ -76,6 +107,25 @@ const result = CompleteChunkedUploadRequestSchema.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ url: string }` | ✅ | | +### Nested Shape: `FileDownloadUrlResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `FileDownloadUrlResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **url** | `string` | ✅ | Short-lived signed download URL; may be server-relative | + --- @@ -105,6 +155,32 @@ const result = CompleteChunkedUploadRequestSchema.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ path: string; name: string; size: integer; mimeType: string; … }` | ✅ | Uploaded file metadata | +### Nested Shape: `FileUploadResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `FileUploadResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **path** | `string` | ✅ | File path | +| **name** | `string` | ✅ | File name | +| **size** | `integer` | ✅ | File size in bytes | +| **mimeType** | `string` | ✅ | MIME type | +| **lastModified** | `string` | ✅ | Last modified timestamp | +| **created** | `string` | ✅ | Creation timestamp | +| **etag** | `string` | optional | Entity tag | +| **fileId** | `string` | optional | Opaque sys_file id (ADR-0104 D3 file-as-reference) | + --- @@ -151,6 +227,30 @@ const result = CompleteChunkedUploadRequestSchema.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ uploadId: string; resumeToken: string; fileId: string; totalChunks: integer; … }` | ✅ | | +### Nested Shape: `InitiateChunkedUploadResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `InitiateChunkedUploadResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **uploadId** | `string` | ✅ | Multipart upload session ID | +| **resumeToken** | `string` | ✅ | Opaque token for resuming interrupted uploads | +| **fileId** | `string` | ✅ | Assigned file ID | +| **totalChunks** | `integer` | ✅ | Expected number of chunks | +| **chunkSize** | `integer` | ✅ | Chunk size in bytes | +| **expiresAt** | `string` | ✅ | Upload session expiration timestamp | + --- @@ -165,6 +265,30 @@ const result = CompleteChunkedUploadRequestSchema.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ uploadUrl: string; downloadUrl?: string; fileId: string; method: Enum<'PUT' \| 'POST'>; … }` | ✅ | | +### Nested Shape: `PresignedUrlResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `PresignedUrlResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **uploadUrl** | `string` | ✅ | PUT/POST URL for direct upload | +| **downloadUrl** | `string` | optional | Public/Private preview URL | +| **fileId** | `string` | ✅ | Temporary File ID | +| **method** | `Enum<'PUT' \| 'POST'>` | ✅ | HTTP Method to use | +| **headers** | `Record` | optional | Required headers for upload | +| **expiresIn** | `number` | ✅ | URL expiry in seconds | + --- @@ -179,6 +303,25 @@ const result = CompleteChunkedUploadRequestSchema.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ key: string }` | ✅ | | +### Nested Shape: `RawUploadResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `RawUploadResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **key** | `string` | ✅ | Storage key the bytes were written to | + --- @@ -206,6 +349,27 @@ const result = CompleteChunkedUploadRequestSchema.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ chunkIndex: integer; eTag: string; bytesReceived: integer }` | ✅ | | +### Nested Shape: `UploadChunkResponse.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `UploadChunkResponse.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **chunkIndex** | `integer` | ✅ | Chunk index that was uploaded | +| **eTag** | `string` | ✅ | Chunk ETag for multipart completion | +| **bytesReceived** | `integer` | ✅ | Bytes received for this chunk | + --- @@ -220,6 +384,35 @@ const result = CompleteChunkedUploadRequestSchema.parse(data); | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ uploadId: string; fileId: string; filename: string; totalSize: integer; … }` | ✅ | | +### Nested Shape: `UploadProgress.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) | +| **message** | `string` | ✅ | Readable error message | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. | +| **category** | `string` | optional | Error category (e.g. validation, authorization) | +| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | +| **details** | `any` | optional | Additional error context (e.g. field validation errors) | +| **requestId** | `string` | optional | Request ID for tracking | + +### Nested Shape: `UploadProgress.data` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **uploadId** | `string` | ✅ | Multipart upload session ID | +| **fileId** | `string` | ✅ | Assigned file ID | +| **filename** | `string` | ✅ | Original filename | +| **totalSize** | `integer` | ✅ | Total file size in bytes | +| **uploadedSize** | `integer` | ✅ | Bytes uploaded so far | +| **totalChunks** | `integer` | ✅ | Total expected chunks | +| **uploadedChunks** | `integer` | ✅ | Number of chunks uploaded | +| **percentComplete** | `number` | ✅ | Upload progress percentage | +| **status** | `Enum<'in_progress' \| 'completing' \| 'completed' \| 'failed' \| 'expired'>` | ✅ | Current upload session status | +| **startedAt** | `string` | ✅ | Upload session start timestamp | +| **expiresAt** | `string` | ✅ | Session expiration timestamp | + --- diff --git a/content/docs/references/api/versioning.mdx b/content/docs/references/api/versioning.mdx index 6ac5a2f104..4fcc2825cf 100644 --- a/content/docs/references/api/versioning.mdx +++ b/content/docs/references/api/versioning.mdx @@ -64,6 +64,19 @@ const result = VersionDefinitionSchema.parse(data); | **deprecated** | `string[]` | optional | Deprecated version identifiers | | **versions** | `{ version: string; status: Enum<'preview' \| 'current' \| 'supported' \| 'deprecated' \| 'retired'>; releasedAt: string; deprecatedAt?: string; … }[]` | optional | Full version definitions with lifecycle metadata | +### Nested Shape: `VersionNegotiationResponse.versions[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **version** | `string` | ✅ | Version identifier (e.g., "v1", "v2beta1", "2025-01-01") | +| **status** | `Enum<'preview' \| 'current' \| 'supported' \| 'deprecated' \| 'retired'>` | ✅ | Lifecycle status of this version | +| **releasedAt** | `string` | ✅ | Release date (ISO 8601, e.g., "2025-01-15") | +| **deprecatedAt** | `string` | optional | Deprecation date (ISO 8601). Only set for deprecated/retired versions | +| **sunsetAt** | `string` | optional | Sunset date (ISO 8601). After this date, the version returns 410 Gone | +| **migrationGuide** | `string` | optional | URL to migration guide for upgrading from this version | +| **description** | `string` | optional | Human-readable description or release notes summary | +| **breakingChanges** | `string[]` | optional | List of breaking changes (for preview/new versions) | + --- @@ -96,6 +109,29 @@ const result = VersionDefinitionSchema.parse(data); | **deprecation** | `{ warnHeader: boolean; sunsetHeader: boolean; linkHeader: boolean; rejectRetired: boolean; … }` | optional | Deprecation lifecycle behavior | | **includeInDiscovery** | `boolean` | optional (default: `true`) | Include version information in the API discovery endpoint | +### Nested Shape: `VersioningConfig.versions[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **version** | `string` | ✅ | Version identifier (e.g., "v1", "v2beta1", "2025-01-01") | +| **status** | `Enum<'preview' \| 'current' \| 'supported' \| 'deprecated' \| 'retired'>` | ✅ | Lifecycle status of this version | +| **releasedAt** | `string` | ✅ | Release date (ISO 8601, e.g., "2025-01-15") | +| **deprecatedAt** | `string` | optional | Deprecation date (ISO 8601). Only set for deprecated/retired versions | +| **sunsetAt** | `string` | optional | Sunset date (ISO 8601). After this date, the version returns 410 Gone | +| **migrationGuide** | `string` | optional | URL to migration guide for upgrading from this version | +| **description** | `string` | optional | Human-readable description or release notes summary | +| **breakingChanges** | `string[]` | optional | List of breaking changes (for preview/new versions) | + +### Nested Shape: `VersioningConfig.deprecation` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **warnHeader** | `boolean` | optional (default: `true`) | Include Deprecation header (RFC 8594) in responses | +| **sunsetHeader** | `boolean` | optional (default: `true`) | Include Sunset header (RFC 8594) with retirement date | +| **linkHeader** | `boolean` | optional (default: `true`) | Include Link header pointing to migration guide URL | +| **rejectRetired** | `boolean` | optional (default: `true`) | Return 410 Gone for retired API versions | +| **warningMessage** | `string` | optional | Custom warning message for deprecated version responses | + --- diff --git a/content/docs/references/api/websocket.mdx b/content/docs/references/api/websocket.mdx index d8fe1e091b..3d026c4ffd 100644 --- a/content/docs/references/api/websocket.mdx +++ b/content/docs/references/api/websocket.mdx @@ -61,6 +61,19 @@ const result = AckMessageSchema.parse(data); | **timestamp** | `string` | ✅ | ISO 8601 datetime when message was sent | | **cursor** | `{ userId: string; sessionId: string; documentId: string; position?: object; … }` | ✅ | Cursor position | +### Nested Shape: `CursorMessage.cursor` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **userId** | `string` | ✅ | User identifier | +| **sessionId** | `string` | ✅ | Session identifier | +| **documentId** | `string` | ✅ | Document identifier being edited | +| **position** | `{ line: integer; column: integer }` | optional | Cursor position in document | +| **selection** | `{ start: object; end: object }` | optional | Selection range (if text is selected) | +| **color** | `string` | optional | Cursor color for visual representation | +| **userName** | `string` | optional | Display name of user | +| **lastUpdate** | `string` | ✅ | ISO 8601 datetime of last cursor update | + --- @@ -79,6 +92,13 @@ const result = AckMessageSchema.parse(data); | **userName** | `string` | optional | Display name of user | | **lastUpdate** | `string` | ✅ | ISO 8601 datetime of last cursor update | +### Nested Shape: `CursorPosition.position` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **line** | `integer` | ✅ | Line number (0-indexed) | +| **column** | `integer` | ✅ | Column number (0-indexed) | + --- @@ -109,6 +129,22 @@ const result = AckMessageSchema.parse(data); | **timestamp** | `string` | ✅ | ISO 8601 datetime when message was sent | | **operation** | `{ operationId: string; documentId: string; userId: string; sessionId: string; … }` | ✅ | Edit operation | +### Nested Shape: `EditMessage.operation` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **operationId** | `string` | ✅ | Unique operation identifier | +| **documentId** | `string` | ✅ | Document identifier | +| **userId** | `string` | ✅ | User who performed the edit | +| **sessionId** | `string` | ✅ | Session identifier | +| **type** | `Enum<'insert' \| 'delete' \| 'replace'>` | ✅ | Type of edit operation | +| **position** | `{ line: integer; column: integer }` | ✅ | Starting position of the operation | +| **endPosition** | `{ line: integer; column: integer }` | optional | Ending position (for delete/replace operations) | +| **content** | `string` | optional | Content to insert/replace | +| **version** | `integer` | ✅ | Document version before this operation | +| **timestamp** | `string` | ✅ | ISO 8601 datetime when operation was created | +| **baseOperationId** | `string` | optional | Previous operation ID this builds upon (for OT) | + --- @@ -130,6 +166,13 @@ const result = AckMessageSchema.parse(data); | **timestamp** | `string` | ✅ | ISO 8601 datetime when operation was created | | **baseOperationId** | `string` | optional | Previous operation ID this builds upon (for OT) | +### Nested Shape: `EditOperation.position` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **line** | `integer` | ✅ | Line number (0-indexed) | +| **column** | `integer` | ✅ | Column number (0-indexed) | + --- @@ -240,6 +283,19 @@ Event pattern (supports wildcards like "record.*" or "*.created") | **timestamp** | `string` | ✅ | ISO 8601 datetime when message was sent | | **presence** | `{ userId: string; sessionId: string; status: Enum<'online' \| 'away' \| 'busy' \| 'offline'>; lastSeen: string; … }` | ✅ | Presence state | +### Nested Shape: `PresenceMessage.presence` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **userId** | `string` | ✅ | User identifier | +| **sessionId** | `string` | ✅ | Unique session identifier | +| **status** | `Enum<'online' \| 'away' \| 'busy' \| 'offline'>` | ✅ | Current presence status | +| **lastSeen** | `string` | ✅ | ISO 8601 datetime of last activity | +| **currentLocation** | `string` | optional | Current page/route user is viewing | +| **device** | `Enum<'desktop' \| 'mobile' \| 'tablet' \| 'other'>` | optional | Device type | +| **customStatus** | `string` | optional | Custom user status message | +| **metadata** | `Record` | optional | Additional custom presence data | + --- @@ -287,6 +343,13 @@ Event pattern (supports wildcards like "record.*" or "*.created") | **position** | `number` | ✅ | Cursor position (character offset from start) | | **selection** | `{ start: number; end: number }` | optional | Text selection range (if text is selected) | +### Nested Shape: `SimpleCursorPosition.selection` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **start** | `number` | ✅ | Selection start position | +| **end** | `number` | ✅ | Selection end position | + --- @@ -316,6 +379,16 @@ Event pattern (supports wildcards like "record.*" or "*.created") | **timestamp** | `string` | ✅ | ISO 8601 datetime when message was sent | | **subscription** | `{ subscriptionId: string; events: string[]; objects?: string[]; filters?: any; … }` | ✅ | Subscription configuration | +### Nested Shape: `SubscribeMessage.subscription` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **subscriptionId** | `string` | ✅ | Unique subscription identifier | +| **events** | `string[]` | ✅ | Event patterns to subscribe to (supports wildcards, e.g., "record.*", "user.created") | +| **objects** | `string[]` | optional | Object names to filter events by (e.g., ["account", "contact"]) | +| **filters** | `any` | optional | Filter conditions for event payloads (not yet enforced — the runtime filters by object name and event type only) | +| **channels** | `string[]` | optional | Channel names for scoped subscriptions | + --- @@ -330,6 +403,12 @@ Event pattern (supports wildcards like "record.*" or "*.created") | **timestamp** | `string` | ✅ | ISO 8601 datetime when message was sent | | **request** | `{ subscriptionId: string }` | ✅ | Unsubscribe request | +### Nested Shape: `UnsubscribeMessage.request` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **subscriptionId** | `string` | ✅ | Subscription ID to unsubscribe from | + --- @@ -395,6 +474,16 @@ This schema accepts one of the following structures: | **timestamp** | `string` | ✅ | ISO 8601 datetime when message was sent | | **subscription** | `{ subscriptionId: string; events: string[]; objects?: string[]; filters?: any; … }` | ✅ | Subscription configuration | +### Nested Shape: `WebSocketMessage.subscription` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **subscriptionId** | `string` | ✅ | Unique subscription identifier | +| **events** | `string[]` | ✅ | Event patterns to subscribe to (supports wildcards, e.g., "record.*", "user.created") | +| **objects** | `string[]` | optional | Object names to filter events by (e.g., ["account", "contact"]) | +| **filters** | `any` | optional | Filter conditions for event payloads (not yet enforced — the runtime filters by object name and event type only) | +| **channels** | `string[]` | optional | Channel names for scoped subscriptions | + --- #### Option 2 @@ -410,6 +499,12 @@ This schema accepts one of the following structures: | **timestamp** | `string` | ✅ | ISO 8601 datetime when message was sent | | **request** | `{ subscriptionId: string }` | ✅ | Unsubscribe request | +### Nested Shape: `WebSocketMessage.request` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **subscriptionId** | `string` | ✅ | Subscription ID to unsubscribe from | + --- #### Option 3 @@ -444,6 +539,19 @@ This schema accepts one of the following structures: | **timestamp** | `string` | ✅ | ISO 8601 datetime when message was sent | | **presence** | `{ userId: string; sessionId: string; status: Enum<'online' \| 'away' \| 'busy' \| 'offline'>; lastSeen: string; … }` | ✅ | Presence state | +### Nested Shape: `WebSocketMessage.presence` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **userId** | `string` | ✅ | User identifier | +| **sessionId** | `string` | ✅ | Unique session identifier | +| **status** | `Enum<'online' \| 'away' \| 'busy' \| 'offline'>` | ✅ | Current presence status | +| **lastSeen** | `string` | ✅ | ISO 8601 datetime of last activity | +| **currentLocation** | `string` | optional | Current page/route user is viewing | +| **device** | `Enum<'desktop' \| 'mobile' \| 'tablet' \| 'other'>` | optional | Device type | +| **customStatus** | `string` | optional | Custom user status message | +| **metadata** | `Record` | optional | Additional custom presence data | + --- #### Option 5 @@ -459,6 +567,19 @@ This schema accepts one of the following structures: | **timestamp** | `string` | ✅ | ISO 8601 datetime when message was sent | | **cursor** | `{ userId: string; sessionId: string; documentId: string; position?: object; … }` | ✅ | Cursor position | +### Nested Shape: `WebSocketMessage.cursor` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **userId** | `string` | ✅ | User identifier | +| **sessionId** | `string` | ✅ | Session identifier | +| **documentId** | `string` | ✅ | Document identifier being edited | +| **position** | `{ line: integer; column: integer }` | optional | Cursor position in document | +| **selection** | `{ start: object; end: object }` | optional | Selection range (if text is selected) | +| **color** | `string` | optional | Cursor color for visual representation | +| **userName** | `string` | optional | Display name of user | +| **lastUpdate** | `string` | ✅ | ISO 8601 datetime of last cursor update | + --- #### Option 6 @@ -474,6 +595,22 @@ This schema accepts one of the following structures: | **timestamp** | `string` | ✅ | ISO 8601 datetime when message was sent | | **operation** | `{ operationId: string; documentId: string; userId: string; sessionId: string; … }` | ✅ | Edit operation | +### Nested Shape: `WebSocketMessage.operation` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **operationId** | `string` | ✅ | Unique operation identifier | +| **documentId** | `string` | ✅ | Document identifier | +| **userId** | `string` | ✅ | User who performed the edit | +| **sessionId** | `string` | ✅ | Session identifier | +| **type** | `Enum<'insert' \| 'delete' \| 'replace'>` | ✅ | Type of edit operation | +| **position** | `{ line: integer; column: integer }` | ✅ | Starting position of the operation | +| **endPosition** | `{ line: integer; column: integer }` | optional | Ending position (for delete/replace operations) | +| **content** | `string` | optional | Content to insert/replace | +| **version** | `integer` | ✅ | Document version before this operation | +| **timestamp** | `string` | ✅ | ISO 8601 datetime when operation was created | +| **baseOperationId** | `string` | optional | Previous operation ID this builds upon (for OT) | + --- #### Option 7 diff --git a/content/docs/references/automation/approval.mdx b/content/docs/references/automation/approval.mdx index 97e6e75552..c4a2deb1df 100644 --- a/content/docs/references/automation/approval.mdx +++ b/content/docs/references/automation/approval.mdx @@ -77,6 +77,36 @@ const result = ApprovalDecision.parse(data); | **escalation** | `{ enabled: boolean; timeoutHours: number; action: Enum<'reassign' \| 'auto_approve' \| 'auto_reject' \| 'notify'>; escalateTo?: string; … }` | optional | Per-node SLA escalation | | **maxRevisions** | `integer` | optional (default: `3`) | Max send-backs for revision before auto-reject (0 = send-back disabled) | +### Nested Shape: `ApprovalNodeConfig.approvers[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'manager' \| 'position' \| 'department' \| 'team' \| 'field' \| 'expression' \| …>` | ✅ | | +| **value** | `string` | optional | User id / membership tier / position / team / department / field — per `type`; for `expression`, a CEL expression over `current.*` / `trigger.*` / `vars.*` | +| **resolveAs** | `Enum<'user' \| 'department' \| 'position' \| 'team'>` | optional | How an `expression` result is expanded into approvers (default 'user') | +| **group** | `string` | optional | Group label for per_group sign-off (e.g. "legal", "finance") | +| **organization** | `string` | optional | ADR-0105 D9 — organization whose directory resolves this approver: `$root` (group org), `$parent` (one level up), or an organization slug. Omitted = the request's own organization. | + +### Nested Shape: `ApprovalNodeConfig.decisionOutputs[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **key** | `string` | ✅ | Output key (the flow variable name under the node id) | +| **label** | `string` | optional | Field label in the decision dialog | +| **type** | `Enum<'text' \| 'user' \| 'department' \| 'position' \| 'team'>` | optional | Decision-dialog input widget (default 'text') | +| **multiple** | `boolean` | optional | Collect multiple values (id array) | +| **required** | `boolean` | optional | Approver must supply this output to approve | + +### Nested Shape: `ApprovalNodeConfig.escalation` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `false`) | Enable SLA-based escalation for this node | +| **timeoutHours** | `number` | ✅ | Hours before escalation triggers | +| **action** | `Enum<'reassign' \| 'auto_approve' \| 'auto_reject' \| 'notify'>` | optional (default: `"notify"`) | Action on escalation timeout | +| **escalateTo** | `string` | optional | User id or position machine name to escalate to | +| **notifySubmitter** | `boolean` | optional (default: `true`) | Notify the original submitter on escalation | + --- diff --git a/content/docs/references/automation/bpmn-interop.mdx b/content/docs/references/automation/bpmn-interop.mdx index dcf0c0dad2..f1df189b8c 100644 --- a/content/docs/references/automation/bpmn-interop.mdx +++ b/content/docs/references/automation/bpmn-interop.mdx @@ -78,6 +78,17 @@ Options for exporting an ObjectStack flow as BPMN 2.0 XML | **prettyPrint** | `boolean` | optional (default: `true`) | Pretty-print XML output with indentation | | **namespacePrefix** | `string` | optional (default: `"bpmn"`) | XML namespace prefix for BPMN elements | +### Nested Shape: `BpmnExportOptions.customMappings[number]` + +Mapping between BPMN XML element and ObjectStack FlowNodeAction + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **bpmnType** | `string` | ✅ | BPMN XML element type (e.g., "bpmn:parallelGateway") | +| **flowNodeAction** | `string` | ✅ | ObjectStack FlowNodeAction value | +| **bidirectional** | `boolean` | optional (default: `true`) | Whether the mapping supports both import and export | +| **notes** | `string` | optional | Notes about mapping limitations | + --- @@ -96,6 +107,17 @@ Options for importing BPMN 2.0 XML into an ObjectStack flow | **flowName** | `string` | optional | Override flow name (defaults to BPMN process name) | | **validateAfterImport** | `boolean` | optional (default: `true`) | Validate imported flow against FlowSchema after import | +### Nested Shape: `BpmnImportOptions.customMappings[number]` + +Mapping between BPMN XML element and ObjectStack FlowNodeAction + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **bpmnType** | `string` | ✅ | BPMN XML element type (e.g., "bpmn:parallelGateway") | +| **flowNodeAction** | `string` | ✅ | ObjectStack FlowNodeAction value | +| **bidirectional** | `boolean` | optional (default: `true`) | Whether the mapping supports both import and export | +| **notes** | `string` | optional | Notes about mapping limitations | + --- @@ -112,6 +134,17 @@ Result of a BPMN import/export operation | **mappedCount** | `integer` | optional (default: `0`) | Number of elements successfully mapped | | **unmappedCount** | `integer` | optional (default: `0`) | Number of elements that could not be mapped | +### Nested Shape: `BpmnInteropResult.diagnostics[number]` + +Diagnostic message from BPMN import/export + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **severity** | `Enum<'info' \| 'warning' \| 'error'>` | ✅ | Diagnostic severity | +| **message** | `string` | ✅ | Diagnostic message | +| **bpmnElementId** | `string` | optional | BPMN element ID related to this diagnostic | +| **nodeId** | `string` | optional | ObjectStack node ID related to this diagnostic | + --- diff --git a/content/docs/references/automation/builtin-node-config.mdx b/content/docs/references/automation/builtin-node-config.mdx index e445660bad..2a4f991a06 100644 --- a/content/docs/references/automation/builtin-node-config.mdx +++ b/content/docs/references/automation/builtin-node-config.mdx @@ -164,6 +164,19 @@ const result = CreateRecordConfigSchema.parse(data); | **recordId** | `string` | optional | Object form only: id of the record to edit (required for mode: 'edit' to be useful) | | **defaults** | `Record` | optional | Object form only: prefilled values | +### Nested Shape: `ScreenConfig.fields[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Field name (the flow variable the value binds to) | +| **label** | `string` | optional | Display label | +| **type** | `string` | optional | Input type | +| **required** | `boolean` | optional | Whether a value is required to submit | +| **options** | `{ value: any; label: string }[]` | optional | Choices for a select-style field | +| **defaultValue** | `any` | optional | Prefilled value (interpolates `{token}` templates) | +| **placeholder** | `string` | optional | Input placeholder text | +| **visibleWhen** | `string` | optional | CEL predicate controlling visibility, evaluated client-side | + --- @@ -182,6 +195,13 @@ const result = CreateRecordConfigSchema.parse(data); | **placeholder** | `string` | optional | Input placeholder text | | **visibleWhen** | `string` | optional | CEL predicate controlling visibility, evaluated client-side | +### Nested Shape: `ScreenFieldConfig.options[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **value** | `any` | ✅ | Stored value | +| **label** | `string` | ✅ | Display label | + --- diff --git a/content/docs/references/automation/control-flow.mdx b/content/docs/references/automation/control-flow.mdx index 81fd7f8fff..c94ec88840 100644 --- a/content/docs/references/automation/control-flow.mdx +++ b/content/docs/references/automation/control-flow.mdx @@ -96,6 +96,34 @@ const result = FlowRegionSchema.parse(data); | **nodes** | `{ id: string; type: string; label: string; config?: Record; … }[]` | ✅ | Region body nodes (single-entry/single-exit sub-graph) | | **edges** | `{ id: string; source: string; target: string; condition?: string \| object; … }[]` | optional | Region body edges | +### Nested Shape: `FlowRegion.nodes[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Node unique ID | +| **type** | `string` | ✅ | Action type — a built-in FlowNodeAction id or a plugin-registered node type. Validated against the live action registry at registerFlow() (ADR-0018), not by a closed enum. | +| **label** | `string` | ✅ | Node label | +| **config** | `Record` | optional | Node configuration | +| **connectorConfig** | `{ connectorId: string; actionId: string; input?: Record }` | optional | | +| **position** | `{ x: number; y: number }` | optional | | +| **timeoutMs** | `integer` | optional | Maximum execution time for this node in milliseconds | +| **inputSchema** | `Record; required?: boolean; description?: string }>` | optional | Input parameter schema for this node | +| **outputSchema** | `never` | optional | [REMOVED] `flow.nodes[].outputSchema` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — it was never validated: the engine does not check node outputs against it, so it documented a contract nothing enforced. Delete the key. Downstream nodes read prior outputs via expressions (`{{nodeId.field}}`) regardless of any declaration. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **waitEventConfig** | `{ eventType: Enum<'timer' \| 'signal' \| 'webhook' \| 'manual' \| 'condition'>; timerDuration?: string; signalName?: string }` | optional | Configuration for wait node event resumption | +| **boundaryConfig** | `{ attachedToNodeId: string; eventType: Enum<'error' \| 'timer' \| 'signal' \| 'cancel'>; interrupting?: boolean; errorCode?: string; … }` | optional | Configuration for boundary events attached to host nodes | + +### Nested Shape: `FlowRegion.edges[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Edge unique ID | +| **source** | `string` | ✅ | Source Node ID | +| **target** | `string` | ✅ | Target Node ID | +| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) returning boolean used for branching. | +| **type** | `Enum<'default' \| 'fault' \| 'conditional' \| 'back'>` | optional (default: `"default"`) | Connection type: default (normal flow), fault (error path), conditional (expression-guarded), or back (ADR-0044 declared back-edge — traversed normally at run time, but excluded from DAG cycle validation so a revise/rework loop can re-enter an earlier node) | +| **label** | `string` | optional | Label on the connector | +| **isDefault** | `boolean` | optional (default: `false`) | BPMN default flow: traverse this edge only when no sibling conditional edge of the same source node matched. Mutually exclusive with `condition`; at most one per source node. | + --- @@ -111,6 +139,13 @@ const result = FlowRegionSchema.parse(data); | **maxIterations** | `integer` | optional | Hard cap on iterations (clamped to the engine ceiling) | | **body** | `{ nodes: object[]; edges?: object[] }` | optional | Loop body region (omit for legacy flat-graph loops) | +### Nested Shape: `LoopConfig.body` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **nodes** | `{ id: string; type: string; label: string; config?: Record; … }[]` | ✅ | Region body nodes (single-entry/single-exit sub-graph) | +| **edges** | `{ id: string; source: string; target: string; condition?: string \| object; … }[]` | optional | Region body edges | + --- @@ -124,6 +159,34 @@ const result = FlowRegionSchema.parse(data); | **nodes** | `{ id: string; type: string; label: string; config?: Record; … }[]` | ✅ | Branch body nodes | | **edges** | `{ id: string; source: string; target: string; condition?: string \| object; … }[]` | optional | Branch body edges | +### Nested Shape: `ParallelBranch.nodes[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Node unique ID | +| **type** | `string` | ✅ | Action type — a built-in FlowNodeAction id or a plugin-registered node type. Validated against the live action registry at registerFlow() (ADR-0018), not by a closed enum. | +| **label** | `string` | ✅ | Node label | +| **config** | `Record` | optional | Node configuration | +| **connectorConfig** | `{ connectorId: string; actionId: string; input?: Record }` | optional | | +| **position** | `{ x: number; y: number }` | optional | | +| **timeoutMs** | `integer` | optional | Maximum execution time for this node in milliseconds | +| **inputSchema** | `Record; required?: boolean; description?: string }>` | optional | Input parameter schema for this node | +| **outputSchema** | `never` | optional | [REMOVED] `flow.nodes[].outputSchema` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — it was never validated: the engine does not check node outputs against it, so it documented a contract nothing enforced. Delete the key. Downstream nodes read prior outputs via expressions (`{{nodeId.field}}`) regardless of any declaration. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **waitEventConfig** | `{ eventType: Enum<'timer' \| 'signal' \| 'webhook' \| 'manual' \| 'condition'>; timerDuration?: string; signalName?: string }` | optional | Configuration for wait node event resumption | +| **boundaryConfig** | `{ attachedToNodeId: string; eventType: Enum<'error' \| 'timer' \| 'signal' \| 'cancel'>; interrupting?: boolean; errorCode?: string; … }` | optional | Configuration for boundary events attached to host nodes | + +### Nested Shape: `ParallelBranch.edges[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Edge unique ID | +| **source** | `string` | ✅ | Source Node ID | +| **target** | `string` | ✅ | Target Node ID | +| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) returning boolean used for branching. | +| **type** | `Enum<'default' \| 'fault' \| 'conditional' \| 'back'>` | optional (default: `"default"`) | Connection type: default (normal flow), fault (error path), conditional (expression-guarded), or back (ADR-0044 declared back-edge — traversed normally at run time, but excluded from DAG cycle validation so a revise/rework loop can re-enter an earlier node) | +| **label** | `string` | optional | Label on the connector | +| **isDefault** | `boolean` | optional (default: `false`) | BPMN default flow: traverse this edge only when no sibling conditional edge of the same source node matched. Mutually exclusive with `condition`; at most one per source node. | + --- @@ -135,6 +198,14 @@ const result = FlowRegionSchema.parse(data); | :--- | :--- | :--- | :--- | | **branches** | `{ name?: string; nodes: object[]; edges?: object[] }[]` | ✅ | Branch regions executed concurrently; implicit join at block end | +### Nested Shape: `ParallelConfig.branches[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional | Branch label | +| **nodes** | `{ id: string; type: string; label: string; config?: Record; … }[]` | ✅ | Branch body nodes | +| **edges** | `{ id: string; source: string; target: string; condition?: string \| object; … }[]` | optional | Branch body edges | + --- @@ -165,6 +236,31 @@ const result = FlowRegionSchema.parse(data); | **errorVariable** | `string` | optional (default: `"$error"`) | Variable holding the caught error in the catch region | | **retry** | `{ maxRetries?: integer; backoffMs?: integer; backoffMultiplier?: number; maxRetryDelayMs?: integer; … }` | optional | Optional retry policy for the try region | +### Nested Shape: `TryCatchConfig.try` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **nodes** | `{ id: string; type: string; label: string; config?: Record; … }[]` | ✅ | Region body nodes (single-entry/single-exit sub-graph) | +| **edges** | `{ id: string; source: string; target: string; condition?: string \| object; … }[]` | optional | Region body edges | + +### Nested Shape: `TryCatchConfig.catch` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **nodes** | `{ id: string; type: string; label: string; config?: Record; … }[]` | ✅ | Region body nodes (single-entry/single-exit sub-graph) | +| **edges** | `{ id: string; source: string; target: string; condition?: string \| object; … }[]` | optional | Region body edges | + +### Nested Shape: `TryCatchConfig.retry` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **maxRetries** | `integer` | optional (default: `0`) | Retry attempts after the initial one. 0 (the default) means no retry — state a count to opt in. | +| **backoffMs** | `integer` | optional (default: `1000`) | Base delay before the first retry (ms); subsequent delays multiply by backoffMultiplier | +| **backoffMultiplier** | `number` | optional (default: `1`) | Exponential backoff multiplier; 1 (the default) keeps the delay flat | +| **maxRetryDelayMs** | `integer` | optional (default: `30000`) | Ceiling for a single backoff delay (ms) | +| **jitter** | `boolean` | optional (default: `false`) | Randomize each delay within [50%, 100%] of its computed value — spreads a thundering herd of simultaneous retries | +| **retryDelayMs** | `never` | optional | [REMOVED] `retryDelayMs` was removed in @objectstack/spec 17.0.0 (#4661, #4964) — the retry policy now has ONE spelling for its base delay across every surface that carries it: `job.retryPolicy`, a `try_catch` node's `retry` and `flow.errorHandling`. Rename the key to `backoffMs`; the value (milliseconds before the first retry) is unchanged. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | + --- diff --git a/content/docs/references/automation/execution.mdx b/content/docs/references/automation/execution.mdx index 225d295d14..4250d54214 100644 --- a/content/docs/references/automation/execution.mdx +++ b/content/docs/references/automation/execution.mdx @@ -114,6 +114,49 @@ const result = CheckpointSchema.parse(data); | **runAs** | `Enum<'system' \| 'user'>` | optional | Execution context identity | | **tenantId** | `string` | optional | Tenant ID for multi-tenant isolation | +### Nested Shape: `ExecutionLog.trigger` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `string` | ✅ | Trigger type (e.g., "record_change", "schedule", "api", "manual") | +| **recordId** | `string` | optional | Triggering record ID | +| **object** | `string` | optional | Triggering object name | +| **userId** | `string` | optional | User who triggered the execution | +| **metadata** | `Record` | optional | Additional trigger context | + +### Nested Shape: `ExecutionLog.steps[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **nodeId** | `string` | ✅ | Node ID that was executed | +| **nodeType** | `string` | ✅ | Node action type (e.g., "decision", "http") | +| **nodeLabel** | `string` | optional | Human-readable node label | +| **status** | `Enum<'success' \| 'failure' \| 'skipped'>` | ✅ | Step execution result | +| **startedAt** | `string` | ✅ | When the step started | +| **completedAt** | `string` | optional | When the step completed | +| **durationMs** | `integer` | optional | Step execution duration in milliseconds | +| **input** | `Record` | optional | Input data passed to the node | +| **output** | `Record` | optional | Output data produced by the node | +| **error** | `{ code: string; message: string; stack?: string }` | optional | Error details if step failed | +| **retryAttempt** | `integer` | optional | Retry attempt number (0 = first try) | +| **parentNodeId** | `string` | optional | Enclosing structured-region container node ID (loop/parallel/try_catch) | +| **iteration** | `integer` | optional | Zero-based loop iteration or parallel branch index of the enclosing region | +| **regionKind** | `string` | optional | Region kind the step ran in: loop-body \| parallel-branch \| try \| catch | +| **metrics** | `{ selected?: integer; acted?: integer; unmeasuredEffect?: boolean }` | optional | Records this step selected / acted on, as reported by the node executor | +| **skippedBy** | `{ nodeId: string; edgeId?: string; label?: string }` | optional | The gate that closed, when `status` is `skipped` | + +### Nested Shape: `ExecutionLog.summary` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **selected** | `integer` | ✅ | Total records read by the run | +| **acted** | `integer` | ✅ | Total records written / effects dispatched by the run | +| **skipped** | `integer` | ✅ | Total node executions a closed gate prevented | +| **unmeasured** | `integer` | optional | Total executions that may have caused an effect the platform cannot count. Absent = not tracked (an older run), which is not the same as zero. | +| **nodes** | `{ nodeId: string; nodeType: string; nodeLabel?: string; status: Enum<'success' \| 'failure' \| 'skipped'>; … }[]` | ✅ | Per-node breakdown, in first-execution order | +| **gates** | `{ nodeId: string; targetNodeId: string; edgeId?: string; label?: string; … }[]` | ✅ | Gates that closed during the run, most-skipped first | +| **detailOmitted** | `boolean` | optional | Set when persistence dropped `nodes`/`gates` to keep the stored row bounded — the totals are still exact. Declared so empty arrays are never mistaken for "nothing ran". | + --- @@ -156,6 +199,30 @@ const result = CheckpointSchema.parse(data); | **metrics** | `{ selected?: integer; acted?: integer; unmeasuredEffect?: boolean }` | optional | Records this step selected / acted on, as reported by the node executor | | **skippedBy** | `{ nodeId: string; edgeId?: string; label?: string }` | optional | The gate that closed, when `status` is `skipped` | +### Nested Shape: `ExecutionStepLog.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `string` | ✅ | Error code | +| **message** | `string` | ✅ | Error message | +| **stack** | `string` | optional | Stack trace | + +### Nested Shape: `ExecutionStepLog.metrics` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **selected** | `integer` | optional | Records this node READ or matched (a `get_record` query, a lookup) | +| **acted** | `integer` | optional | Records this node WROTE (created / updated / deleted) or effects it dispatched (notifications delivered) | +| **unmeasuredEffect** | `boolean` | optional | This execution may have caused an effect the platform cannot count (an external write through a connector). NOT interchangeable with `acted: 0` — it says the count is unknown, not that it is zero. | + +### Nested Shape: `ExecutionStepLog.skippedBy` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **nodeId** | `string` | ✅ | Node whose out-edge did not open (the gate) | +| **edgeId** | `string` | optional | Edge whose condition evaluated false | +| **label** | `string` | optional | Edge label, when the flow names its branches | + --- @@ -234,6 +301,31 @@ const result = CheckpointSchema.parse(data); | **gates** | `{ nodeId: string; targetNodeId: string; edgeId?: string; label?: string; … }[]` | ✅ | Gates that closed during the run, most-skipped first | | **detailOmitted** | `boolean` | optional | Set when persistence dropped `nodes`/`gates` to keep the stored row bounded — the totals are still exact. Declared so empty arrays are never mistaken for "nothing ran". | +### Nested Shape: `FlowRunSummary.nodes[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **nodeId** | `string` | ✅ | Node ID | +| **nodeType** | `string` | ✅ | Node action type (e.g., "get_record", "decision") | +| **nodeLabel** | `string` | optional | Human-readable node label | +| **status** | `Enum<'success' \| 'failure' \| 'skipped'>` | ✅ | Terminal status of the node across the run — `failure` if any execution failed, else `success` if any succeeded, else `skipped` | +| **runs** | `integer` | ✅ | Times the node executed (loop iterations and parallel branches each count) | +| **failures** | `integer` | ✅ | Executions that failed | +| **skipped** | `integer` | ✅ | Times a closed gate kept this node from running at all | +| **selected** | `integer` | optional | Records read across every execution — omitted for a node that reads none | +| **acted** | `integer` | optional | Records written / effects dispatched across every execution — omitted for a node that writes none | +| **unmeasured** | `integer` | optional | Executions that may have caused an effect the platform cannot count (see ExecutionStepMetrics.unmeasuredEffect) | + +### Nested Shape: `FlowRunSummary.gates[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **nodeId** | `string` | ✅ | Node whose out-edge did not open (the gate) | +| **targetNodeId** | `string` | ✅ | Node the closed edge would have run | +| **edgeId** | `string` | optional | Edge whose condition evaluated false | +| **label** | `string` | optional | Edge label, when the flow names its branches | +| **skipped** | `integer` | ✅ | Times this gate evaluated false (once per loop iteration) | + --- diff --git a/content/docs/references/automation/flow.mdx b/content/docs/references/automation/flow.mdx index f2a9f502e1..c968db93d1 100644 --- a/content/docs/references/automation/flow.mdx +++ b/content/docs/references/automation/flow.mdx @@ -63,6 +63,65 @@ const result = FlowSchema.parse(data); | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `Flow.variables[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Variable name | +| **type** | `string` | ✅ | Data type (text, number, boolean, object, list) | +| **isInput** | `boolean` | optional (default: `false`) | Is input parameter | +| **isOutput** | `boolean` | optional (default: `false`) | Is output parameter | +| **defaultValue** | `any` | optional | Value bound at run start when no parameter supplies one — this is what makes a declared variable always bound. An explicitly supplied param wins, including `false` and `null`; the boundary is `params[name] !== undefined`. | + +### Nested Shape: `Flow.nodes[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Node unique ID | +| **type** | `string` | ✅ | Action type — a built-in FlowNodeAction id or a plugin-registered node type. Validated against the live action registry at registerFlow() (ADR-0018), not by a closed enum. | +| **label** | `string` | ✅ | Node label | +| **config** | `Record` | optional | Node configuration | +| **connectorConfig** | `{ connectorId: string; actionId: string; input?: Record }` | optional | | +| **position** | `{ x: number; y: number }` | optional | | +| **timeoutMs** | `integer` | optional | Maximum execution time for this node in milliseconds | +| **inputSchema** | `Record; required?: boolean; description?: string }>` | optional | Input parameter schema for this node | +| **outputSchema** | `never` | optional | [REMOVED] `flow.nodes[].outputSchema` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — it was never validated: the engine does not check node outputs against it, so it documented a contract nothing enforced. Delete the key. Downstream nodes read prior outputs via expressions (`{{nodeId.field}}`) regardless of any declaration. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **waitEventConfig** | `{ eventType: Enum<'timer' \| 'signal' \| 'webhook' \| 'manual' \| 'condition'>; timerDuration?: string; signalName?: string }` | optional | Configuration for wait node event resumption | +| **boundaryConfig** | `{ attachedToNodeId: string; eventType: Enum<'error' \| 'timer' \| 'signal' \| 'cancel'>; interrupting?: boolean; errorCode?: string; … }` | optional | Configuration for boundary events attached to host nodes | + +### Nested Shape: `Flow.edges[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Edge unique ID | +| **source** | `string` | ✅ | Source Node ID | +| **target** | `string` | ✅ | Target Node ID | +| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) returning boolean used for branching. | +| **type** | `Enum<'default' \| 'fault' \| 'conditional' \| 'back'>` | optional (default: `"default"`) | Connection type: default (normal flow), fault (error path), conditional (expression-guarded), or back (ADR-0044 declared back-edge — traversed normally at run time, but excluded from DAG cycle validation so a revise/rework loop can re-enter an earlier node) | +| **label** | `string` | optional | Label on the connector | +| **isDefault** | `boolean` | optional (default: `false`) | BPMN default flow: traverse this edge only when no sibling conditional edge of the same source node matched. Mutually exclusive with `condition`; at most one per source node. | + +### Nested Shape: `Flow.errorHandling` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **strategy** | `Enum<'fail' \| 'retry' \| 'continue'>` | optional (default: `"fail"`) | How to handle node execution errors. 'retry' governs ONE synchronous dispatch: a durable pause (approval/screen/wait) ends the retry-governed segment, so a failure after the run resumes is not retried. | +| **maxRetries** | `integer` | optional (default: `0`) | Retry attempts after the initial one. Read only under strategy: 'retry', which requires >= 1; 0 (the default) means no retry. | +| **backoffMs** | `integer` | optional (default: `1000`) | Base delay before the first retry (ms); subsequent delays multiply by backoffMultiplier | +| **backoffMultiplier** | `number` | optional (default: `1`) | Exponential backoff multiplier; 1 (the default) keeps the delay flat | +| **maxRetryDelayMs** | `integer` | optional (default: `30000`) | Ceiling for a single backoff delay (ms) | +| **jitter** | `boolean` | optional (default: `false`) | Randomize each delay within [50%, 100%] of its computed value — spreads a thundering herd of simultaneous retries | +| **retryDelayMs** | `never` | optional | [REMOVED] `retryDelayMs` was removed in @objectstack/spec 17.0.0 (#4661, #4964) — the retry policy now has ONE spelling for its base delay across every surface that carries it: `job.retryPolicy`, a `try_catch` node's `retry` and `flow.errorHandling`. Rename the key to `backoffMs`; the value (milliseconds before the first retry) is unchanged. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **fallbackNodeId** | `never` | optional | [REMOVED] `flow.errorHandling.fallbackNodeId` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — the engine routes unrecoverable node errors via per-node fault edges (an edge with type: 'fault'), and never read this key: a fallback configured here silently did not exist. Delete the key and draw a fault edge from the failing node to the handler node instead. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | + +### Nested Shape: `Flow.protection` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | ✅ | Lock policy — none \| no-overlay \| no-delete \| full. | +| **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | +| **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | + --- @@ -101,6 +160,43 @@ const result = FlowSchema.parse(data); | **waitEventConfig** | `{ eventType: Enum<'timer' \| 'signal' \| 'webhook' \| 'manual' \| 'condition'>; timerDuration?: string; signalName?: string }` | optional | Configuration for wait node event resumption | | **boundaryConfig** | `{ attachedToNodeId: string; eventType: Enum<'error' \| 'timer' \| 'signal' \| 'cancel'>; interrupting?: boolean; errorCode?: string; … }` | optional | Configuration for boundary events attached to host nodes | +### Nested Shape: `FlowNode.connectorConfig` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **connectorId** | `string` | ✅ | Registered connector name | +| **actionId** | `string` | ✅ | Action key declared by the connector | +| **input** | `Record` | optional | Mapped inputs for the action | + +### Nested Shape: `FlowNode.inputSchema[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'string' \| 'number' \| 'boolean' \| 'object' \| 'array'>` | ✅ | Parameter type | +| **required** | `boolean` | optional (default: `false`) | Whether the parameter is required | +| **description** | `string` | optional | Parameter description | + +### Nested Shape: `FlowNode.waitEventConfig` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **eventType** | `Enum<'timer' \| 'signal' \| 'webhook' \| 'manual' \| 'condition'>` | ✅ | What kind of event resumes the execution | +| **timerDuration** | `string` | optional | ISO 8601 duration (e.g., "PT1H") or wait time for timer events | +| **signalName** | `string` | optional | Named signal or webhook event to wait for | +| **timeoutMs** | `never` | optional | [REMOVED] `waitEventConfig.timeoutMs` was removed in @objectstack/spec 17 (#4158). It documented a timeout guard that never existed: nothing ever failed or resumed a wait on a deadline. Its only reader treated it as the timer DURATION when `timerDuration` was absent, so use `timerDuration` — but QUOTE the number: the key is a string, and a bare numeric string is read as milliseconds, making `timeoutMs: 60000` and `timerDuration: '60000'` the same wait (`timerDuration: 'PT1M'` is the ISO 8601 spelling of that same 60s). Stored flows are converted automatically — the conversion does the quoting for you. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **onTimeout** | `never` | optional | [REMOVED] `waitEventConfig.onTimeout` was removed in @objectstack/spec 17 (#4158). It had no readers at all — no code path ever inspected it, so neither `fail` nor `continue` ever happened. Delete the key. There is no replacement: `wait` has no timeout, and a wait node resumes only when its timer elapses or its signal arrives. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | + +### Nested Shape: `FlowNode.boundaryConfig` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **attachedToNodeId** | `string` | ✅ | Host node ID this boundary event monitors | +| **eventType** | `Enum<'error' \| 'timer' \| 'signal' \| 'cancel'>` | ✅ | Boundary event trigger type | +| **interrupting** | `boolean` | optional (default: `true`) | If true, the host activity is cancelled when this event fires | +| **errorCode** | `string` | optional | Specific error code to catch (empty = catch all errors) | +| **timerDuration** | `string` | optional | ISO 8601 duration for timer boundary events | +| **signalName** | `string` | optional | Named signal to catch | + --- @@ -160,6 +256,34 @@ const result = FlowSchema.parse(data); | **createdBy** | `string` | optional | User who created this version | | **changeNote** | `string` | optional | Description of what changed in this version | +### Nested Shape: `FlowVersionHistory.definition` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Machine name | +| **label** | `string` | ✅ | Flow label | +| **description** | `string` | optional | | +| **successMessage** | `string` | optional | Message carried on AutomationResult for every terminal run (not only screen flows); the screen-flow UI shows it as a toast instead of a generic "Done". | +| **errorMessage** | `string` | optional | Message carried on AutomationResult for every terminal run (not only screen flows); the screen-flow UI shows it as a toast instead of the raw error. | +| **version** | `integer` | optional (default: `1`) | Version number | +| **status** | `Enum<'draft' \| 'active' \| 'obsolete' \| 'invalid'>` | optional (default: `"draft"`) | Deployment status | +| **template** | `never` | optional | [REMOVED] `flow.template` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no designer or engine path ever read it, so flagging a flow as a template/subflow did nothing. Delete the key. Shared logic is invoked via a subflow NODE referencing the flow by name. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **type** | `Enum<'autolaunched' \| 'record_change' \| 'schedule' \| 'screen' \| 'api'>` | ✅ | Flow type | +| **variables** | `{ name: string; type: string; isInput?: boolean; isOutput?: boolean; … }[]` | optional | Flow variables | +| **nodes** | `{ id: string; type: string; label: string; config?: Record; … }[]` | ✅ | Flow nodes | +| **edges** | `{ id: string; source: string; target: string; condition?: string \| object; … }[]` | ✅ | Flow connections | +| **active** | `never` | optional | [REMOVED] `flow.active` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — it never had an effect: the engine arms flows from `status`, and `active: false` did NOT stop a flow (worse, the default read as disabled while the engine treated unset as enabled). Delete the key. Use `status: 'obsolete'` (or 'invalid') to unbind and disable a flow, `status: 'active'` to arm it. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **runAs** | `Enum<'system' \| 'user'>` | optional (default: `"user"`) | Execution identity for the run: system = elevated (bypasses RLS), user = the triggering user (RLS-respecting). A run with no trigger user has no identity to scope to, so under user its data operations are REFUSED — declare system to make the elevation explicit. This covers schedule/time-relative/api triggers AND any record-change flow fired by a write that carried no user. | +| **errorHandling** | `{ strategy?: Enum<'fail' \| 'retry' \| 'continue'>; maxRetries?: integer; backoffMs?: integer; backoffMultiplier?: number; … }` | optional | Flow-level error handling configuration. A durable pause ends the retry-governed segment: strategy: 'retry' describes one synchronous dispatch, so a run that parks on an approval/screen/wait node and later resumes gets one attempt for anything that fails after the pause. Protect the post-pause half with its own failure handling in the flow — a try_catch node's retry around the post-resume work, or fault edges to a handler node. | +| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this flow. | +| **_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. | + --- diff --git a/content/docs/references/automation/schemaless-node-config.mdx b/content/docs/references/automation/schemaless-node-config.mdx index 2faf8a615e..53cb4aabce 100644 --- a/content/docs/references/automation/schemaless-node-config.mdx +++ b/content/docs/references/automation/schemaless-node-config.mdx @@ -138,6 +138,13 @@ const result = DecisionConditionSchema.parse(data); | :--- | :--- | :--- | :--- | | **conditions** | `{ label: string; expression: string }[]` | optional | Ordered decision branches (first true expression wins; omit to branch purely on edge conditions) | +### Nested Shape: `DecisionConfig.conditions[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string` | ✅ | Branch label; the winning branch resumes down the out-edge with this label (no match → the out-edge marked isDefault, or one labelled 'default') | +| **expression** | `string` | ✅ | Bare CEL predicate deciding this branch | + --- diff --git a/content/docs/references/automation/state-machine.mdx b/content/docs/references/automation/state-machine.mdx index ec2e29d7f5..536fa71a7a 100644 --- a/content/docs/references/automation/state-machine.mdx +++ b/content/docs/references/automation/state-machine.mdx @@ -146,6 +146,19 @@ Type: `string` | **states** | `Record; entry?: (string \| object)[]; exit?: (string \| object)[]; on?: Record; … }>` | ✅ | State Nodes | | **on** | `Record` | optional | | +### Nested Shape: `StateMachine.states[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'atomic' \| 'compound' \| 'parallel' \| 'final' \| 'history'>` | optional (default: `"atomic"`) | | +| **entry** | `(string \| { type: string; params?: Record })[]` | optional | Actions to run when entering this state | +| **exit** | `(string \| { type: string; params?: Record })[]` | optional | Actions to run when leaving this state | +| **on** | `Record` | optional | Map of Event Type -> Transition Definition | +| **always** | `{ target?: string; cond?: string \| object; actions?: (string \| object)[]; description?: string }[]` | optional | | +| **initial** | `string` | optional | Initial child state (if compound) | +| **states** | `Record; entry?: (string \| object)[]; exit?: (string \| object)[]; on?: Record; … }>` | optional | | +| **meta** | `{ label?: string; description?: string; color?: string; aiInstructions?: string }` | optional | | + --- @@ -164,6 +177,24 @@ Type: `string` | **states** | `Record` | optional | | | **meta** | `{ label?: string; description?: string; color?: string; aiInstructions?: string }` | optional | | +### Nested Shape: `StateNode.always[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.meta` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string` | optional | | +| **description** | `string` | optional | | +| **color** | `string` | optional | | +| **aiInstructions** | `string` | optional | Specific instructions for AI when in this state | + --- diff --git a/content/docs/references/automation/webhook.mdx b/content/docs/references/automation/webhook.mdx index ee450e6139..84d9bc4574 100644 --- a/content/docs/references/automation/webhook.mdx +++ b/content/docs/references/automation/webhook.mdx @@ -78,6 +78,14 @@ const result = WebhookSchema.parse(data); | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `Webhook.protection` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | ✅ | Lock policy — none \| no-overlay \| no-delete \| full. | +| **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | +| **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | + --- diff --git a/content/docs/references/cloud/app-store.mdx b/content/docs/references/cloud/app-store.mdx index 9156f340f5..1fe4636ee3 100644 --- a/content/docs/references/cloud/app-store.mdx +++ b/content/docs/references/cloud/app-store.mdx @@ -68,6 +68,62 @@ const result = AppDiscoveryRequestSchema.parse(data); | **newArrivals** | `{ listingId: string; name: string; tagline?: string; iconUrl?: string; … }[]` | optional | | | **collections** | `{ id: string; name: string; description?: string; coverImageUrl?: string; … }[]` | optional | | +### Nested Shape: `AppDiscoveryResponse.featured[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **listingId** | `string` | ✅ | | +| **name** | `string` | ✅ | | +| **tagline** | `string` | optional | | +| **iconUrl** | `string` | optional | | +| **category** | `Enum<'crm' \| 'erp' \| 'hr' \| 'finance' \| 'project' \| 'collaboration' \| 'analytics' \| …>` | ✅ | Marketplace package category | +| **pricing** | `Enum<'free' \| 'freemium' \| 'paid' \| 'subscription' \| 'usage-based' \| 'contact-sales'>` | ✅ | Package pricing model | +| **averageRating** | `number` | optional | | +| **activeInstalls** | `integer` | optional | | +| **reason** | `Enum<'popular-in-category' \| 'similar-users' \| 'complements-installed' \| 'trending' \| …>` | ✅ | | + +### Nested Shape: `AppDiscoveryResponse.recommended[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **listingId** | `string` | ✅ | | +| **name** | `string` | ✅ | | +| **tagline** | `string` | optional | | +| **iconUrl** | `string` | optional | | +| **category** | `Enum<'crm' \| 'erp' \| 'hr' \| 'finance' \| 'project' \| 'collaboration' \| 'analytics' \| …>` | ✅ | Marketplace package category | +| **pricing** | `Enum<'free' \| 'freemium' \| 'paid' \| 'subscription' \| 'usage-based' \| 'contact-sales'>` | ✅ | Package pricing model | +| **averageRating** | `number` | optional | | +| **activeInstalls** | `integer` | optional | | +| **reason** | `Enum<'popular-in-category' \| 'similar-users' \| 'complements-installed' \| 'trending' \| …>` | ✅ | | + +### Nested Shape: `AppDiscoveryResponse.trending[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **listingId** | `string` | ✅ | | +| **name** | `string` | ✅ | | +| **tagline** | `string` | optional | | +| **iconUrl** | `string` | optional | | +| **category** | `Enum<'crm' \| 'erp' \| 'hr' \| 'finance' \| 'project' \| 'collaboration' \| 'analytics' \| …>` | ✅ | Marketplace package category | +| **pricing** | `Enum<'free' \| 'freemium' \| 'paid' \| 'subscription' \| 'usage-based' \| 'contact-sales'>` | ✅ | Package pricing model | +| **averageRating** | `number` | optional | | +| **activeInstalls** | `integer` | optional | | +| **reason** | `Enum<'popular-in-category' \| 'similar-users' \| 'complements-installed' \| 'trending' \| …>` | ✅ | | + +### Nested Shape: `AppDiscoveryResponse.newArrivals[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **listingId** | `string` | ✅ | | +| **name** | `string` | ✅ | | +| **tagline** | `string` | optional | | +| **iconUrl** | `string` | optional | | +| **category** | `Enum<'crm' \| 'erp' \| 'hr' \| 'finance' \| 'project' \| 'collaboration' \| 'analytics' \| …>` | ✅ | Marketplace package category | +| **pricing** | `Enum<'free' \| 'freemium' \| 'paid' \| 'subscription' \| 'usage-based' \| 'contact-sales'>` | ✅ | Package pricing model | +| **averageRating** | `number` | optional | | +| **activeInstalls** | `integer` | optional | | +| **reason** | `Enum<'popular-in-category' \| 'similar-users' \| 'complements-installed' \| 'trending' \| …>` | ✅ | | + --- @@ -171,6 +227,24 @@ const result = AppDiscoveryRequestSchema.parse(data); | **pageSize** | `integer` | ✅ | | | **ratingSummary** | `{ averageRating: number; totalRatings: integer; distribution: object }` | optional | | +### Nested Shape: `ListReviewsResponse.items[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Review ID | +| **listingId** | `string` | ✅ | Listing being reviewed | +| **userId** | `string` | ✅ | Review author user ID | +| **displayName** | `string` | optional | Reviewer display name | +| **rating** | `integer` | ✅ | Star rating (1-5) | +| **title** | `string` | optional | Review title | +| **body** | `string` | optional | Review text | +| **appVersion** | `string` | optional | App version being reviewed | +| **moderationStatus** | `Enum<'pending' \| 'approved' \| 'flagged' \| 'rejected'>` | optional (default: `"pending"`) | | +| **helpfulCount** | `integer` | optional (default: `0`) | | +| **publisherResponse** | `{ body: string; respondedAt: string }` | optional | Publisher response to review | +| **submittedAt** | `string` | ✅ | | +| **updatedAt** | `string` | optional | | + --- diff --git a/content/docs/references/cloud/developer-portal.mdx b/content/docs/references/cloud/developer-portal.mdx index 2ee9ae5d8f..6c97ac7b93 100644 --- a/content/docs/references/cloud/developer-portal.mdx +++ b/content/docs/references/cloud/developer-portal.mdx @@ -148,6 +148,18 @@ const result = AnalyticsTimeRangeSchema.parse(data); | **timeSeries** | `Record` | optional | Time series keyed by metric name | | **ratingDistribution** | `{ 1: integer; 2: integer; 3: integer; 4: integer; … }` | optional | | +### Nested Shape: `PublishingAnalyticsResponse.summary` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **totalInstalls** | `integer` | ✅ | | +| **activeInstalls** | `integer` | ✅ | | +| **totalUninstalls** | `integer` | ✅ | | +| **averageRating** | `number` | optional | | +| **totalRatings** | `integer` | ✅ | | +| **totalRevenue** | `number` | optional | Revenue in cents | +| **pageViews** | `integer` | ✅ | | + --- diff --git a/content/docs/references/cloud/environment-package.mdx b/content/docs/references/cloud/environment-package.mdx index e7b02c4689..a4ba268ec7 100644 --- a/content/docs/references/cloud/environment-package.mdx +++ b/content/docs/references/cloud/environment-package.mdx @@ -106,6 +106,25 @@ List of packages installed in an environment | **packages** | `{ id: string; environmentId: string; packageVersionId: string; packageId: string; … }[]` | ✅ | Packages installed in this environment | | **total** | `number` | ✅ | Total count | +### Nested Shape: `ListEnvironmentPackagesResponse.packages[number]` + +Package installation record in an environment (sys_package_installation) + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique installation record ID | +| **environmentId** | `string` | ✅ | Environment this installation belongs to | +| **packageVersionId** | `string` | ✅ | UUID of the installed sys_package_version row | +| **packageId** | `string` | ✅ | UUID of the parent sys_package row (denormalized for constraint enforcement) | +| **status** | `Enum<'installed' \| 'installing' \| 'upgrading' \| 'disabled' \| 'error'>` | optional (default: `"installed"`) | Package installation status within an environment | +| **enabled** | `boolean` | optional (default: `true`) | Whether the package metadata is loaded | +| **settings** | `Record` | optional | Per-installation configuration settings | +| **withSampleData** | `boolean` | optional (default: `false`) | Replay the package seed datasets on next kernel cold-start | +| **installedAt** | `string` | ✅ | Installation timestamp (ISO-8601) | +| **installedBy** | `string` | optional | User ID of the installer | +| **updatedAt** | `string` | optional | Last update timestamp (ISO-8601) | +| **errorMessage** | `string` | optional | Error message when status is error | + --- diff --git a/content/docs/references/cloud/environment.mdx b/content/docs/references/cloud/environment.mdx index a13d9ae4f6..4731452c71 100644 --- a/content/docs/references/cloud/environment.mdx +++ b/content/docs/references/cloud/environment.mdx @@ -227,6 +227,51 @@ Public exposure of this environment artifacts (private | unlisted | public). | **warnings** | `string[]` | optional | Non-fatal warnings emitted during provisioning | | **hostnameAssignment** | `{ requestedHostname: string; assignedHostname: string }` | optional | Populated ONLY when the control plane auto-renamed the requested hostname to avoid a collision. Absent means the requested hostname was assigned unchanged — never read absence as "unknown". | +### Nested Shape: `ProvisionEnvironmentResponse.environment` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | UUID of the environment (stable, never reused) | +| **organizationId** | `string` | ✅ | Organization that owns this environment | +| **displayName** | `string` | ✅ | Display name shown in Studio and APIs | +| **isDefault** | `boolean` | optional (default: `false`) | Whether this is the default environment for the organization | +| **isSystem** | `boolean` | optional (default: `false`) | Whether this is a system environment (platform infrastructure, not user data) | +| **plan** | `string` | optional (default: `"free"`) | Plan tier for this environment | +| **status** | `Enum<'provisioning' \| 'active' \| 'suspended' \| 'archived' \| 'failed' \| 'migrating'>` | optional (default: `"provisioning"`) | Environment lifecycle status | +| **createdBy** | `string` | ✅ | User ID that created the environment | +| **createdAt** | `string` | ✅ | Creation timestamp (ISO-8601) | +| **updatedAt** | `string` | ✅ | Last update timestamp (ISO-8601) | +| **databaseUrl** | `string` | optional | Full connection URL for the environment database | +| **databaseDriver** | `string` | optional | Data-plane driver key (turso, libsql, sqlite, memory, postgres) | +| **storageLimitMb** | `integer` | optional | Storage quota in megabytes | +| **provisionedAt** | `string` | optional | Provisioning timestamp (ISO-8601) | +| **metadata** | `Record` | optional | Free-form metadata | +| **hostname** | `string` | optional | Canonical hostname for this environment. UNIQUE. Auto-set on creation; can be overridden for custom domains. | +| **consoleUrl** | `string` | optional | Pre-computed admin Console URL for this environment | +| **apiBaseUrl** | `string` | optional | Pre-computed REST API base URL for this environment | +| **visibility** | `Enum<'private' \| 'unlisted' \| 'public'>` | optional (default: `"private"`) | Public exposure of this environment artifacts (private \| unlisted \| public). | + +### Nested Shape: `ProvisionEnvironmentResponse.credential` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | UUID of the credential | +| **environmentId** | `string` | ✅ | Environment this credential authorizes | +| **secretCiphertext** | `string` | ✅ | Encrypted auth token or secret (ciphertext) | +| **encryptionKeyId** | `string` | ✅ | Encryption key ID used to encrypt the secret | +| **authorization** | `Enum<'full_access' \| 'read_only'>` | optional (default: `"full_access"`) | Authorization scope for this credential | +| **status** | `Enum<'active' \| 'rotating' \| 'revoked'>` | optional (default: `"active"`) | Credential lifecycle status | +| **createdAt** | `string` | ✅ | Creation timestamp (ISO-8601) | +| **expiresAt** | `string` | optional | Optional expiry timestamp | +| **revokedAt** | `string` | optional | Revocation timestamp (if revoked) | + +### Nested Shape: `ProvisionEnvironmentResponse.hostnameAssignment` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **requestedHostname** | `string` | ✅ | Hostname the caller asked for (explicitly, or as auto-derived from displayName) | +| **assignedHostname** | `string` | ✅ | Hostname actually assigned after the collision-avoiding rename; equals `environment.hostname` | + --- @@ -257,6 +302,16 @@ Public exposure of this environment artifacts (private | unlisted | public). | **durationMs** | `number` | ✅ | Total bootstrap duration in milliseconds | | **warnings** | `string[]` | optional | Non-fatal warnings | +### Nested Shape: `ProvisionOrganizationResponse.defaultEnvironment` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **environment** | `{ id: string; organizationId: string; displayName: string; isDefault: boolean; … }` | ✅ | Provisioned environment (includes database addressing) | +| **credential** | `{ id: string; environmentId: string; secretCiphertext: string; encryptionKeyId: string; … }` | ✅ | Freshly-minted credential for the environment DB | +| **durationMs** | `number` | ✅ | Total provisioning duration in milliseconds | +| **warnings** | `string[]` | optional | Non-fatal warnings emitted during provisioning | +| **hostnameAssignment** | `{ requestedHostname: string; assignedHostname: string }` | optional | Populated ONLY when the control plane auto-renamed the requested hostname to avoid a collision. Absent means the requested hostname was assigned unchanged — never read absence as "unknown". | + --- diff --git a/content/docs/references/cloud/marketplace-admin.mdx b/content/docs/references/cloud/marketplace-admin.mdx index 6ae9fc8e4f..6b7e5b8821 100644 --- a/content/docs/references/cloud/marketplace-admin.mdx +++ b/content/docs/references/cloud/marketplace-admin.mdx @@ -189,6 +189,17 @@ const result = CuratedCollectionSchema.parse(data); | **startedAt** | `string` | optional | | | **completedAt** | `string` | optional | | +### Nested Shape: `SubmissionReview.criteria[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Criterion ID | +| **category** | `Enum<'security' \| 'performance' \| 'quality' \| 'ux' \| 'documentation' \| 'policy' \| 'compatibility'>` | ✅ | | +| **description** | `string` | ✅ | | +| **required** | `boolean` | optional (default: `true`) | | +| **passed** | `boolean` | optional | | +| **notes** | `string` | optional | | + --- diff --git a/content/docs/references/cloud/marketplace.mdx b/content/docs/references/cloud/marketplace.mdx index dd1b304d15..b6d16017e6 100644 --- a/content/docs/references/cloud/marketplace.mdx +++ b/content/docs/references/cloud/marketplace.mdx @@ -149,6 +149,16 @@ Install from marketplace request | **artifactRef** | `{ url: string; sha256: string; size: integer; format: Enum<'tgz' \| 'zip'>; … }` | optional | Artifact reference for direct installation | | **tenantId** | `string` | optional | Tenant identifier | +### Nested Shape: `MarketplaceInstallRequest.artifactRef` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **url** | `string` | ✅ | Artifact download URL | +| **sha256** | `string` | ✅ | SHA256 checksum | +| **size** | `integer` | ✅ | Artifact size in bytes | +| **format** | `Enum<'tgz' \| 'zip'>` | optional (default: `"tgz"`) | Artifact format | +| **uploadedAt** | `string` | ✅ | Upload timestamp | + --- @@ -201,6 +211,39 @@ Public-facing package listing on the marketplace | **updatedAt** | `string` | optional | Last updated timestamp | | **translations** | `Record` | optional | Locale-keyed overrides for name / tagline / description / screenshot captions | +### Nested Shape: `MarketplaceListing.versions[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **version** | `string` | ✅ | Version string | +| **releaseDate** | `string` | ✅ | Release date | +| **releaseNotes** | `string` | optional | Release notes | +| **minPlatformVersion** | `string` | optional | Minimum platform version | +| **deprecated** | `boolean` | optional (default: `false`) | Whether this version is deprecated | +| **artifact** | `{ url: string; sha256: string; size: integer; format: Enum<'tgz' \| 'zip'>; … }` | optional | Downloadable artifact for this version | + +### Nested Shape: `MarketplaceListing.stats` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **totalInstalls** | `integer` | optional (default: `0`) | Total installs | +| **activeInstalls** | `integer` | optional (default: `0`) | Active installs | +| **averageRating** | `number` | optional | Average user rating (0-5) | +| **totalRatings** | `integer` | optional (default: `0`) | Total ratings count | +| **totalReviews** | `integer` | optional (default: `0`) | Total reviews count | + +### Nested Shape: `MarketplaceListing.translations[string]` + +Per-locale overrides for a package listing + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **displayName** | `string` | optional | Localized display name | +| **description** | `string` | optional | Localized short description | +| **readme** | `string` | optional | Localized long-form readme (markdown) | +| **tagline** | `string` | optional | Localized short tagline (marketplace listing only) | +| **screenshotCaptions** | `Record` | optional | Per-index screenshot caption overrides | + --- @@ -240,6 +283,37 @@ Marketplace search response | **pageSize** | `integer` | ✅ | Items per page | | **facets** | `{ categories?: object[]; pricing?: object[] }` | optional | Aggregation facets for refining search | +### Nested Shape: `MarketplaceSearchResponse.items[number]` + +Public-facing package listing on the marketplace + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Listing ID (matches package manifest ID) | +| **packageId** | `string` | ✅ | Package identifier | +| **packageType** | `Enum<'app'>` | optional (default: `"app"`) | Consumer-installable package type (ADR-0019: only `app` is listable) | +| **publisherId** | `string` | ✅ | Publisher ID | +| **status** | `Enum<'draft' \| 'submitted' \| 'in-review' \| 'approved' \| 'published' \| 'rejected' \| …>` | optional (default: `"draft"`) | Publication state: draft, published, under-review, suspended, deprecated, or unlisted | +| **name** | `string` | ✅ | Display name | +| **tagline** | `string` | optional | Short tagline (max 120 chars) | +| **description** | `string` | optional | Full description (Markdown) | +| **category** | `Enum<'crm' \| 'erp' \| 'hr' \| 'finance' \| 'project' \| 'collaboration' \| 'analytics' \| …>` | ✅ | Package category | +| **tags** | `string[]` | optional | Search tags | +| **iconUrl** | `string` | optional | Package icon URL | +| **screenshots** | `{ url: string; caption?: string }[]` | optional | Screenshots | +| **documentationUrl** | `string` | optional | Documentation URL | +| **supportUrl** | `string` | optional | Support URL | +| **repositoryUrl** | `string` | optional | Source repository URL | +| **pricing** | `Enum<'free' \| 'freemium' \| 'paid' \| 'subscription' \| 'usage-based' \| 'contact-sales'>` | optional (default: `"free"`) | Pricing model | +| **priceInCents** | `integer` | optional | Price in cents (e.g. 999 = $9.99) | +| **latestVersion** | `string` | ✅ | Latest published version | +| **minPlatformVersion** | `string` | optional | Minimum ObjectStack platform version | +| **versions** | `{ version: string; releaseDate: string; releaseNotes?: string; minPlatformVersion?: string; … }[]` | optional | Published versions | +| **stats** | `{ totalInstalls: integer; activeInstalls: integer; averageRating?: number; totalRatings: integer; … }` | optional | Aggregate marketplace statistics | +| **publishedAt** | `string` | optional | First published timestamp | +| **updatedAt** | `string` | optional | Last updated timestamp | +| **translations** | `Record` | optional | Locale-keyed overrides for name / tagline / description / screenshot captions | + --- diff --git a/content/docs/references/cloud/package-version.mdx b/content/docs/references/cloud/package-version.mdx index 13b5e49285..d3a4480432 100644 --- a/content/docs/references/cloud/package-version.mdx +++ b/content/docs/references/cloud/package-version.mdx @@ -88,6 +88,16 @@ Package manifest snapshot embedded in a package version | **configurationSchema** | `Record` | optional | JSON Schema for per-installation configuration properties | | **metadata** | `Record` | optional | Extension metadata | +### Nested Shape: `PackageManifest.dependencies[number]` + +Package dependency declaration + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **packageId** | `string` | ✅ | Manifest ID of the dependency | +| **versionRange** | `string` | ✅ | Semver version range (e.g. ^1.0.0) | +| **optional** | `boolean` | optional (default: `false`) | Whether this dependency is optional | + --- diff --git a/content/docs/references/cloud/package.mdx b/content/docs/references/cloud/package.mdx index 0a0ba8ebcb..0b51a92958 100644 --- a/content/docs/references/cloud/package.mdx +++ b/content/docs/references/cloud/package.mdx @@ -59,6 +59,18 @@ Register a new package in the Control Plane | **translations** | `Record` | optional | Locale-keyed overrides; missing keys fall back to base columns | | **createdBy** | `string` | ✅ | User ID creating the package | +### Nested Shape: `CreatePackageRequest.translations[string]` + +Per-locale overrides for a package listing + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **displayName** | `string` | optional | Localized display name | +| **description** | `string` | optional | Localized short description | +| **readme** | `string` | optional | Localized long-form readme (markdown) | +| **tagline** | `string` | optional | Localized short tagline (marketplace listing only) | +| **screenshotCaptions** | `Record` | optional | Per-index screenshot caption overrides | + --- @@ -88,6 +100,18 @@ Register a new package in the Control Plane | **updatedAt** | `string` | ✅ | Last update timestamp (ISO-8601) | | **createdBy** | `string` | ✅ | User ID that created the package | +### Nested Shape: `Package.translations[string]` + +Per-locale overrides for a package listing + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **displayName** | `string` | optional | Localized display name | +| **description** | `string` | optional | Localized short description | +| **readme** | `string` | optional | Localized long-form readme (markdown) | +| **tagline** | `string` | optional | Localized short tagline (marketplace listing only) | +| **screenshotCaptions** | `Record` | optional | Per-index screenshot caption overrides | + --- @@ -183,6 +207,18 @@ Update mutable package metadata | **isStarter** | `boolean` | optional | | | **translations** | `Record` | optional | Locale-keyed overrides; missing keys fall back to base columns | +### Nested Shape: `UpdatePackageRequest.translations[string]` + +Per-locale overrides for a package listing + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **displayName** | `string` | optional | Localized display name | +| **description** | `string` | optional | Localized short description | +| **readme** | `string` | optional | Localized long-form readme (markdown) | +| **tagline** | `string` | optional | Localized short tagline (marketplace listing only) | +| **screenshotCaptions** | `Record` | optional | Per-index screenshot caption overrides | + --- diff --git a/content/docs/references/cloud/template-manifest.mdx b/content/docs/references/cloud/template-manifest.mdx index b864f86cc3..3ba9f63eda 100644 --- a/content/docs/references/cloud/template-manifest.mdx +++ b/content/docs/references/cloud/template-manifest.mdx @@ -61,6 +61,18 @@ objectstack.manifest.json — template / package source descriptor | **scaffold** | `{ variables?: Record; postInstall?: string[] }` | optional | | | **readmePath** | `string` | optional | Path (relative to manifest) to long-form README | +### Nested Shape: `TemplateManifest.translations[string]` + +Per-locale overrides for a package listing + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **displayName** | `string` | optional | Localized display name | +| **description** | `string` | optional | Localized short description | +| **readme** | `string` | optional | Localized long-form readme (markdown) | +| **tagline** | `string` | optional | Localized short tagline (marketplace listing only) | +| **screenshotCaptions** | `Record` | optional | Per-index screenshot caption overrides | + --- diff --git a/content/docs/references/cloud/tenant.mdx b/content/docs/references/cloud/tenant.mdx index 445f03b4dc..6aaf6e8873 100644 --- a/content/docs/references/cloud/tenant.mdx +++ b/content/docs/references/cloud/tenant.mdx @@ -89,6 +89,24 @@ const result = PackageInstallationSchema.parse(data); | **durationMs** | `number` | ✅ | Provisioning duration in milliseconds | | **warnings** | `string[]` | optional | Provisioning warnings | +### Nested Shape: `ProvisionTenantResponse.tenant` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique tenant database identifier (UUID) | +| **organizationId** | `string` | ✅ | Organization ID (foreign key to sys_organization) | +| **databaseName** | `string` | ✅ | Database name (UUID-based) | +| **databaseUrl** | `string` | ✅ | Full database URL | +| **authToken** | `string` | ✅ | Encrypted tenant-specific auth token | +| **status** | `Enum<'provisioning' \| 'active' \| 'suspended' \| 'archived' \| 'failed'>` | optional (default: `"provisioning"`) | Database status | +| **region** | `string` | ✅ | Deployment region | +| **plan** | `string` | optional (default: `"free"`) | Tenant plan tier | +| **storageLimitMb** | `integer` | ✅ | Storage limit in megabytes | +| **createdAt** | `string` | ✅ | Database creation timestamp | +| **updatedAt** | `string` | ✅ | Last update timestamp | +| **lastAccessedAt** | `string` | optional | Last accessed timestamp | +| **metadata** | `Record` | optional | Custom tenant configuration | + --- diff --git a/content/docs/references/data/analytics.mdx b/content/docs/references/data/analytics.mdx index dfeb127a00..b08e1aebdf 100644 --- a/content/docs/references/data/analytics.mdx +++ b/content/docs/references/data/analytics.mdx @@ -88,6 +88,43 @@ const result = AggregationMetricType.parse(data); | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `Cube.measures[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Unique metric ID | +| **label** | `string` | ✅ | Human readable label | +| **description** | `string` | optional | | +| **type** | `Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct' \| 'number' \| 'string' \| 'boolean'>` | ✅ | | +| **sql** | `string` | ✅ | SQL expression or field reference | +| **format** | `string` | optional | | + +### Nested Shape: `Cube.dimensions[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Unique dimension ID | +| **label** | `string` | ✅ | Human readable label | +| **description** | `string` | optional | | +| **type** | `Enum<'string' \| 'number' \| 'boolean' \| 'time' \| 'geo'>` | ✅ | | +| **sql** | `string` | ✅ | SQL expression or column reference | +| **granularities** | `Enum<'second' \| 'minute' \| 'hour' \| 'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>[]` | optional | | + +### Nested Shape: `Cube.joins[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Target cube name | +| **relationship** | `Enum<'one_to_one' \| 'one_to_many' \| 'many_to_one'>` | optional (default: `"many_to_one"`) | | +| **sql** | `string` | ✅ | Join condition (ON clause) | + +### Nested Shape: `Cube.refreshKey` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **every** | `string` | optional | Refresh interval (e.g. "1 hour") | +| **sql** | `string` | optional | SQL to check for data changes | + --- diff --git a/content/docs/references/data/data-engine.mdx b/content/docs/references/data/data-engine.mdx index ac9f8bbf69..045183e46d 100644 --- a/content/docs/references/data/data-engine.mdx +++ b/content/docs/references/data/data-engine.mdx @@ -38,6 +38,42 @@ const result = BaseEngineOptionsSchema.parse(data); | :--- | :--- | :--- | :--- | | **context** | `{ userId?: string; actor?: string; attributedUserId?: string; email?: string; … }` | optional | | +### Nested Shape: `BaseEngineOptions.context` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **userId** | `string` | optional | | +| **actor** | `string` | optional | | +| **attributedUserId** | `string` | optional | | +| **email** | `string` | optional | | +| **tenantId** | `string` | optional | | +| **timezone** | `string` | optional | | +| **locale** | `string` | optional | | +| **currency** | `string` | optional | | +| **positions** | `string[]` | optional (default: `[]`) | | +| **principalKind** | `Enum<'human' \| 'agent' \| 'service' \| 'guest' \| 'system'>` | optional | | +| **audience** | `Enum<'internal' \| 'external'>` | optional | | +| **posture** | `Enum<'PLATFORM_ADMIN' \| 'TENANT_ADMIN' \| 'MEMBER' \| 'EXTERNAL'>` | optional | ADR-0095 D2 posture rung — PLATFORM_ADMIN crosses the tenant wall where object posture permits; TENANT_ADMIN sees all rows in the org; MEMBER gets business RLS; EXTERNAL sees only explicitly shared rows. | +| **authGate** | `{ code: string; message: string }` | optional | ADR-0069 authentication-policy gate: present only while the principal is blocked from protected resources until they remediate (expired password, enforced MFA), absent for every healthy session. `code` is the stable machine code the client branches on (PASSWORD_EXPIRED / MFA_REQUIRED) and `message` is what the blocked user reads; both are required because the transport seam renders them as the 403 body. AUTHENTICATION, not authorization — it suspends access entirely rather than narrowing it, and nothing in the permission/RLS path reads it, while the allow-listed remediation endpoints stay reachable. Server-constructed only, never client-supplied; a guest/anonymous principal never carries one. | +| **onBehalfOf** | `{ userId: string; principalKind?: Enum<'human' \| 'agent' \| 'service' \| 'guest' \| 'system'> }` | optional | | +| **permissions** | `string[]` | optional (default: `[]`) | | +| **systemPermissions** | `string[]` | optional | | +| **tabPermissions** | `Record>` | optional | | +| **org_user_ids** | `string[]` | optional | | +| **accessible_org_ids** | `string[]` | optional | | +| **rlsMembership** | `Record` | optional | | +| **isSystem** | `boolean` | optional (default: `false`) | | +| **flowRunId** | `string` | optional | | +| **skipTriggers** | `boolean` | optional | | +| **skipAutomations** | `boolean` | optional | | +| **seedReplay** | `boolean` | optional | | +| **skipStateMachine** | `boolean` | optional | | +| **preserveAudit** | `boolean` | optional | Historical import: preserve the ORIGINAL audit timeline for this write instead of stamping it "now" (#3493). Opt-in and server-constructed only, never client-supplied. On the UPDATE path it admits a whitelist — the audit/timestamp family (created_at / created_by / updated_at / updated_by) plus author-declared business `readonly` fields — while platform-managed `system` columns (tenancy, generated) stay stripped. On INSERT the exemption does NOT apply (#6640): a create is stripped earlier, at the DataProtocol ingress, whose only exemption is `context.isSystem`, so a non-system create carrying `preserveAudit` still has those fields stripped and is warned (WARN) that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. Permissions / RLS / field-level security are unaffected. | +| **oauthScopes** | `string[]` | optional | | +| **accessToken** | `string` | optional | | +| **transaction** | `any` | optional | | +| **traceId** | `string` | optional | | + --- @@ -54,6 +90,42 @@ Options for DataEngine.aggregate operations | **groupBy** | `string[]` | optional | | | **aggregations** | `{ field: string; method: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>; alias?: string }[]` | optional | | +### Nested Shape: `DataEngineAggregateOptions.context` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **userId** | `string` | optional | | +| **actor** | `string` | optional | | +| **attributedUserId** | `string` | optional | | +| **email** | `string` | optional | | +| **tenantId** | `string` | optional | | +| **timezone** | `string` | optional | | +| **locale** | `string` | optional | | +| **currency** | `string` | optional | | +| **positions** | `string[]` | optional (default: `[]`) | | +| **principalKind** | `Enum<'human' \| 'agent' \| 'service' \| 'guest' \| 'system'>` | optional | | +| **audience** | `Enum<'internal' \| 'external'>` | optional | | +| **posture** | `Enum<'PLATFORM_ADMIN' \| 'TENANT_ADMIN' \| 'MEMBER' \| 'EXTERNAL'>` | optional | ADR-0095 D2 posture rung — PLATFORM_ADMIN crosses the tenant wall where object posture permits; TENANT_ADMIN sees all rows in the org; MEMBER gets business RLS; EXTERNAL sees only explicitly shared rows. | +| **authGate** | `{ code: string; message: string }` | optional | ADR-0069 authentication-policy gate: present only while the principal is blocked from protected resources until they remediate (expired password, enforced MFA), absent for every healthy session. `code` is the stable machine code the client branches on (PASSWORD_EXPIRED / MFA_REQUIRED) and `message` is what the blocked user reads; both are required because the transport seam renders them as the 403 body. AUTHENTICATION, not authorization — it suspends access entirely rather than narrowing it, and nothing in the permission/RLS path reads it, while the allow-listed remediation endpoints stay reachable. Server-constructed only, never client-supplied; a guest/anonymous principal never carries one. | +| **onBehalfOf** | `{ userId: string; principalKind?: Enum<'human' \| 'agent' \| 'service' \| 'guest' \| 'system'> }` | optional | | +| **permissions** | `string[]` | optional (default: `[]`) | | +| **systemPermissions** | `string[]` | optional | | +| **tabPermissions** | `Record>` | optional | | +| **org_user_ids** | `string[]` | optional | | +| **accessible_org_ids** | `string[]` | optional | | +| **rlsMembership** | `Record` | optional | | +| **isSystem** | `boolean` | optional (default: `false`) | | +| **flowRunId** | `string` | optional | | +| **skipTriggers** | `boolean` | optional | | +| **skipAutomations** | `boolean` | optional | | +| **seedReplay** | `boolean` | optional | | +| **skipStateMachine** | `boolean` | optional | | +| **preserveAudit** | `boolean` | optional | Historical import: preserve the ORIGINAL audit timeline for this write instead of stamping it "now" (#3493). Opt-in and server-constructed only, never client-supplied. On the UPDATE path it admits a whitelist — the audit/timestamp family (created_at / created_by / updated_at / updated_by) plus author-declared business `readonly` fields — while platform-managed `system` columns (tenancy, generated) stay stripped. On INSERT the exemption does NOT apply (#6640): a create is stripped earlier, at the DataProtocol ingress, whose only exemption is `context.isSystem`, so a non-system create carrying `preserveAudit` still has those fields stripped and is warned (WARN) that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. Permissions / RLS / field-level security are unaffected. | +| **oauthScopes** | `string[]` | optional | | +| **accessToken** | `string` | optional | | +| **transaction** | `any` | optional | | +| **traceId** | `string` | optional | | + --- @@ -67,6 +139,18 @@ Options for DataEngine.aggregate operations | **object** | `string` | ✅ | | | **query** | `{ context?: object; where?: Record \| any; groupBy?: (string \| object)[]; aggregations?: object[]; … }` | ✅ | | +### Nested Shape: `DataEngineAggregateRequest.query` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **context** | `{ userId?: string; actor?: string; attributedUserId?: string; email?: string; … }` | optional | | +| **where** | `Record \| any` | optional | | +| **groupBy** | `(string \| { field: string; dateGranularity?: Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>; alias?: string })[]` | optional | GROUP BY targets (strings or `{field, dateGranularity?}` objects for date bucketing) | +| **aggregations** | `{ function: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>; field?: string; alias: string; filter?: any }[]` | optional | | +| **having** | `any` | optional | HAVING — filter over the aggregated rows (aggregation aliases + groupBy projections); applied engine-side after aggregation | +| **timezone** | `string` | optional | | +| **filter** | `Record \| any` | optional | Data Engine query filter conditions | + --- @@ -81,6 +165,42 @@ Options for DataEngine.count operations | **context** | `{ userId?: string; actor?: string; attributedUserId?: string; email?: string; … }` | optional | | | **filter** | `Record \| any` | optional | Data Engine query filter conditions | +### Nested Shape: `DataEngineCountOptions.context` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **userId** | `string` | optional | | +| **actor** | `string` | optional | | +| **attributedUserId** | `string` | optional | | +| **email** | `string` | optional | | +| **tenantId** | `string` | optional | | +| **timezone** | `string` | optional | | +| **locale** | `string` | optional | | +| **currency** | `string` | optional | | +| **positions** | `string[]` | optional (default: `[]`) | | +| **principalKind** | `Enum<'human' \| 'agent' \| 'service' \| 'guest' \| 'system'>` | optional | | +| **audience** | `Enum<'internal' \| 'external'>` | optional | | +| **posture** | `Enum<'PLATFORM_ADMIN' \| 'TENANT_ADMIN' \| 'MEMBER' \| 'EXTERNAL'>` | optional | ADR-0095 D2 posture rung — PLATFORM_ADMIN crosses the tenant wall where object posture permits; TENANT_ADMIN sees all rows in the org; MEMBER gets business RLS; EXTERNAL sees only explicitly shared rows. | +| **authGate** | `{ code: string; message: string }` | optional | ADR-0069 authentication-policy gate: present only while the principal is blocked from protected resources until they remediate (expired password, enforced MFA), absent for every healthy session. `code` is the stable machine code the client branches on (PASSWORD_EXPIRED / MFA_REQUIRED) and `message` is what the blocked user reads; both are required because the transport seam renders them as the 403 body. AUTHENTICATION, not authorization — it suspends access entirely rather than narrowing it, and nothing in the permission/RLS path reads it, while the allow-listed remediation endpoints stay reachable. Server-constructed only, never client-supplied; a guest/anonymous principal never carries one. | +| **onBehalfOf** | `{ userId: string; principalKind?: Enum<'human' \| 'agent' \| 'service' \| 'guest' \| 'system'> }` | optional | | +| **permissions** | `string[]` | optional (default: `[]`) | | +| **systemPermissions** | `string[]` | optional | | +| **tabPermissions** | `Record>` | optional | | +| **org_user_ids** | `string[]` | optional | | +| **accessible_org_ids** | `string[]` | optional | | +| **rlsMembership** | `Record` | optional | | +| **isSystem** | `boolean` | optional (default: `false`) | | +| **flowRunId** | `string` | optional | | +| **skipTriggers** | `boolean` | optional | | +| **skipAutomations** | `boolean` | optional | | +| **seedReplay** | `boolean` | optional | | +| **skipStateMachine** | `boolean` | optional | | +| **preserveAudit** | `boolean` | optional | Historical import: preserve the ORIGINAL audit timeline for this write instead of stamping it "now" (#3493). Opt-in and server-constructed only, never client-supplied. On the UPDATE path it admits a whitelist — the audit/timestamp family (created_at / created_by / updated_at / updated_by) plus author-declared business `readonly` fields — while platform-managed `system` columns (tenancy, generated) stay stripped. On INSERT the exemption does NOT apply (#6640): a create is stripped earlier, at the DataProtocol ingress, whose only exemption is `context.isSystem`, so a non-system create carrying `preserveAudit` still has those fields stripped and is warned (WARN) that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. Permissions / RLS / field-level security are unaffected. | +| **oauthScopes** | `string[]` | optional | | +| **accessToken** | `string` | optional | | +| **transaction** | `any` | optional | | +| **traceId** | `string` | optional | | + --- @@ -94,6 +214,14 @@ Options for DataEngine.count operations | **object** | `string` | ✅ | | | **query** | `{ context?: object; where?: Record \| any; filter?: Record \| any }` | optional | | +### Nested Shape: `DataEngineCountRequest.query` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **context** | `{ userId?: string; actor?: string; attributedUserId?: string; email?: string; … }` | optional | | +| **where** | `Record \| any` | optional | | +| **filter** | `Record \| any` | optional | Data Engine query filter conditions | + --- @@ -109,6 +237,42 @@ Options for DataEngine.delete operations | **filter** | `Record \| any` | optional | Data Engine query filter conditions | | **multi** | `boolean` | optional (default: `false`) | | +### Nested Shape: `DataEngineDeleteOptions.context` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **userId** | `string` | optional | | +| **actor** | `string` | optional | | +| **attributedUserId** | `string` | optional | | +| **email** | `string` | optional | | +| **tenantId** | `string` | optional | | +| **timezone** | `string` | optional | | +| **locale** | `string` | optional | | +| **currency** | `string` | optional | | +| **positions** | `string[]` | optional (default: `[]`) | | +| **principalKind** | `Enum<'human' \| 'agent' \| 'service' \| 'guest' \| 'system'>` | optional | | +| **audience** | `Enum<'internal' \| 'external'>` | optional | | +| **posture** | `Enum<'PLATFORM_ADMIN' \| 'TENANT_ADMIN' \| 'MEMBER' \| 'EXTERNAL'>` | optional | ADR-0095 D2 posture rung — PLATFORM_ADMIN crosses the tenant wall where object posture permits; TENANT_ADMIN sees all rows in the org; MEMBER gets business RLS; EXTERNAL sees only explicitly shared rows. | +| **authGate** | `{ code: string; message: string }` | optional | ADR-0069 authentication-policy gate: present only while the principal is blocked from protected resources until they remediate (expired password, enforced MFA), absent for every healthy session. `code` is the stable machine code the client branches on (PASSWORD_EXPIRED / MFA_REQUIRED) and `message` is what the blocked user reads; both are required because the transport seam renders them as the 403 body. AUTHENTICATION, not authorization — it suspends access entirely rather than narrowing it, and nothing in the permission/RLS path reads it, while the allow-listed remediation endpoints stay reachable. Server-constructed only, never client-supplied; a guest/anonymous principal never carries one. | +| **onBehalfOf** | `{ userId: string; principalKind?: Enum<'human' \| 'agent' \| 'service' \| 'guest' \| 'system'> }` | optional | | +| **permissions** | `string[]` | optional (default: `[]`) | | +| **systemPermissions** | `string[]` | optional | | +| **tabPermissions** | `Record>` | optional | | +| **org_user_ids** | `string[]` | optional | | +| **accessible_org_ids** | `string[]` | optional | | +| **rlsMembership** | `Record` | optional | | +| **isSystem** | `boolean` | optional (default: `false`) | | +| **flowRunId** | `string` | optional | | +| **skipTriggers** | `boolean` | optional | | +| **skipAutomations** | `boolean` | optional | | +| **seedReplay** | `boolean` | optional | | +| **skipStateMachine** | `boolean` | optional | | +| **preserveAudit** | `boolean` | optional | Historical import: preserve the ORIGINAL audit timeline for this write instead of stamping it "now" (#3493). Opt-in and server-constructed only, never client-supplied. On the UPDATE path it admits a whitelist — the audit/timestamp family (created_at / created_by / updated_at / updated_by) plus author-declared business `readonly` fields — while platform-managed `system` columns (tenancy, generated) stay stripped. On INSERT the exemption does NOT apply (#6640): a create is stripped earlier, at the DataProtocol ingress, whose only exemption is `context.isSystem`, so a non-system create carrying `preserveAudit` still has those fields stripped and is warned (WARN) that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. Permissions / RLS / field-level security are unaffected. | +| **oauthScopes** | `string[]` | optional | | +| **accessToken** | `string` | optional | | +| **transaction** | `any` | optional | | +| **traceId** | `string` | optional | | + --- @@ -123,6 +287,15 @@ Options for DataEngine.delete operations | **id** | `string \| number` | optional | ID for single delete, or use where in options | | **options** | `{ context?: object; where?: Record \| any; multi?: boolean; filter?: Record \| any }` | optional | | +### Nested Shape: `DataEngineDeleteRequest.options` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **context** | `{ userId?: string; actor?: string; attributedUserId?: string; email?: string; … }` | optional | | +| **where** | `Record \| any` | optional | | +| **multi** | `boolean` | optional (default: `false`) | | +| **filter** | `Record \| any` | optional | Data Engine query filter conditions | + --- @@ -172,6 +345,28 @@ Reference: any | **object** | `string` | ✅ | | | **query** | `{ context?: object; where?: Record \| any; fields?: string[]; orderBy?: object[]; … }` | optional | | +### Nested Shape: `DataEngineFindOneRequest.query` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **context** | `{ userId?: string; actor?: string; attributedUserId?: string; email?: string; … }` | optional | | +| **where** | `Record \| any` | optional | | +| **fields** | `string[]` | optional | | +| **orderBy** | `{ field: string; order?: Enum<'asc' \| 'desc'> }[]` | optional | | +| **limit** | `number` | optional | | +| **offset** | `number` | optional | | +| **top** | `number` | optional | | +| **cursor** | `never` | optional | [REMOVED] `query.cursor` was removed in @objectstack/spec 17 (#4286, ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; `QueryBuilder.cursor()` was removed with it. Express the keyset as an ordinary `where` predicate on your sort key — `where: { created_at: { $gt: last.created_at } }` with the matching `orderBy` — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. | +| **search** | `string \| { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' \| 'or'>; … }` | optional | | +| **searchFields** | `string[]` | optional | | +| **expand** | `Record` | optional | | +| **distinct** | `never` | optional | [REMOVED] `query.distinct` was removed in @objectstack/spec 17 (#4286, ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded `total`/`hasMore` to a page-local estimate while still returning duplicate rows. Delete the key; `QueryBuilder.distinct()` was removed with it, and the count suppression is gone (`total` is truthful again). For unique values of one column use the SQL/memory drivers' `distinct(object, field)` door; for unique combinations, `groupBy`; for a deduplicated count, the `count_distinct` aggregation. | +| **filter** | `Record \| any` | optional | Data Engine query filter conditions | +| **select** | `string[]` | optional | | +| **sort** | `Record> \| Record \| { field: string; order?: Enum<'asc' \| 'desc'> }[]` | optional | Sort order definition | +| **skip** | `integer` | optional | | +| **populate** | `string[]` | optional | | + --- @@ -185,6 +380,28 @@ Reference: any | **object** | `string` | ✅ | | | **query** | `{ context?: object; where?: Record \| any; fields?: string[]; orderBy?: object[]; … }` | optional | | +### Nested Shape: `DataEngineFindRequest.query` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **context** | `{ userId?: string; actor?: string; attributedUserId?: string; email?: string; … }` | optional | | +| **where** | `Record \| any` | optional | | +| **fields** | `string[]` | optional | | +| **orderBy** | `{ field: string; order?: Enum<'asc' \| 'desc'> }[]` | optional | | +| **limit** | `number` | optional | | +| **offset** | `number` | optional | | +| **top** | `number` | optional | | +| **cursor** | `never` | optional | [REMOVED] `query.cursor` was removed in @objectstack/spec 17 (#4286, ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; `QueryBuilder.cursor()` was removed with it. Express the keyset as an ordinary `where` predicate on your sort key — `where: { created_at: { $gt: last.created_at } }` with the matching `orderBy` — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. | +| **search** | `string \| { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' \| 'or'>; … }` | optional | | +| **searchFields** | `string[]` | optional | | +| **expand** | `Record` | optional | | +| **distinct** | `never` | optional | [REMOVED] `query.distinct` was removed in @objectstack/spec 17 (#4286, ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded `total`/`hasMore` to a page-local estimate while still returning duplicate rows. Delete the key; `QueryBuilder.distinct()` was removed with it, and the count suppression is gone (`total` is truthful again). For unique values of one column use the SQL/memory drivers' `distinct(object, field)` door; for unique combinations, `groupBy`; for a deduplicated count, the `count_distinct` aggregation. | +| **filter** | `Record \| any` | optional | Data Engine query filter conditions | +| **select** | `string[]` | optional | | +| **sort** | `Record> \| Record \| { field: string; order?: Enum<'asc' \| 'desc'> }[]` | optional | Sort order definition | +| **skip** | `integer` | optional | | +| **populate** | `string[]` | optional | | + --- @@ -199,6 +416,42 @@ Options for DataEngine.insert operations | **context** | `{ userId?: string; actor?: string; attributedUserId?: string; email?: string; … }` | optional | | | **returning** | `boolean` | optional (default: `true`) | | +### Nested Shape: `DataEngineInsertOptions.context` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **userId** | `string` | optional | | +| **actor** | `string` | optional | | +| **attributedUserId** | `string` | optional | | +| **email** | `string` | optional | | +| **tenantId** | `string` | optional | | +| **timezone** | `string` | optional | | +| **locale** | `string` | optional | | +| **currency** | `string` | optional | | +| **positions** | `string[]` | optional (default: `[]`) | | +| **principalKind** | `Enum<'human' \| 'agent' \| 'service' \| 'guest' \| 'system'>` | optional | | +| **audience** | `Enum<'internal' \| 'external'>` | optional | | +| **posture** | `Enum<'PLATFORM_ADMIN' \| 'TENANT_ADMIN' \| 'MEMBER' \| 'EXTERNAL'>` | optional | ADR-0095 D2 posture rung — PLATFORM_ADMIN crosses the tenant wall where object posture permits; TENANT_ADMIN sees all rows in the org; MEMBER gets business RLS; EXTERNAL sees only explicitly shared rows. | +| **authGate** | `{ code: string; message: string }` | optional | ADR-0069 authentication-policy gate: present only while the principal is blocked from protected resources until they remediate (expired password, enforced MFA), absent for every healthy session. `code` is the stable machine code the client branches on (PASSWORD_EXPIRED / MFA_REQUIRED) and `message` is what the blocked user reads; both are required because the transport seam renders them as the 403 body. AUTHENTICATION, not authorization — it suspends access entirely rather than narrowing it, and nothing in the permission/RLS path reads it, while the allow-listed remediation endpoints stay reachable. Server-constructed only, never client-supplied; a guest/anonymous principal never carries one. | +| **onBehalfOf** | `{ userId: string; principalKind?: Enum<'human' \| 'agent' \| 'service' \| 'guest' \| 'system'> }` | optional | | +| **permissions** | `string[]` | optional (default: `[]`) | | +| **systemPermissions** | `string[]` | optional | | +| **tabPermissions** | `Record>` | optional | | +| **org_user_ids** | `string[]` | optional | | +| **accessible_org_ids** | `string[]` | optional | | +| **rlsMembership** | `Record` | optional | | +| **isSystem** | `boolean` | optional (default: `false`) | | +| **flowRunId** | `string` | optional | | +| **skipTriggers** | `boolean` | optional | | +| **skipAutomations** | `boolean` | optional | | +| **seedReplay** | `boolean` | optional | | +| **skipStateMachine** | `boolean` | optional | | +| **preserveAudit** | `boolean` | optional | Historical import: preserve the ORIGINAL audit timeline for this write instead of stamping it "now" (#3493). Opt-in and server-constructed only, never client-supplied. On the UPDATE path it admits a whitelist — the audit/timestamp family (created_at / created_by / updated_at / updated_by) plus author-declared business `readonly` fields — while platform-managed `system` columns (tenancy, generated) stay stripped. On INSERT the exemption does NOT apply (#6640): a create is stripped earlier, at the DataProtocol ingress, whose only exemption is `context.isSystem`, so a non-system create carrying `preserveAudit` still has those fields stripped and is warned (WARN) that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. Permissions / RLS / field-level security are unaffected. | +| **oauthScopes** | `string[]` | optional | | +| **accessToken** | `string` | optional | | +| **transaction** | `any` | optional | | +| **traceId** | `string` | optional | | + --- @@ -233,6 +486,42 @@ Query options for IDataEngine.find() operations | **top** | `integer` | optional | | | **populate** | `string[]` | optional | | +### Nested Shape: `DataEngineQueryOptions.context` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **userId** | `string` | optional | | +| **actor** | `string` | optional | | +| **attributedUserId** | `string` | optional | | +| **email** | `string` | optional | | +| **tenantId** | `string` | optional | | +| **timezone** | `string` | optional | | +| **locale** | `string` | optional | | +| **currency** | `string` | optional | | +| **positions** | `string[]` | optional (default: `[]`) | | +| **principalKind** | `Enum<'human' \| 'agent' \| 'service' \| 'guest' \| 'system'>` | optional | | +| **audience** | `Enum<'internal' \| 'external'>` | optional | | +| **posture** | `Enum<'PLATFORM_ADMIN' \| 'TENANT_ADMIN' \| 'MEMBER' \| 'EXTERNAL'>` | optional | ADR-0095 D2 posture rung — PLATFORM_ADMIN crosses the tenant wall where object posture permits; TENANT_ADMIN sees all rows in the org; MEMBER gets business RLS; EXTERNAL sees only explicitly shared rows. | +| **authGate** | `{ code: string; message: string }` | optional | ADR-0069 authentication-policy gate: present only while the principal is blocked from protected resources until they remediate (expired password, enforced MFA), absent for every healthy session. `code` is the stable machine code the client branches on (PASSWORD_EXPIRED / MFA_REQUIRED) and `message` is what the blocked user reads; both are required because the transport seam renders them as the 403 body. AUTHENTICATION, not authorization — it suspends access entirely rather than narrowing it, and nothing in the permission/RLS path reads it, while the allow-listed remediation endpoints stay reachable. Server-constructed only, never client-supplied; a guest/anonymous principal never carries one. | +| **onBehalfOf** | `{ userId: string; principalKind?: Enum<'human' \| 'agent' \| 'service' \| 'guest' \| 'system'> }` | optional | | +| **permissions** | `string[]` | optional (default: `[]`) | | +| **systemPermissions** | `string[]` | optional | | +| **tabPermissions** | `Record>` | optional | | +| **org_user_ids** | `string[]` | optional | | +| **accessible_org_ids** | `string[]` | optional | | +| **rlsMembership** | `Record` | optional | | +| **isSystem** | `boolean` | optional (default: `false`) | | +| **flowRunId** | `string` | optional | | +| **skipTriggers** | `boolean` | optional | | +| **skipAutomations** | `boolean` | optional | | +| **seedReplay** | `boolean` | optional | | +| **skipStateMachine** | `boolean` | optional | | +| **preserveAudit** | `boolean` | optional | Historical import: preserve the ORIGINAL audit timeline for this write instead of stamping it "now" (#3493). Opt-in and server-constructed only, never client-supplied. On the UPDATE path it admits a whitelist — the audit/timestamp family (created_at / created_by / updated_at / updated_by) plus author-declared business `readonly` fields — while platform-managed `system` columns (tenancy, generated) stay stripped. On INSERT the exemption does NOT apply (#6640): a create is stripped earlier, at the DataProtocol ingress, whose only exemption is `context.isSystem`, so a non-system create carrying `preserveAudit` still has those fields stripped and is warned (WARN) that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. Permissions / RLS / field-level security are unaffected. | +| **oauthScopes** | `string[]` | optional | | +| **accessToken** | `string` | optional | | +| **transaction** | `any` | optional | | +| **traceId** | `string` | optional | | + --- @@ -254,6 +543,28 @@ This schema accepts one of the following structures: | **object** | `string` | ✅ | | | **query** | `{ context?: object; where?: Record \| any; fields?: string[]; orderBy?: object[]; … }` | optional | | +### Nested Shape: `DataEngineRequest.query` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **context** | `{ userId?: string; actor?: string; attributedUserId?: string; email?: string; … }` | optional | | +| **where** | `Record \| any` | optional | | +| **fields** | `string[]` | optional | | +| **orderBy** | `{ field: string; order?: Enum<'asc' \| 'desc'> }[]` | optional | | +| **limit** | `number` | optional | | +| **offset** | `number` | optional | | +| **top** | `number` | optional | | +| **cursor** | `never` | optional | [REMOVED] `query.cursor` was removed in @objectstack/spec 17 (#4286, ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; `QueryBuilder.cursor()` was removed with it. Express the keyset as an ordinary `where` predicate on your sort key — `where: { created_at: { $gt: last.created_at } }` with the matching `orderBy` — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. | +| **search** | `string \| { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' \| 'or'>; … }` | optional | | +| **searchFields** | `string[]` | optional | | +| **expand** | `Record` | optional | | +| **distinct** | `never` | optional | [REMOVED] `query.distinct` was removed in @objectstack/spec 17 (#4286, ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded `total`/`hasMore` to a page-local estimate while still returning duplicate rows. Delete the key; `QueryBuilder.distinct()` was removed with it, and the count suppression is gone (`total` is truthful again). For unique values of one column use the SQL/memory drivers' `distinct(object, field)` door; for unique combinations, `groupBy`; for a deduplicated count, the `count_distinct` aggregation. | +| **filter** | `Record \| any` | optional | Data Engine query filter conditions | +| **select** | `string[]` | optional | | +| **sort** | `Record> \| Record \| { field: string; order?: Enum<'asc' \| 'desc'> }[]` | optional | Sort order definition | +| **skip** | `integer` | optional | | +| **populate** | `string[]` | optional | | + --- #### Option 2 @@ -266,6 +577,28 @@ This schema accepts one of the following structures: | **object** | `string` | ✅ | | | **query** | `{ context?: object; where?: Record \| any; fields?: string[]; orderBy?: object[]; … }` | optional | | +### Nested Shape: `DataEngineRequest.query` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **context** | `{ userId?: string; actor?: string; attributedUserId?: string; email?: string; … }` | optional | | +| **where** | `Record \| any` | optional | | +| **fields** | `string[]` | optional | | +| **orderBy** | `{ field: string; order?: Enum<'asc' \| 'desc'> }[]` | optional | | +| **limit** | `number` | optional | | +| **offset** | `number` | optional | | +| **top** | `number` | optional | | +| **cursor** | `never` | optional | [REMOVED] `query.cursor` was removed in @objectstack/spec 17 (#4286, ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; `QueryBuilder.cursor()` was removed with it. Express the keyset as an ordinary `where` predicate on your sort key — `where: { created_at: { $gt: last.created_at } }` with the matching `orderBy` — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. | +| **search** | `string \| { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' \| 'or'>; … }` | optional | | +| **searchFields** | `string[]` | optional | | +| **expand** | `Record` | optional | | +| **distinct** | `never` | optional | [REMOVED] `query.distinct` was removed in @objectstack/spec 17 (#4286, ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded `total`/`hasMore` to a page-local estimate while still returning duplicate rows. Delete the key; `QueryBuilder.distinct()` was removed with it, and the count suppression is gone (`total` is truthful again). For unique values of one column use the SQL/memory drivers' `distinct(object, field)` door; for unique combinations, `groupBy`; for a deduplicated count, the `count_distinct` aggregation. | +| **filter** | `Record \| any` | optional | Data Engine query filter conditions | +| **select** | `string[]` | optional | | +| **sort** | `Record> \| Record \| { field: string; order?: Enum<'asc' \| 'desc'> }[]` | optional | Sort order definition | +| **skip** | `integer` | optional | | +| **populate** | `string[]` | optional | | + --- #### Option 3 @@ -293,6 +626,17 @@ This schema accepts one of the following structures: | **id** | `string \| number` | optional | ID for single update, or use where in options | | **options** | `{ context?: object; where?: Record \| any; multi?: boolean; returning?: boolean; … }` | optional | | +### Nested Shape: `DataEngineRequest.options` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **context** | `{ userId?: string; actor?: string; attributedUserId?: string; email?: string; … }` | optional | | +| **where** | `Record \| any` | optional | | +| **upsert** | `never` | optional | [REMOVED] `update.options.upsert` was removed in @objectstack/spec 17 (#8057, ADR-0049) — it was declared and allowlisted but never implemented: no engine or driver path ever read it, so `{ upsert: true }` was accepted and silently dropped and the update stayed a plain update. Delete the key. Express create-if-absent explicitly: a by-id update whose id names no row throws RECORD_NOT_FOUND (#7867's not-found gate) rather than inserting, so read the row first (`findOne`) and call `insert` or `update` on what you find. A first-class upsert, if ever built, must reconcile with that gate by design rather than through this silent flag. | +| **multi** | `boolean` | optional (default: `false`) | | +| **returning** | `boolean` | optional (default: `false`) | | +| **filter** | `Record \| any` | optional | Data Engine query filter conditions | + --- #### Option 5 @@ -306,6 +650,15 @@ This schema accepts one of the following structures: | **id** | `string \| number` | optional | ID for single delete, or use where in options | | **options** | `{ context?: object; where?: Record \| any; multi?: boolean; filter?: Record \| any }` | optional | | +### Nested Shape: `DataEngineRequest.options` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **context** | `{ userId?: string; actor?: string; attributedUserId?: string; email?: string; … }` | optional | | +| **where** | `Record \| any` | optional | | +| **multi** | `boolean` | optional (default: `false`) | | +| **filter** | `Record \| any` | optional | Data Engine query filter conditions | + --- #### Option 6 @@ -318,6 +671,14 @@ This schema accepts one of the following structures: | **object** | `string` | ✅ | | | **query** | `{ context?: object; where?: Record \| any; filter?: Record \| any }` | optional | | +### Nested Shape: `DataEngineRequest.query` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **context** | `{ userId?: string; actor?: string; attributedUserId?: string; email?: string; … }` | optional | | +| **where** | `Record \| any` | optional | | +| **filter** | `Record \| any` | optional | Data Engine query filter conditions | + --- #### Option 7 @@ -330,6 +691,18 @@ This schema accepts one of the following structures: | **object** | `string` | ✅ | | | **query** | `{ context?: object; where?: Record \| any; groupBy?: (string \| object)[]; aggregations?: object[]; … }` | ✅ | | +### Nested Shape: `DataEngineRequest.query` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **context** | `{ userId?: string; actor?: string; attributedUserId?: string; email?: string; … }` | optional | | +| **where** | `Record \| any` | optional | | +| **groupBy** | `(string \| { field: string; dateGranularity?: Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>; alias?: string })[]` | optional | GROUP BY targets (strings or `{field, dateGranularity?}` objects for date bucketing) | +| **aggregations** | `{ function: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>; field?: string; alias: string; filter?: any }[]` | optional | | +| **having** | `any` | optional | HAVING — filter over the aggregated rows (aggregation aliases + groupBy projections); applied engine-side after aggregation | +| **timezone** | `string` | optional | | +| **filter** | `Record \| any` | optional | Data Engine query filter conditions | + --- #### Option 8 @@ -406,6 +779,42 @@ Options for DataEngine.update operations | **multi** | `boolean` | optional (default: `false`) | | | **returning** | `boolean` | optional (default: `false`) | | +### Nested Shape: `DataEngineUpdateOptions.context` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **userId** | `string` | optional | | +| **actor** | `string` | optional | | +| **attributedUserId** | `string` | optional | | +| **email** | `string` | optional | | +| **tenantId** | `string` | optional | | +| **timezone** | `string` | optional | | +| **locale** | `string` | optional | | +| **currency** | `string` | optional | | +| **positions** | `string[]` | optional (default: `[]`) | | +| **principalKind** | `Enum<'human' \| 'agent' \| 'service' \| 'guest' \| 'system'>` | optional | | +| **audience** | `Enum<'internal' \| 'external'>` | optional | | +| **posture** | `Enum<'PLATFORM_ADMIN' \| 'TENANT_ADMIN' \| 'MEMBER' \| 'EXTERNAL'>` | optional | ADR-0095 D2 posture rung — PLATFORM_ADMIN crosses the tenant wall where object posture permits; TENANT_ADMIN sees all rows in the org; MEMBER gets business RLS; EXTERNAL sees only explicitly shared rows. | +| **authGate** | `{ code: string; message: string }` | optional | ADR-0069 authentication-policy gate: present only while the principal is blocked from protected resources until they remediate (expired password, enforced MFA), absent for every healthy session. `code` is the stable machine code the client branches on (PASSWORD_EXPIRED / MFA_REQUIRED) and `message` is what the blocked user reads; both are required because the transport seam renders them as the 403 body. AUTHENTICATION, not authorization — it suspends access entirely rather than narrowing it, and nothing in the permission/RLS path reads it, while the allow-listed remediation endpoints stay reachable. Server-constructed only, never client-supplied; a guest/anonymous principal never carries one. | +| **onBehalfOf** | `{ userId: string; principalKind?: Enum<'human' \| 'agent' \| 'service' \| 'guest' \| 'system'> }` | optional | | +| **permissions** | `string[]` | optional (default: `[]`) | | +| **systemPermissions** | `string[]` | optional | | +| **tabPermissions** | `Record>` | optional | | +| **org_user_ids** | `string[]` | optional | | +| **accessible_org_ids** | `string[]` | optional | | +| **rlsMembership** | `Record` | optional | | +| **isSystem** | `boolean` | optional (default: `false`) | | +| **flowRunId** | `string` | optional | | +| **skipTriggers** | `boolean` | optional | | +| **skipAutomations** | `boolean` | optional | | +| **seedReplay** | `boolean` | optional | | +| **skipStateMachine** | `boolean` | optional | | +| **preserveAudit** | `boolean` | optional | Historical import: preserve the ORIGINAL audit timeline for this write instead of stamping it "now" (#3493). Opt-in and server-constructed only, never client-supplied. On the UPDATE path it admits a whitelist — the audit/timestamp family (created_at / created_by / updated_at / updated_by) plus author-declared business `readonly` fields — while platform-managed `system` columns (tenancy, generated) stay stripped. On INSERT the exemption does NOT apply (#6640): a create is stripped earlier, at the DataProtocol ingress, whose only exemption is `context.isSystem`, so a non-system create carrying `preserveAudit` still has those fields stripped and is warned (WARN) that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. Permissions / RLS / field-level security are unaffected. | +| **oauthScopes** | `string[]` | optional | | +| **accessToken** | `string` | optional | | +| **transaction** | `any` | optional | | +| **traceId** | `string` | optional | | + --- @@ -421,6 +830,17 @@ Options for DataEngine.update operations | **id** | `string \| number` | optional | ID for single update, or use where in options | | **options** | `{ context?: object; where?: Record \| any; multi?: boolean; returning?: boolean; … }` | optional | | +### Nested Shape: `DataEngineUpdateRequest.options` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **context** | `{ userId?: string; actor?: string; attributedUserId?: string; email?: string; … }` | optional | | +| **where** | `Record \| any` | optional | | +| **upsert** | `never` | optional | [REMOVED] `update.options.upsert` was removed in @objectstack/spec 17 (#8057, ADR-0049) — it was declared and allowlisted but never implemented: no engine or driver path ever read it, so `{ upsert: true }` was accepted and silently dropped and the update stayed a plain update. Delete the key. Express create-if-absent explicitly: a by-id update whose id names no row throws RECORD_NOT_FOUND (#7867's not-found gate) rather than inserting, so read the row first (`findOne`) and call `insert` or `update` on what you find. A first-class upsert, if ever built, must reconcile with that gate by design rather than through this silent flag. | +| **multi** | `boolean` | optional (default: `false`) | | +| **returning** | `boolean` | optional (default: `false`) | | +| **filter** | `Record \| any` | optional | Data Engine query filter conditions | + --- @@ -471,6 +891,60 @@ QueryAST-aligned options for DataEngine.aggregate operations | **having** | `any` | optional | HAVING — filter over the aggregated rows (aggregation aliases + groupBy projections); applied engine-side after aggregation | | **timezone** | `string` | optional | | +### Nested Shape: `EngineAggregateOptions.context` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **userId** | `string` | optional | | +| **actor** | `string` | optional | | +| **attributedUserId** | `string` | optional | | +| **email** | `string` | optional | | +| **tenantId** | `string` | optional | | +| **timezone** | `string` | optional | | +| **locale** | `string` | optional | | +| **currency** | `string` | optional | | +| **positions** | `string[]` | optional (default: `[]`) | | +| **principalKind** | `Enum<'human' \| 'agent' \| 'service' \| 'guest' \| 'system'>` | optional | | +| **audience** | `Enum<'internal' \| 'external'>` | optional | | +| **posture** | `Enum<'PLATFORM_ADMIN' \| 'TENANT_ADMIN' \| 'MEMBER' \| 'EXTERNAL'>` | optional | ADR-0095 D2 posture rung — PLATFORM_ADMIN crosses the tenant wall where object posture permits; TENANT_ADMIN sees all rows in the org; MEMBER gets business RLS; EXTERNAL sees only explicitly shared rows. | +| **authGate** | `{ code: string; message: string }` | optional | ADR-0069 authentication-policy gate: present only while the principal is blocked from protected resources until they remediate (expired password, enforced MFA), absent for every healthy session. `code` is the stable machine code the client branches on (PASSWORD_EXPIRED / MFA_REQUIRED) and `message` is what the blocked user reads; both are required because the transport seam renders them as the 403 body. AUTHENTICATION, not authorization — it suspends access entirely rather than narrowing it, and nothing in the permission/RLS path reads it, while the allow-listed remediation endpoints stay reachable. Server-constructed only, never client-supplied; a guest/anonymous principal never carries one. | +| **onBehalfOf** | `{ userId: string; principalKind?: Enum<'human' \| 'agent' \| 'service' \| 'guest' \| 'system'> }` | optional | | +| **permissions** | `string[]` | optional (default: `[]`) | | +| **systemPermissions** | `string[]` | optional | | +| **tabPermissions** | `Record>` | optional | | +| **org_user_ids** | `string[]` | optional | | +| **accessible_org_ids** | `string[]` | optional | | +| **rlsMembership** | `Record` | optional | | +| **isSystem** | `boolean` | optional (default: `false`) | | +| **flowRunId** | `string` | optional | | +| **skipTriggers** | `boolean` | optional | | +| **skipAutomations** | `boolean` | optional | | +| **seedReplay** | `boolean` | optional | | +| **skipStateMachine** | `boolean` | optional | | +| **preserveAudit** | `boolean` | optional | Historical import: preserve the ORIGINAL audit timeline for this write instead of stamping it "now" (#3493). Opt-in and server-constructed only, never client-supplied. On the UPDATE path it admits a whitelist — the audit/timestamp family (created_at / created_by / updated_at / updated_by) plus author-declared business `readonly` fields — while platform-managed `system` columns (tenancy, generated) stay stripped. On INSERT the exemption does NOT apply (#6640): a create is stripped earlier, at the DataProtocol ingress, whose only exemption is `context.isSystem`, so a non-system create carrying `preserveAudit` still has those fields stripped and is warned (WARN) that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. Permissions / RLS / field-level security are unaffected. | +| **oauthScopes** | `string[]` | optional | | +| **accessToken** | `string` | optional | | +| **transaction** | `any` | optional | | +| **traceId** | `string` | optional | | + +### Nested Shape: `EngineAggregateOptions.groupBy[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field to group by | +| **dateGranularity** | `Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>` | optional | Bucket date values into uniform periods (day/week/month/quarter/year) | +| **alias** | `string` | optional | Alias for the projected group value | + +### Nested Shape: `EngineAggregateOptions.aggregations[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **function** | `Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>` | ✅ | Aggregation function | +| **field** | `string` | optional | Field to aggregate (optional for COUNT(*)) | +| **alias** | `string` | ✅ | Result column alias | +| **distinct** | `never` | optional | [REMOVED] `query.aggregations[].distinct` was removed in @objectstack/spec 17 (#6815, ADR-0049) — exactly ONE of the six faces that read an aggregation honoured it. The objectql in-memory fallback deduplicated the values before applying the function, while `driver-sql`, `driver-turso`, `driver-mongodb`, `driver-memory` and the service-analytics SQL builder all ignored it — so `{ function: 'sum', field: 'amount', distinct: true }` answered a DEDUPLICATED sum when the engine fell back in memory and an ordinary sum on every SQL datasource: one query, two numbers, chosen by which backend happened to serve it. Both answers are plausible, so nothing surfaced the divergence. Delete the key. For a deduplicated COUNT the live spelling is the `count_distinct` aggregation function, which every SQL face compiles to `COUNT(DISTINCT field)` (#6409) and the in-memory fallback computes identically. `SUM(DISTINCT …)` / `AVG(DISTINCT …)` get no replacement: no backend ever computed them here, and a per-row measure that needs deduplicating is a modelling problem to fix in the data, not a flag on the read. | +| **filter** | `any` | optional | Per-aggregation filter (SQL FILTER (WHERE …) semantics): narrows the source rows THIS aggregation reads, leaving sibling aggregations unfiltered. Enforced by engine.aggregate (#10576): lowered in memory for drivers without native conditional aggregation; a driver reached directly refuses rather than silently dropping it. | + --- @@ -485,6 +959,42 @@ QueryAST-aligned options for DataEngine.count operations | **context** | `{ userId?: string; actor?: string; attributedUserId?: string; email?: string; … }` | optional | | | **where** | `Record \| any` | optional | | +### Nested Shape: `EngineCountOptions.context` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **userId** | `string` | optional | | +| **actor** | `string` | optional | | +| **attributedUserId** | `string` | optional | | +| **email** | `string` | optional | | +| **tenantId** | `string` | optional | | +| **timezone** | `string` | optional | | +| **locale** | `string` | optional | | +| **currency** | `string` | optional | | +| **positions** | `string[]` | optional (default: `[]`) | | +| **principalKind** | `Enum<'human' \| 'agent' \| 'service' \| 'guest' \| 'system'>` | optional | | +| **audience** | `Enum<'internal' \| 'external'>` | optional | | +| **posture** | `Enum<'PLATFORM_ADMIN' \| 'TENANT_ADMIN' \| 'MEMBER' \| 'EXTERNAL'>` | optional | ADR-0095 D2 posture rung — PLATFORM_ADMIN crosses the tenant wall where object posture permits; TENANT_ADMIN sees all rows in the org; MEMBER gets business RLS; EXTERNAL sees only explicitly shared rows. | +| **authGate** | `{ code: string; message: string }` | optional | ADR-0069 authentication-policy gate: present only while the principal is blocked from protected resources until they remediate (expired password, enforced MFA), absent for every healthy session. `code` is the stable machine code the client branches on (PASSWORD_EXPIRED / MFA_REQUIRED) and `message` is what the blocked user reads; both are required because the transport seam renders them as the 403 body. AUTHENTICATION, not authorization — it suspends access entirely rather than narrowing it, and nothing in the permission/RLS path reads it, while the allow-listed remediation endpoints stay reachable. Server-constructed only, never client-supplied; a guest/anonymous principal never carries one. | +| **onBehalfOf** | `{ userId: string; principalKind?: Enum<'human' \| 'agent' \| 'service' \| 'guest' \| 'system'> }` | optional | | +| **permissions** | `string[]` | optional (default: `[]`) | | +| **systemPermissions** | `string[]` | optional | | +| **tabPermissions** | `Record>` | optional | | +| **org_user_ids** | `string[]` | optional | | +| **accessible_org_ids** | `string[]` | optional | | +| **rlsMembership** | `Record` | optional | | +| **isSystem** | `boolean` | optional (default: `false`) | | +| **flowRunId** | `string` | optional | | +| **skipTriggers** | `boolean` | optional | | +| **skipAutomations** | `boolean` | optional | | +| **seedReplay** | `boolean` | optional | | +| **skipStateMachine** | `boolean` | optional | | +| **preserveAudit** | `boolean` | optional | Historical import: preserve the ORIGINAL audit timeline for this write instead of stamping it "now" (#3493). Opt-in and server-constructed only, never client-supplied. On the UPDATE path it admits a whitelist — the audit/timestamp family (created_at / created_by / updated_at / updated_by) plus author-declared business `readonly` fields — while platform-managed `system` columns (tenancy, generated) stay stripped. On INSERT the exemption does NOT apply (#6640): a create is stripped earlier, at the DataProtocol ingress, whose only exemption is `context.isSystem`, so a non-system create carrying `preserveAudit` still has those fields stripped and is warned (WARN) that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. Permissions / RLS / field-level security are unaffected. | +| **oauthScopes** | `string[]` | optional | | +| **accessToken** | `string` | optional | | +| **transaction** | `any` | optional | | +| **traceId** | `string` | optional | | + --- @@ -500,6 +1010,42 @@ QueryAST-aligned options for DataEngine.delete operations | **where** | `Record \| any` | optional | | | **multi** | `boolean` | optional (default: `false`) | | +### Nested Shape: `EngineDeleteOptions.context` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **userId** | `string` | optional | | +| **actor** | `string` | optional | | +| **attributedUserId** | `string` | optional | | +| **email** | `string` | optional | | +| **tenantId** | `string` | optional | | +| **timezone** | `string` | optional | | +| **locale** | `string` | optional | | +| **currency** | `string` | optional | | +| **positions** | `string[]` | optional (default: `[]`) | | +| **principalKind** | `Enum<'human' \| 'agent' \| 'service' \| 'guest' \| 'system'>` | optional | | +| **audience** | `Enum<'internal' \| 'external'>` | optional | | +| **posture** | `Enum<'PLATFORM_ADMIN' \| 'TENANT_ADMIN' \| 'MEMBER' \| 'EXTERNAL'>` | optional | ADR-0095 D2 posture rung — PLATFORM_ADMIN crosses the tenant wall where object posture permits; TENANT_ADMIN sees all rows in the org; MEMBER gets business RLS; EXTERNAL sees only explicitly shared rows. | +| **authGate** | `{ code: string; message: string }` | optional | ADR-0069 authentication-policy gate: present only while the principal is blocked from protected resources until they remediate (expired password, enforced MFA), absent for every healthy session. `code` is the stable machine code the client branches on (PASSWORD_EXPIRED / MFA_REQUIRED) and `message` is what the blocked user reads; both are required because the transport seam renders them as the 403 body. AUTHENTICATION, not authorization — it suspends access entirely rather than narrowing it, and nothing in the permission/RLS path reads it, while the allow-listed remediation endpoints stay reachable. Server-constructed only, never client-supplied; a guest/anonymous principal never carries one. | +| **onBehalfOf** | `{ userId: string; principalKind?: Enum<'human' \| 'agent' \| 'service' \| 'guest' \| 'system'> }` | optional | | +| **permissions** | `string[]` | optional (default: `[]`) | | +| **systemPermissions** | `string[]` | optional | | +| **tabPermissions** | `Record>` | optional | | +| **org_user_ids** | `string[]` | optional | | +| **accessible_org_ids** | `string[]` | optional | | +| **rlsMembership** | `Record` | optional | | +| **isSystem** | `boolean` | optional (default: `false`) | | +| **flowRunId** | `string` | optional | | +| **skipTriggers** | `boolean` | optional | | +| **skipAutomations** | `boolean` | optional | | +| **seedReplay** | `boolean` | optional | | +| **skipStateMachine** | `boolean` | optional | | +| **preserveAudit** | `boolean` | optional | Historical import: preserve the ORIGINAL audit timeline for this write instead of stamping it "now" (#3493). Opt-in and server-constructed only, never client-supplied. On the UPDATE path it admits a whitelist — the audit/timestamp family (created_at / created_by / updated_at / updated_by) plus author-declared business `readonly` fields — while platform-managed `system` columns (tenancy, generated) stay stripped. On INSERT the exemption does NOT apply (#6640): a create is stripped earlier, at the DataProtocol ingress, whose only exemption is `context.isSystem`, so a non-system create carrying `preserveAudit` still has those fields stripped and is warned (WARN) that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. Permissions / RLS / field-level security are unaffected. | +| **oauthScopes** | `string[]` | optional | | +| **accessToken** | `string` | optional | | +| **transaction** | `any` | optional | | +| **traceId** | `string` | optional | | + --- @@ -524,6 +1070,77 @@ QueryAST-aligned query options for IDataEngine.find() operations | **expand** | `Record` | optional | | | **distinct** | `never` | optional | [REMOVED] `query.distinct` was removed in @objectstack/spec 17 (#4286, ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded `total`/`hasMore` to a page-local estimate while still returning duplicate rows. Delete the key; `QueryBuilder.distinct()` was removed with it, and the count suppression is gone (`total` is truthful again). For unique values of one column use the SQL/memory drivers' `distinct(object, field)` door; for unique combinations, `groupBy`; for a deduplicated count, the `count_distinct` aggregation. | +### Nested Shape: `EngineQueryOptions.context` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **userId** | `string` | optional | | +| **actor** | `string` | optional | | +| **attributedUserId** | `string` | optional | | +| **email** | `string` | optional | | +| **tenantId** | `string` | optional | | +| **timezone** | `string` | optional | | +| **locale** | `string` | optional | | +| **currency** | `string` | optional | | +| **positions** | `string[]` | optional (default: `[]`) | | +| **principalKind** | `Enum<'human' \| 'agent' \| 'service' \| 'guest' \| 'system'>` | optional | | +| **audience** | `Enum<'internal' \| 'external'>` | optional | | +| **posture** | `Enum<'PLATFORM_ADMIN' \| 'TENANT_ADMIN' \| 'MEMBER' \| 'EXTERNAL'>` | optional | ADR-0095 D2 posture rung — PLATFORM_ADMIN crosses the tenant wall where object posture permits; TENANT_ADMIN sees all rows in the org; MEMBER gets business RLS; EXTERNAL sees only explicitly shared rows. | +| **authGate** | `{ code: string; message: string }` | optional | ADR-0069 authentication-policy gate: present only while the principal is blocked from protected resources until they remediate (expired password, enforced MFA), absent for every healthy session. `code` is the stable machine code the client branches on (PASSWORD_EXPIRED / MFA_REQUIRED) and `message` is what the blocked user reads; both are required because the transport seam renders them as the 403 body. AUTHENTICATION, not authorization — it suspends access entirely rather than narrowing it, and nothing in the permission/RLS path reads it, while the allow-listed remediation endpoints stay reachable. Server-constructed only, never client-supplied; a guest/anonymous principal never carries one. | +| **onBehalfOf** | `{ userId: string; principalKind?: Enum<'human' \| 'agent' \| 'service' \| 'guest' \| 'system'> }` | optional | | +| **permissions** | `string[]` | optional (default: `[]`) | | +| **systemPermissions** | `string[]` | optional | | +| **tabPermissions** | `Record>` | optional | | +| **org_user_ids** | `string[]` | optional | | +| **accessible_org_ids** | `string[]` | optional | | +| **rlsMembership** | `Record` | optional | | +| **isSystem** | `boolean` | optional (default: `false`) | | +| **flowRunId** | `string` | optional | | +| **skipTriggers** | `boolean` | optional | | +| **skipAutomations** | `boolean` | optional | | +| **seedReplay** | `boolean` | optional | | +| **skipStateMachine** | `boolean` | optional | | +| **preserveAudit** | `boolean` | optional | Historical import: preserve the ORIGINAL audit timeline for this write instead of stamping it "now" (#3493). Opt-in and server-constructed only, never client-supplied. On the UPDATE path it admits a whitelist — the audit/timestamp family (created_at / created_by / updated_at / updated_by) plus author-declared business `readonly` fields — while platform-managed `system` columns (tenancy, generated) stay stripped. On INSERT the exemption does NOT apply (#6640): a create is stripped earlier, at the DataProtocol ingress, whose only exemption is `context.isSystem`, so a non-system create carrying `preserveAudit` still has those fields stripped and is warned (WARN) that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. Permissions / RLS / field-level security are unaffected. | +| **oauthScopes** | `string[]` | optional | | +| **accessToken** | `string` | optional | | +| **transaction** | `any` | optional | | +| **traceId** | `string` | optional | | + +### Nested Shape: `EngineQueryOptions.search` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **query** | `string` | ✅ | Search query text | +| **fields** | `string[]` | optional | Fields to search in (if not specified, searches all text fields) | +| **fuzzy** | `boolean` | optional (default: `false`) | [EXPERIMENTAL — not enforced] Fuzzy matching (tolerate typos). The ADR-0061 expansion reads only `query` + `fields`; no executor receives this flag (#4286). | +| **operator** | `Enum<'and' \| 'or'>` | optional (default: `"or"`) | [EXPERIMENTAL — not enforced] Logical operator between terms. The ADR-0061 expansion applies its own term semantics; no executor receives this flag (#4286). | +| **boost** | `Record` | optional | [EXPERIMENTAL — not enforced] Field-specific relevance boosting (field name -> boost factor). No executor scores results (#4286). | +| **minScore** | `number` | optional | [EXPERIMENTAL — not enforced] Minimum relevance score threshold. No executor scores results (#4286). | +| **language** | `string` | optional | [EXPERIMENTAL — not enforced] Language for text analysis (e.g., "en", "zh", "es"). No executor selects an analyzer (#4286). | +| **highlight** | `boolean` | optional (default: `false`) | [EXPERIMENTAL — not enforced] Search result highlighting. No executor emits highlights (#4286). | + +### Nested Shape: `EngineQueryOptions.expand[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **object** | `string` | ✅ | Object name (e.g. account) | +| **fields** | `string[]` | optional | Fields to retrieve — names of the queried object's OWN columns. A dotted path (`owner.name`) is not a projection: no driver resolves one, and the ingress refuses it with `400 INVALID_FIELD` (#7532). Related data is read with `expand`, whose nested QueryAST both filters (`where`) and selects (`fields`) the related record's columns. The projection must RETAIN the foreign-key column: `fields: ['title']` with `expand: 'project_id'` resolves nothing, because the relation is carried by that key — add `'project_id'` and it works. Where the value is wanted on the queried object itself, denormalise it onto that object (a stored field, written when the source changes), the same remedy the sort axis prescribes (#6924). | +| **where** | `any` | optional | Filtering criteria (WHERE) | +| **search** | `string \| { query: string; fields?: string[]; fuzzy: boolean; operator: Enum<'and' \| 'or'>; … }` | optional | Full-text search — the query text (canonical, ADR-0061 D1), or a structured FullTextSearch configuration | +| **searchFields** | `string[]` | optional | Narrow the search to these fields (server-intersected with the allowed searchable set — can only narrow, never widen; ADR-0061 D1) | +| **orderBy** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Sorting instructions (ORDER BY) | +| **limit** | `number` | optional | Max records to return (LIMIT) | +| **offset** | `number` | optional | Records to skip (OFFSET) | +| **top** | `number` | optional | Alias for limit (OData compatibility) | +| **cursor** | `never` | optional | [REMOVED] `query.cursor` was removed in @objectstack/spec 17 (#4286, ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; `QueryBuilder.cursor()` was removed with it. Express the keyset as an ordinary `where` predicate on your sort key — `where: { created_at: { $gt: last.created_at } }` with the matching `orderBy` — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. | +| **joins** | `never` | optional | [REMOVED] `query.joins` was removed in @objectstack/spec 17 (#4286, ADR-0049) — no engine or driver ever read it: a query carrying `joins` behaved exactly as if the key were absent, while its name squatted on the reserved REST parameter set. Delete the key. Related records are read through `expand` — `expand: { owner_id: { object: 'user', fields: ['name'] } }` — which the engine resolves via batch $in queries, and whose nested query selects the related record's own columns. Keep the foreign key in your own projection (`fields: ['title', 'owner_id']`): the relation is carried by that column, so projecting it away leaves expansion nothing to resolve. A dotted `fields` path is NOT a replacement — no driver ever resolved one and the ingress refuses it (`400 INVALID_FIELD`, #7532). | +| **aggregations** | `{ function: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>; field?: string; alias: string; filter?: any }[]` | optional | Aggregation functions | +| **groupBy** | `(string \| { field: string; dateGranularity?: Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>; alias?: string })[]` | optional | GROUP BY targets (strings or `{field, dateGranularity?}` objects for date bucketing) | +| **having** | `any` | optional | HAVING — filter over the AGGREGATED rows (aggregation aliases + groupBy projections); applied engine-side after aggregation | +| **windowFunctions** | `never` | optional | [REMOVED] `query.windowFunctions` was removed in @objectstack/spec 17 (#4286, ADR-0049) — `find()` never applied it: no engine or driver read the key on the query path, so every OVER clause it declared was silently dropped. Delete the key. Window functions are a SQL-driver capability behind `SqlDriver.findWithWindowFunctions(object, query)` (embedder-level; not on the `IDataDriver` contract or the REST surface); request-level analytics are `aggregations` + `groupBy`. | +| **distinct** | `never` | optional | [REMOVED] `query.distinct` was removed in @objectstack/spec 17 (#4286, ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded `total`/`hasMore` to a page-local estimate while still returning duplicate rows. Delete the key; `QueryBuilder.distinct()` was removed with it, and the count suppression is gone (`total` is truthful again). For unique values of one column use the SQL/memory drivers' `distinct(object, field)` door; for unique combinations, `groupBy`; for a deduplicated count, the `count_distinct` aggregation. | +| **expand** | `Record` | optional | Recursive relation loading map. Keys are lookup/master_detail field names; values are nested QueryAST objects that control select (`fields`) and filter (`where`, AND-merged with the batch $in), plus further expansion on the related object. The engine resolves expand via batch $in queries (driver-agnostic) with a default max depth of 3; per-parent `limit`/`offset`/`orderBy` are NOT applied on this path. | + --- @@ -541,6 +1158,42 @@ QueryAST-aligned options for DataEngine.update operations | **multi** | `boolean` | optional (default: `false`) | | | **returning** | `boolean` | optional (default: `false`) | | +### Nested Shape: `EngineUpdateOptions.context` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **userId** | `string` | optional | | +| **actor** | `string` | optional | | +| **attributedUserId** | `string` | optional | | +| **email** | `string` | optional | | +| **tenantId** | `string` | optional | | +| **timezone** | `string` | optional | | +| **locale** | `string` | optional | | +| **currency** | `string` | optional | | +| **positions** | `string[]` | optional (default: `[]`) | | +| **principalKind** | `Enum<'human' \| 'agent' \| 'service' \| 'guest' \| 'system'>` | optional | | +| **audience** | `Enum<'internal' \| 'external'>` | optional | | +| **posture** | `Enum<'PLATFORM_ADMIN' \| 'TENANT_ADMIN' \| 'MEMBER' \| 'EXTERNAL'>` | optional | ADR-0095 D2 posture rung — PLATFORM_ADMIN crosses the tenant wall where object posture permits; TENANT_ADMIN sees all rows in the org; MEMBER gets business RLS; EXTERNAL sees only explicitly shared rows. | +| **authGate** | `{ code: string; message: string }` | optional | ADR-0069 authentication-policy gate: present only while the principal is blocked from protected resources until they remediate (expired password, enforced MFA), absent for every healthy session. `code` is the stable machine code the client branches on (PASSWORD_EXPIRED / MFA_REQUIRED) and `message` is what the blocked user reads; both are required because the transport seam renders them as the 403 body. AUTHENTICATION, not authorization — it suspends access entirely rather than narrowing it, and nothing in the permission/RLS path reads it, while the allow-listed remediation endpoints stay reachable. Server-constructed only, never client-supplied; a guest/anonymous principal never carries one. | +| **onBehalfOf** | `{ userId: string; principalKind?: Enum<'human' \| 'agent' \| 'service' \| 'guest' \| 'system'> }` | optional | | +| **permissions** | `string[]` | optional (default: `[]`) | | +| **systemPermissions** | `string[]` | optional | | +| **tabPermissions** | `Record>` | optional | | +| **org_user_ids** | `string[]` | optional | | +| **accessible_org_ids** | `string[]` | optional | | +| **rlsMembership** | `Record` | optional | | +| **isSystem** | `boolean` | optional (default: `false`) | | +| **flowRunId** | `string` | optional | | +| **skipTriggers** | `boolean` | optional | | +| **skipAutomations** | `boolean` | optional | | +| **seedReplay** | `boolean` | optional | | +| **skipStateMachine** | `boolean` | optional | | +| **preserveAudit** | `boolean` | optional | Historical import: preserve the ORIGINAL audit timeline for this write instead of stamping it "now" (#3493). Opt-in and server-constructed only, never client-supplied. On the UPDATE path it admits a whitelist — the audit/timestamp family (created_at / created_by / updated_at / updated_by) plus author-declared business `readonly` fields — while platform-managed `system` columns (tenancy, generated) stay stripped. On INSERT the exemption does NOT apply (#6640): a create is stripped earlier, at the DataProtocol ingress, whose only exemption is `context.isSystem`, so a non-system create carrying `preserveAudit` still has those fields stripped and is warned (WARN) that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. Permissions / RLS / field-level security are unaffected. | +| **oauthScopes** | `string[]` | optional | | +| **accessToken** | `string` | optional | | +| **transaction** | `any` | optional | | +| **traceId** | `string` | optional | | + --- diff --git a/content/docs/references/data/datasource.mdx b/content/docs/references/data/datasource.mdx index 405b9f6864..fdced8d694 100644 --- a/content/docs/references/data/datasource.mdx +++ b/content/docs/references/data/datasource.mdx @@ -50,6 +50,35 @@ const result = DatasourceSchema.parse(data); | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `Datasource.pool` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **min** | `number` | optional (default: `0`) | Minimum connections | +| **max** | `number` | optional (default: `10`) | Maximum connections | +| **idleTimeoutMillis** | `number` | optional (default: `30000`) | Idle timeout | +| **connectionTimeoutMillis** | `number` | optional (default: `3000`) | Connection establishment timeout | + +### Nested Shape: `Datasource.ssl` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `false`) | Enable SSL/TLS for database connection | +| **rejectUnauthorized** | `boolean` | optional (default: `true`) | Reject connections with invalid/self-signed certificates | +| **ca** | `string` | optional | CA certificate (PEM format or path to file) | +| **cert** | `string` | optional | Client certificate (PEM format or path to file) | +| **key** | `string` | optional | Client private key (PEM format or path to file) | + +### Nested Shape: `Datasource.external` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **allowedSchemas** | `string[]` | optional | Whitelist of remote schemas/databases that may be exposed. | +| **allowWrites** | `boolean` | optional (default: `false`) | Global write gate. Individual objects must also opt in via object.external.writable. | +| **validation** | `{ onMismatch: Enum<'fail' \| 'warn' \| 'ignore'>; checkOnBoot: boolean; checkIntervalMs?: number }` | optional (default: `{"onMismatch":"fail","checkOnBoot":true}`) | Boot/drift validation policy | +| **credentialsRef** | `string` | optional | Reference into the secrets store; never inline credentials. Valid in every schemaMode — the one `external` key a managed datasource may carry (#8153). | +| **queryTimeoutMs** | `number` | optional (default: `30000`) | Hard cap on per-query execution time. | + --- @@ -91,6 +120,14 @@ External datasource settings: federation policy (schemaMode != "managed") plus t | **credentialsRef** | `string` | optional | Reference into the secrets store; never inline credentials. Valid in every schemaMode — the one `external` key a managed datasource may carry (#8153). | | **queryTimeoutMs** | `number` | optional (default: `30000`) | Hard cap on per-query execution time. | +### Nested Shape: `ExternalDatasourceSettings.validation` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **onMismatch** | `Enum<'fail' \| 'warn' \| 'ignore'>` | optional (default: `"fail"`) | What to do when a federated object diverges from the remote table. | +| **checkOnBoot** | `boolean` | optional (default: `true`) | Validate federated objects against the remote schema at boot. | +| **checkIntervalMs** | `number` | optional | Optional background drift-check interval in milliseconds. | + --- diff --git a/content/docs/references/data/document.mdx b/content/docs/references/data/document.mdx index e69d1ebcd1..d3404cb7d1 100644 --- a/content/docs/references/data/document.mdx +++ b/content/docs/references/data/document.mdx @@ -58,6 +58,44 @@ const result = DocumentSchema.parse(data); | **access** | `{ isPublic: boolean; sharedWith?: string[]; expiresAt?: number }` | optional | Access control | | **metadata** | `Record` | optional | Custom metadata | +### Nested Shape: `Document.versioning` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | ✅ | Versioning enabled | +| **versions** | `{ versionNumber: number; createdAt: number; createdBy: string; size: number; … }[]` | ✅ | Version history | +| **majorVersion** | `number` | ✅ | Major version | +| **minorVersion** | `number` | ✅ | Minor version | + +### Nested Shape: `Document.template` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Template ID | +| **name** | `string` | ✅ | Template name | +| **description** | `string` | optional | Template description | +| **fileUrl** | `string` | ✅ | Template file URL | +| **fileType** | `string` | ✅ | File MIME type | +| **placeholders** | `{ key: string; label: string; type: Enum<'text' \| 'number' \| 'date' \| 'image'>; required: boolean }[]` | ✅ | Template placeholders | + +### Nested Shape: `Document.eSignature` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **provider** | `Enum<'docusign' \| 'adobe-sign' \| 'hellosign' \| 'custom'>` | ✅ | E-signature provider | +| **enabled** | `boolean` | optional (default: `false`) | E-signature enabled | +| **signers** | `{ email: string; name: string; role: string; order: number }[]` | ✅ | Document signers | +| **expirationDays** | `number` | optional (default: `30`) | Expiration days | +| **reminderDays** | `number` | optional (default: `7`) | Reminder interval days | + +### Nested Shape: `Document.access` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **isPublic** | `boolean` | optional (default: `false`) | Public access | +| **sharedWith** | `string[]` | optional | Shared with | +| **expiresAt** | `number` | optional | Access expiration | + --- @@ -74,6 +112,15 @@ const result = DocumentSchema.parse(data); | **fileType** | `string` | ✅ | File MIME type | | **placeholders** | `{ key: string; label: string; type: Enum<'text' \| 'number' \| 'date' \| 'image'>; required: boolean }[]` | ✅ | Template placeholders | +### Nested Shape: `DocumentTemplate.placeholders[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **key** | `string` | ✅ | Placeholder key | +| **label** | `string` | ✅ | Placeholder label | +| **type** | `Enum<'text' \| 'number' \| 'date' \| 'image'>` | ✅ | Placeholder type | +| **required** | `boolean` | optional (default: `false`) | Is required | + --- @@ -106,6 +153,15 @@ const result = DocumentSchema.parse(data); | **expirationDays** | `number` | optional (default: `30`) | Expiration days | | **reminderDays** | `number` | optional (default: `7`) | Reminder interval days | +### Nested Shape: `ESignatureConfig.signers[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **email** | `string` | ✅ | Signer email | +| **name** | `string` | ✅ | Signer name | +| **role** | `string` | ✅ | Signer role | +| **order** | `number` | ✅ | Signing order | + --- diff --git a/content/docs/references/data/driver-nosql.mdx b/content/docs/references/data/driver-nosql.mdx index 4d4fc3c5c0..af34504454 100644 --- a/content/docs/references/data/driver-nosql.mdx +++ b/content/docs/references/data/driver-nosql.mdx @@ -34,6 +34,26 @@ const result = AggregationPipelineSchema.parse(data); | **stages** | `{ operator: string; options: Record }[]` | ✅ | Aggregation pipeline stages | | **options** | `{ consistency?: Enum<'all' \| 'quorum' \| 'one' \| 'local_quorum' \| 'each_quorum' \| 'eventual'>; readFromSecondary?: boolean; projection?: Record; timeout?: integer; … }` | optional | Query options | +### Nested Shape: `AggregationPipeline.stages[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **operator** | `string` | ✅ | Aggregation operator (e.g., $match, $group, $sort) | +| **options** | `Record` | ✅ | Stage-specific options | + +### Nested Shape: `AggregationPipeline.options` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **consistency** | `Enum<'all' \| 'quorum' \| 'one' \| 'local_quorum' \| 'each_quorum' \| 'eventual'>` | optional | Consistency level override | +| **readFromSecondary** | `boolean` | optional | Allow reading from secondary replicas | +| **projection** | `Record` | optional | Field projection | +| **timeout** | `integer` | optional | Query timeout (ms) | +| **useCursor** | `boolean` | optional | Use cursor instead of loading all results | +| **batchSize** | `integer` | optional | Cursor batch size | +| **profile** | `boolean` | optional | Enable query profiling | +| **hint** | `string` | optional | Index hint for query optimization | + --- @@ -138,6 +158,98 @@ const result = AggregationPipelineSchema.parse(data); | **maxDocumentSize** | `integer` | optional | Maximum document size in bytes | | **collectionPrefix** | `string` | optional | Prefix for collection/table names | +### Nested Shape: `NoSQLDriverConfig.capabilities` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **queryDateGranularity** | `Record` | optional | Per-granularity native date bucketing (day/week/month/quarter/year). Missing keys fall back to in-memory bucketing. | +| **autonumber** | `boolean` | optional | Driver natively generates persistent autonumber/sequence values | +| **batchSchemaSync** | `boolean` | optional | Supports batched schema sync to reduce schema DDL round-trips (absence = false) | +| **create** | `never` | optional | [REMOVED] `DriverCapabilities.create` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. CRUD is not optional for a driver: `create`/`find`/`findOne`/`update`/`delete` are REQUIRED `IDataDriver` methods and the engine calls them unconditionally. Delete the key. | +| **read** | `never` | optional | [REMOVED] `DriverCapabilities.read` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. CRUD is not optional for a driver: reads go through the REQUIRED `find`/`findOne`/`count` methods, called unconditionally. Delete the key. | +| **update** | `never` | optional | [REMOVED] `DriverCapabilities.update` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. CRUD is not optional for a driver: `update`/`upsert` are REQUIRED `IDataDriver` methods, called unconditionally. Delete the key. | +| **delete** | `never` | optional | [REMOVED] `DriverCapabilities.delete` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. CRUD is not optional for a driver: `delete` is a REQUIRED `IDataDriver` method, called unconditionally. Delete the key. | +| **bulkCreate** | `never` | optional | [REMOVED] `DriverCapabilities.bulkCreate` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. The bulk methods (`bulkCreate`/`bulkUpdate`/`bulkDelete`) are REQUIRED `IDataDriver` methods and the engine calls them directly; wire-level batch capability is advertised by REST discovery from the live composition (#3298), never from this record. Delete the key. | +| **bulkUpdate** | `never` | optional | [REMOVED] `DriverCapabilities.bulkUpdate` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. The bulk methods are REQUIRED `IDataDriver` methods and the engine calls them directly; wire-level batch capability is advertised by REST discovery from the live composition (#3298), never from this record. Delete the key. | +| **bulkDelete** | `never` | optional | [REMOVED] `DriverCapabilities.bulkDelete` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. The bulk methods are REQUIRED `IDataDriver` methods and the engine calls them directly; wire-level batch capability is advertised by REST discovery from the live composition (#3298), never from this record. Delete the key. | +| **transactions** | `never` | optional | [REMOVED] `DriverCapabilities.transactions` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Transaction use is gated on METHOD PRESENCE — `driver.beginTransaction` (`engine.transaction()`, ADR-0034 ambient transactions): a driver without the method gets the non-transactional fallback, whatever this bit claimed. Discovery's `transactionalBatch` capability is likewise derived from `engine.transaction` plus the mounted batch route, never from this bit. Delete the key. | +| **savepoints** | `never` | optional | [REMOVED] `DriverCapabilities.savepoints` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No savepoint code path exists in the engine — a capability bit for a feature the platform does not call is a false affordance, not documentation. Delete the key. | +| **isolationLevels** | `never` | optional | [REMOVED] `DriverCapabilities.isolationLevels` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Isolation is requested per transaction via `beginTransaction({ isolationLevel })`; no planner ever consulted this list to decide anything. Delete the key. | +| **queryFilters** | `never` | optional | [REMOVED] `DriverCapabilities.queryFilters` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `find()` receives the full QueryAST (`where`/`orderBy`/`limit`/`offset`) and MUST execute all of it — the "ObjectQL will filter in memory" fallback this bit's description promised was never built. Delete the key. | +| **querySorting** | `never` | optional | [REMOVED] `DriverCapabilities.querySorting` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `find()` receives the full QueryAST and MUST execute all of it — the "ObjectQL will sort in memory" fallback this bit's description promised was never built. Delete the key. | +| **queryPagination** | `never` | optional | [REMOVED] `DriverCapabilities.queryPagination` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `find()` receives the full QueryAST and MUST execute all of it — the "ObjectQL will paginate in memory" fallback this bit's description promised was never built. Delete the key. | +| **queryAggregations** | `never` | optional | [REMOVED] `DriverCapabilities.queryAggregations` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Aggregate pushdown is decided by `typeof driver.aggregate === 'function'` plus `queryDateGranularity` (engine aggregate dispatch) — never by this bit. Delete the key. | +| **queryWindowFunctions** | `never` | optional | [REMOVED] `DriverCapabilities.queryWindowFunctions` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans window functions through a driver, so there was nothing for the bit to switch on. Delete the key. | +| **querySubqueries** | `never` | optional | [REMOVED] `DriverCapabilities.querySubqueries` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans subqueries through a driver, so there was nothing for the bit to switch on. Delete the key. | +| **queryCTE** | `never` | optional | [REMOVED] `DriverCapabilities.queryCTE` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans Common Table Expressions through a driver, so there was nothing for the bit to switch on. Delete the key. | +| **joins** | `never` | optional | [REMOVED] `DriverCapabilities.joins` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Related data is resolved by the engine (lookup expansion over `find()`), not by driver-side JOIN planning — no code consulted the bit. Delete the key. | +| **fullTextSearch** | `never` | optional | [REMOVED] `DriverCapabilities.fullTextSearch` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `$search` is compiled by the engine into an `$or` of `$contains` predicates over the searchable fields (ADR-0061) and removed from the AST before the driver sees it — no driver-side full-text path exists. Delete the key. | +| **jsonQuery** | `never` | optional | [REMOVED] `DriverCapabilities.jsonQuery` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No engine path ever branched on driver-side JSON querying. Delete the key. | +| **geospatialQuery** | `never` | optional | [REMOVED] `DriverCapabilities.geospatialQuery` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No geospatial query path exists in the platform — declaring the bit advertised a capability nothing delivers. Delete the key. | +| **streaming** | `never` | optional | [REMOVED] `DriverCapabilities.streaming` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, and `findStream`, the only read this bit could describe, was itself removed in 17.0.0 (#4484): nothing ever called it, and two of its three implementations materialised the entire result set before yielding. The bit carried the same defect one level up (`SqlDriver` implemented `findStream` yet declared `streaming: false`; `InMemoryDriver` declared `true` over a full-table read) — which is what zero readers makes inevitable. Page large reads through `find()` with `limit`/`offset`. Delete the key. | +| **jsonFields** | `never` | optional | [REMOVED] `DriverCapabilities.jsonFields` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Field-type handling is negotiated per object at `syncSchema` time by the driver itself (e.g. `SqlDriver`'s per-object JSON/date column tracking); no engine path consulted the bit. Delete the key. | +| **arrayFields** | `never` | optional | [REMOVED] `DriverCapabilities.arrayFields` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Field-type handling is negotiated per object at `syncSchema` time by the driver itself; no engine path consulted the bit. Delete the key. | +| **vectorSearch** | `never` | optional | [REMOVED] `DriverCapabilities.vectorSearch` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No vector read path routes through `IDataDriver`. When one exists it should arrive WITH its caller and its capability bit together (the honest order under enforce-or-remove), not as a dangling boolean. Delete the key. | +| **schemaSync** | `never` | optional | [REMOVED] `DriverCapabilities.schemaSync` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Schema sync is gated on METHOD PRESENCE — `typeof driver.syncSchema === 'function'` (engine and ObjectQL plugin init). Delete the key. | +| **migrations** | `never` | optional | [REMOVED] `DriverCapabilities.migrations` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No migration engine ever consulted it. Delete the key. | +| **indexes** | `never` | optional | [REMOVED] `DriverCapabilities.indexes` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Declared indexes are materialised by the driver itself during schema sync (`SqlDriver.syncDeclaredIndexes`); no engine path consulted the bit. Delete the key. | +| **connectionPooling** | `never` | optional | [REMOVED] `DriverCapabilities.connectionPooling` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Pooling is configured via `poolConfig` and owned by the driver; `getPoolStats` is duck-typed where monitoring wants it. Nothing consulted the bit. Delete the key. | +| **preparedStatements** | `never` | optional | [REMOVED] `DriverCapabilities.preparedStatements` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Parameterised execution is an implementation detail of the driver (`execute(command, parameters)`); nothing consulted the bit. Delete the key. | +| **queryCache** | `never` | optional | [REMOVED] `DriverCapabilities.queryCache` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No query-cache layer keyed off it exists; `DriverOptions.skipCache` is a per-call hint to the driver, not a switch on this bit. Delete the key. | + +### Nested Shape: `NoSQLDriverConfig.poolConfig` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **min** | `number` | optional (default: `2`) | Minimum number of connections in pool | +| **max** | `number` | optional (default: `10`) | Maximum number of connections in pool | +| **idleTimeoutMillis** | `number` | optional (default: `30000`) | Time in ms before idle connection is closed | +| **connectionTimeoutMillis** | `number` | optional (default: `5000`) | Time in ms to wait for available connection | + +### Nested Shape: `NoSQLDriverConfig.dataTypeMapping` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **text** | `string` | ✅ | NoSQL type for text fields | +| **number** | `string` | ✅ | NoSQL type for number fields | +| **boolean** | `string` | ✅ | NoSQL type for boolean fields | +| **date** | `string` | ✅ | NoSQL type for date fields | +| **datetime** | `string` | ✅ | NoSQL type for datetime fields | +| **json** | `string` | optional | NoSQL type for JSON/object fields | +| **uuid** | `string` | optional | NoSQL type for UUID fields | +| **binary** | `string` | optional | NoSQL type for binary fields | +| **array** | `string` | optional | NoSQL type for array fields | +| **objectId** | `string` | optional | NoSQL type for ObjectID fields (MongoDB) | +| **geopoint** | `string` | optional | NoSQL type for geospatial point fields | + +### Nested Shape: `NoSQLDriverConfig.replication` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `false`) | Enable replication | +| **replicaSetName** | `string` | optional | Replica set name | +| **replicas** | `integer` | optional | Number of replicas | +| **readPreference** | `Enum<'primary' \| 'primaryPreferred' \| 'secondary' \| 'secondaryPreferred' \| 'nearest'>` | optional | Read preference for replica set | +| **writeConcern** | `Enum<'majority' \| 'acknowledged' \| 'unacknowledged'>` | optional | Write concern level | + +### Nested Shape: `NoSQLDriverConfig.sharding` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `false`) | Enable sharding | +| **shardKey** | `string` | optional | Field to use as shard key | +| **shardingStrategy** | `Enum<'hash' \| 'range' \| 'zone'>` | optional | Sharding strategy | +| **numShards** | `integer` | optional | Number of shards | + +### Nested Shape: `NoSQLDriverConfig.schemaValidation` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `false`) | Enable schema validation | +| **validationLevel** | `Enum<'strict' \| 'moderate' \| 'off'>` | optional | Validation strictness | +| **validationAction** | `Enum<'error' \| 'warn'>` | optional | Action on validation failure | +| **jsonSchema** | `Record` | optional | JSON Schema for validation | + --- @@ -156,6 +268,13 @@ const result = AggregationPipelineSchema.parse(data); | **partialFilterExpression** | `Record` | optional | Partial index filter | | **background** | `boolean` | optional (default: `false`) | Create index in background | +### Nested Shape: `NoSQLIndex.fields[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name | +| **order** | `Enum<'asc' \| 'desc' \| 'text' \| '2dsphere'>` | optional | Index order or type | + --- diff --git a/content/docs/references/data/driver-sql.mdx b/content/docs/references/data/driver-sql.mdx index b51b5155f0..488cb2415a 100644 --- a/content/docs/references/data/driver-sql.mdx +++ b/content/docs/references/data/driver-sql.mdx @@ -72,6 +72,76 @@ const result = DataTypeMappingSchema.parse(data); | **ssl** | `boolean` | optional (default: `false`) | Enable SSL/TLS connection | | **sslConfig** | `{ rejectUnauthorized: boolean; ca?: string; cert?: string; key?: string }` | optional | SSL/TLS configuration (required when ssl is true) | +### Nested Shape: `SQLDriverConfig.capabilities` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **queryDateGranularity** | `Record` | optional | Per-granularity native date bucketing (day/week/month/quarter/year). Missing keys fall back to in-memory bucketing. | +| **autonumber** | `boolean` | optional | Driver natively generates persistent autonumber/sequence values | +| **batchSchemaSync** | `boolean` | optional | Supports batched schema sync to reduce schema DDL round-trips (absence = false) | +| **create** | `never` | optional | [REMOVED] `DriverCapabilities.create` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. CRUD is not optional for a driver: `create`/`find`/`findOne`/`update`/`delete` are REQUIRED `IDataDriver` methods and the engine calls them unconditionally. Delete the key. | +| **read** | `never` | optional | [REMOVED] `DriverCapabilities.read` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. CRUD is not optional for a driver: reads go through the REQUIRED `find`/`findOne`/`count` methods, called unconditionally. Delete the key. | +| **update** | `never` | optional | [REMOVED] `DriverCapabilities.update` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. CRUD is not optional for a driver: `update`/`upsert` are REQUIRED `IDataDriver` methods, called unconditionally. Delete the key. | +| **delete** | `never` | optional | [REMOVED] `DriverCapabilities.delete` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. CRUD is not optional for a driver: `delete` is a REQUIRED `IDataDriver` method, called unconditionally. Delete the key. | +| **bulkCreate** | `never` | optional | [REMOVED] `DriverCapabilities.bulkCreate` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. The bulk methods (`bulkCreate`/`bulkUpdate`/`bulkDelete`) are REQUIRED `IDataDriver` methods and the engine calls them directly; wire-level batch capability is advertised by REST discovery from the live composition (#3298), never from this record. Delete the key. | +| **bulkUpdate** | `never` | optional | [REMOVED] `DriverCapabilities.bulkUpdate` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. The bulk methods are REQUIRED `IDataDriver` methods and the engine calls them directly; wire-level batch capability is advertised by REST discovery from the live composition (#3298), never from this record. Delete the key. | +| **bulkDelete** | `never` | optional | [REMOVED] `DriverCapabilities.bulkDelete` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. The bulk methods are REQUIRED `IDataDriver` methods and the engine calls them directly; wire-level batch capability is advertised by REST discovery from the live composition (#3298), never from this record. Delete the key. | +| **transactions** | `never` | optional | [REMOVED] `DriverCapabilities.transactions` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Transaction use is gated on METHOD PRESENCE — `driver.beginTransaction` (`engine.transaction()`, ADR-0034 ambient transactions): a driver without the method gets the non-transactional fallback, whatever this bit claimed. Discovery's `transactionalBatch` capability is likewise derived from `engine.transaction` plus the mounted batch route, never from this bit. Delete the key. | +| **savepoints** | `never` | optional | [REMOVED] `DriverCapabilities.savepoints` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No savepoint code path exists in the engine — a capability bit for a feature the platform does not call is a false affordance, not documentation. Delete the key. | +| **isolationLevels** | `never` | optional | [REMOVED] `DriverCapabilities.isolationLevels` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Isolation is requested per transaction via `beginTransaction({ isolationLevel })`; no planner ever consulted this list to decide anything. Delete the key. | +| **queryFilters** | `never` | optional | [REMOVED] `DriverCapabilities.queryFilters` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `find()` receives the full QueryAST (`where`/`orderBy`/`limit`/`offset`) and MUST execute all of it — the "ObjectQL will filter in memory" fallback this bit's description promised was never built. Delete the key. | +| **querySorting** | `never` | optional | [REMOVED] `DriverCapabilities.querySorting` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `find()` receives the full QueryAST and MUST execute all of it — the "ObjectQL will sort in memory" fallback this bit's description promised was never built. Delete the key. | +| **queryPagination** | `never` | optional | [REMOVED] `DriverCapabilities.queryPagination` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `find()` receives the full QueryAST and MUST execute all of it — the "ObjectQL will paginate in memory" fallback this bit's description promised was never built. Delete the key. | +| **queryAggregations** | `never` | optional | [REMOVED] `DriverCapabilities.queryAggregations` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Aggregate pushdown is decided by `typeof driver.aggregate === 'function'` plus `queryDateGranularity` (engine aggregate dispatch) — never by this bit. Delete the key. | +| **queryWindowFunctions** | `never` | optional | [REMOVED] `DriverCapabilities.queryWindowFunctions` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans window functions through a driver, so there was nothing for the bit to switch on. Delete the key. | +| **querySubqueries** | `never` | optional | [REMOVED] `DriverCapabilities.querySubqueries` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans subqueries through a driver, so there was nothing for the bit to switch on. Delete the key. | +| **queryCTE** | `never` | optional | [REMOVED] `DriverCapabilities.queryCTE` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans Common Table Expressions through a driver, so there was nothing for the bit to switch on. Delete the key. | +| **joins** | `never` | optional | [REMOVED] `DriverCapabilities.joins` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Related data is resolved by the engine (lookup expansion over `find()`), not by driver-side JOIN planning — no code consulted the bit. Delete the key. | +| **fullTextSearch** | `never` | optional | [REMOVED] `DriverCapabilities.fullTextSearch` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `$search` is compiled by the engine into an `$or` of `$contains` predicates over the searchable fields (ADR-0061) and removed from the AST before the driver sees it — no driver-side full-text path exists. Delete the key. | +| **jsonQuery** | `never` | optional | [REMOVED] `DriverCapabilities.jsonQuery` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No engine path ever branched on driver-side JSON querying. Delete the key. | +| **geospatialQuery** | `never` | optional | [REMOVED] `DriverCapabilities.geospatialQuery` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No geospatial query path exists in the platform — declaring the bit advertised a capability nothing delivers. Delete the key. | +| **streaming** | `never` | optional | [REMOVED] `DriverCapabilities.streaming` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, and `findStream`, the only read this bit could describe, was itself removed in 17.0.0 (#4484): nothing ever called it, and two of its three implementations materialised the entire result set before yielding. The bit carried the same defect one level up (`SqlDriver` implemented `findStream` yet declared `streaming: false`; `InMemoryDriver` declared `true` over a full-table read) — which is what zero readers makes inevitable. Page large reads through `find()` with `limit`/`offset`. Delete the key. | +| **jsonFields** | `never` | optional | [REMOVED] `DriverCapabilities.jsonFields` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Field-type handling is negotiated per object at `syncSchema` time by the driver itself (e.g. `SqlDriver`'s per-object JSON/date column tracking); no engine path consulted the bit. Delete the key. | +| **arrayFields** | `never` | optional | [REMOVED] `DriverCapabilities.arrayFields` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Field-type handling is negotiated per object at `syncSchema` time by the driver itself; no engine path consulted the bit. Delete the key. | +| **vectorSearch** | `never` | optional | [REMOVED] `DriverCapabilities.vectorSearch` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No vector read path routes through `IDataDriver`. When one exists it should arrive WITH its caller and its capability bit together (the honest order under enforce-or-remove), not as a dangling boolean. Delete the key. | +| **schemaSync** | `never` | optional | [REMOVED] `DriverCapabilities.schemaSync` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Schema sync is gated on METHOD PRESENCE — `typeof driver.syncSchema === 'function'` (engine and ObjectQL plugin init). Delete the key. | +| **migrations** | `never` | optional | [REMOVED] `DriverCapabilities.migrations` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No migration engine ever consulted it. Delete the key. | +| **indexes** | `never` | optional | [REMOVED] `DriverCapabilities.indexes` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Declared indexes are materialised by the driver itself during schema sync (`SqlDriver.syncDeclaredIndexes`); no engine path consulted the bit. Delete the key. | +| **connectionPooling** | `never` | optional | [REMOVED] `DriverCapabilities.connectionPooling` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Pooling is configured via `poolConfig` and owned by the driver; `getPoolStats` is duck-typed where monitoring wants it. Nothing consulted the bit. Delete the key. | +| **preparedStatements** | `never` | optional | [REMOVED] `DriverCapabilities.preparedStatements` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Parameterised execution is an implementation detail of the driver (`execute(command, parameters)`); nothing consulted the bit. Delete the key. | +| **queryCache** | `never` | optional | [REMOVED] `DriverCapabilities.queryCache` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No query-cache layer keyed off it exists; `DriverOptions.skipCache` is a per-call hint to the driver, not a switch on this bit. Delete the key. | + +### Nested Shape: `SQLDriverConfig.poolConfig` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **min** | `number` | optional (default: `2`) | Minimum number of connections in pool | +| **max** | `number` | optional (default: `10`) | Maximum number of connections in pool | +| **idleTimeoutMillis** | `number` | optional (default: `30000`) | Time in ms before idle connection is closed | +| **connectionTimeoutMillis** | `number` | optional (default: `5000`) | Time in ms to wait for available connection | + +### Nested Shape: `SQLDriverConfig.dataTypeMapping` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **text** | `string` | ✅ | SQL type for text fields (e.g., VARCHAR, TEXT) | +| **number** | `string` | ✅ | SQL type for number fields (e.g., NUMERIC, DECIMAL, INT) | +| **boolean** | `string` | ✅ | SQL type for boolean fields (e.g., BOOLEAN, BIT) | +| **date** | `string` | ✅ | SQL type for date fields (e.g., DATE) | +| **datetime** | `string` | ✅ | SQL type for datetime fields (e.g., TIMESTAMP, DATETIME) | +| **json** | `string` | optional | SQL type for JSON fields (e.g., JSON, JSONB) | +| **uuid** | `string` | optional | SQL type for UUID fields (e.g., UUID, CHAR(36)) | +| **binary** | `string` | optional | SQL type for binary fields (e.g., BLOB, BYTEA) | + +### Nested Shape: `SQLDriverConfig.sslConfig` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **rejectUnauthorized** | `boolean` | optional (default: `true`) | Reject connections with invalid certificates | +| **ca** | `string` | optional | CA certificate file path or content | +| **cert** | `string` | optional | Client certificate file path or content | +| **key** | `string` | optional | Client private key file path or content | + --- diff --git a/content/docs/references/data/driver-turso.mdx b/content/docs/references/data/driver-turso.mdx index 9cf1398e38..7e10def6f0 100644 --- a/content/docs/references/data/driver-turso.mdx +++ b/content/docs/references/data/driver-turso.mdx @@ -74,6 +74,13 @@ Turso / libSQL Connection Configuration | **timeout** | `integer` | optional | Operation timeout in milliseconds for remote operations | | **mode** | `Enum<'local' \| 'replica' \| 'remote'>` | optional | Force a transport mode instead of inferring it from `url` | +### Nested Shape: `TursoConfig.sync` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **intervalSeconds** | `integer` | optional | Periodic sync interval in seconds (0 = manual only) | +| **onConnect** | `boolean` | optional | Sync immediately on connect | + --- diff --git a/content/docs/references/data/driver.mdx b/content/docs/references/data/driver.mdx index b871cb8e64..b0ebf332c9 100644 --- a/content/docs/references/data/driver.mdx +++ b/content/docs/references/data/driver.mdx @@ -80,6 +80,54 @@ const result = DriverCapabilitiesSchema.parse(data); | **connectionString** | `string` | optional | Database connection string (driver-specific format) | | **poolConfig** | `{ min: number; max: number; idleTimeoutMillis: number; connectionTimeoutMillis: number }` | optional | Connection pool configuration | +### Nested Shape: `DriverConfig.capabilities` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **queryDateGranularity** | `Record` | optional | Per-granularity native date bucketing (day/week/month/quarter/year). Missing keys fall back to in-memory bucketing. | +| **autonumber** | `boolean` | optional | Driver natively generates persistent autonumber/sequence values | +| **batchSchemaSync** | `boolean` | optional | Supports batched schema sync to reduce schema DDL round-trips (absence = false) | +| **create** | `never` | optional | [REMOVED] `DriverCapabilities.create` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. CRUD is not optional for a driver: `create`/`find`/`findOne`/`update`/`delete` are REQUIRED `IDataDriver` methods and the engine calls them unconditionally. Delete the key. | +| **read** | `never` | optional | [REMOVED] `DriverCapabilities.read` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. CRUD is not optional for a driver: reads go through the REQUIRED `find`/`findOne`/`count` methods, called unconditionally. Delete the key. | +| **update** | `never` | optional | [REMOVED] `DriverCapabilities.update` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. CRUD is not optional for a driver: `update`/`upsert` are REQUIRED `IDataDriver` methods, called unconditionally. Delete the key. | +| **delete** | `never` | optional | [REMOVED] `DriverCapabilities.delete` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. CRUD is not optional for a driver: `delete` is a REQUIRED `IDataDriver` method, called unconditionally. Delete the key. | +| **bulkCreate** | `never` | optional | [REMOVED] `DriverCapabilities.bulkCreate` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. The bulk methods (`bulkCreate`/`bulkUpdate`/`bulkDelete`) are REQUIRED `IDataDriver` methods and the engine calls them directly; wire-level batch capability is advertised by REST discovery from the live composition (#3298), never from this record. Delete the key. | +| **bulkUpdate** | `never` | optional | [REMOVED] `DriverCapabilities.bulkUpdate` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. The bulk methods are REQUIRED `IDataDriver` methods and the engine calls them directly; wire-level batch capability is advertised by REST discovery from the live composition (#3298), never from this record. Delete the key. | +| **bulkDelete** | `never` | optional | [REMOVED] `DriverCapabilities.bulkDelete` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. The bulk methods are REQUIRED `IDataDriver` methods and the engine calls them directly; wire-level batch capability is advertised by REST discovery from the live composition (#3298), never from this record. Delete the key. | +| **transactions** | `never` | optional | [REMOVED] `DriverCapabilities.transactions` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Transaction use is gated on METHOD PRESENCE — `driver.beginTransaction` (`engine.transaction()`, ADR-0034 ambient transactions): a driver without the method gets the non-transactional fallback, whatever this bit claimed. Discovery's `transactionalBatch` capability is likewise derived from `engine.transaction` plus the mounted batch route, never from this bit. Delete the key. | +| **savepoints** | `never` | optional | [REMOVED] `DriverCapabilities.savepoints` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No savepoint code path exists in the engine — a capability bit for a feature the platform does not call is a false affordance, not documentation. Delete the key. | +| **isolationLevels** | `never` | optional | [REMOVED] `DriverCapabilities.isolationLevels` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Isolation is requested per transaction via `beginTransaction({ isolationLevel })`; no planner ever consulted this list to decide anything. Delete the key. | +| **queryFilters** | `never` | optional | [REMOVED] `DriverCapabilities.queryFilters` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `find()` receives the full QueryAST (`where`/`orderBy`/`limit`/`offset`) and MUST execute all of it — the "ObjectQL will filter in memory" fallback this bit's description promised was never built. Delete the key. | +| **querySorting** | `never` | optional | [REMOVED] `DriverCapabilities.querySorting` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `find()` receives the full QueryAST and MUST execute all of it — the "ObjectQL will sort in memory" fallback this bit's description promised was never built. Delete the key. | +| **queryPagination** | `never` | optional | [REMOVED] `DriverCapabilities.queryPagination` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `find()` receives the full QueryAST and MUST execute all of it — the "ObjectQL will paginate in memory" fallback this bit's description promised was never built. Delete the key. | +| **queryAggregations** | `never` | optional | [REMOVED] `DriverCapabilities.queryAggregations` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Aggregate pushdown is decided by `typeof driver.aggregate === 'function'` plus `queryDateGranularity` (engine aggregate dispatch) — never by this bit. Delete the key. | +| **queryWindowFunctions** | `never` | optional | [REMOVED] `DriverCapabilities.queryWindowFunctions` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans window functions through a driver, so there was nothing for the bit to switch on. Delete the key. | +| **querySubqueries** | `never` | optional | [REMOVED] `DriverCapabilities.querySubqueries` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans subqueries through a driver, so there was nothing for the bit to switch on. Delete the key. | +| **queryCTE** | `never` | optional | [REMOVED] `DriverCapabilities.queryCTE` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans Common Table Expressions through a driver, so there was nothing for the bit to switch on. Delete the key. | +| **joins** | `never` | optional | [REMOVED] `DriverCapabilities.joins` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Related data is resolved by the engine (lookup expansion over `find()`), not by driver-side JOIN planning — no code consulted the bit. Delete the key. | +| **fullTextSearch** | `never` | optional | [REMOVED] `DriverCapabilities.fullTextSearch` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `$search` is compiled by the engine into an `$or` of `$contains` predicates over the searchable fields (ADR-0061) and removed from the AST before the driver sees it — no driver-side full-text path exists. Delete the key. | +| **jsonQuery** | `never` | optional | [REMOVED] `DriverCapabilities.jsonQuery` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No engine path ever branched on driver-side JSON querying. Delete the key. | +| **geospatialQuery** | `never` | optional | [REMOVED] `DriverCapabilities.geospatialQuery` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No geospatial query path exists in the platform — declaring the bit advertised a capability nothing delivers. Delete the key. | +| **streaming** | `never` | optional | [REMOVED] `DriverCapabilities.streaming` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, and `findStream`, the only read this bit could describe, was itself removed in 17.0.0 (#4484): nothing ever called it, and two of its three implementations materialised the entire result set before yielding. The bit carried the same defect one level up (`SqlDriver` implemented `findStream` yet declared `streaming: false`; `InMemoryDriver` declared `true` over a full-table read) — which is what zero readers makes inevitable. Page large reads through `find()` with `limit`/`offset`. Delete the key. | +| **jsonFields** | `never` | optional | [REMOVED] `DriverCapabilities.jsonFields` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Field-type handling is negotiated per object at `syncSchema` time by the driver itself (e.g. `SqlDriver`'s per-object JSON/date column tracking); no engine path consulted the bit. Delete the key. | +| **arrayFields** | `never` | optional | [REMOVED] `DriverCapabilities.arrayFields` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Field-type handling is negotiated per object at `syncSchema` time by the driver itself; no engine path consulted the bit. Delete the key. | +| **vectorSearch** | `never` | optional | [REMOVED] `DriverCapabilities.vectorSearch` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No vector read path routes through `IDataDriver`. When one exists it should arrive WITH its caller and its capability bit together (the honest order under enforce-or-remove), not as a dangling boolean. Delete the key. | +| **schemaSync** | `never` | optional | [REMOVED] `DriverCapabilities.schemaSync` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Schema sync is gated on METHOD PRESENCE — `typeof driver.syncSchema === 'function'` (engine and ObjectQL plugin init). Delete the key. | +| **migrations** | `never` | optional | [REMOVED] `DriverCapabilities.migrations` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No migration engine ever consulted it. Delete the key. | +| **indexes** | `never` | optional | [REMOVED] `DriverCapabilities.indexes` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Declared indexes are materialised by the driver itself during schema sync (`SqlDriver.syncDeclaredIndexes`); no engine path consulted the bit. Delete the key. | +| **connectionPooling** | `never` | optional | [REMOVED] `DriverCapabilities.connectionPooling` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Pooling is configured via `poolConfig` and owned by the driver; `getPoolStats` is duck-typed where monitoring wants it. Nothing consulted the bit. Delete the key. | +| **preparedStatements** | `never` | optional | [REMOVED] `DriverCapabilities.preparedStatements` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Parameterised execution is an implementation detail of the driver (`execute(command, parameters)`); nothing consulted the bit. Delete the key. | +| **queryCache** | `never` | optional | [REMOVED] `DriverCapabilities.queryCache` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No query-cache layer keyed off it exists; `DriverOptions.skipCache` is a per-call hint to the driver, not a switch on this bit. Delete the key. | + +### Nested Shape: `DriverConfig.poolConfig` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **min** | `number` | optional (default: `2`) | Minimum number of connections in pool | +| **max** | `number` | optional (default: `10`) | Maximum number of connections in pool | +| **idleTimeoutMillis** | `number` | optional (default: `30000`) | Time in ms before idle connection is closed | +| **connectionTimeoutMillis** | `number` | optional (default: `5000`) | Time in ms to wait for available connection | + --- diff --git a/content/docs/references/data/external-catalog.mdx b/content/docs/references/data/external-catalog.mdx index e22c173472..90a74855c1 100644 --- a/content/docs/references/data/external-catalog.mdx +++ b/content/docs/references/data/external-catalog.mdx @@ -42,6 +42,16 @@ const result = ExternalCatalogSchema.parse(data); | **dialect** | `string` | optional | Remote SQL dialect, when known. | | **tables** | `{ remoteSchema?: string; remoteName: string; columns: object[]; indexes?: object[]; … }[]` | ✅ | Snapshotted remote tables. | +### Nested Shape: `ExternalCatalog.tables[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **remoteSchema** | `string` | optional | Remote schema/database qualifier | +| **remoteName** | `string` | ✅ | Remote table/view name | +| **columns** | `{ name: string; sqlType: string; nullable: boolean; primaryKey: boolean; … }[]` | ✅ | Remote columns | +| **indexes** | `{ name: string; columns: string[]; unique: boolean }[]` | optional | Remote indexes, when introspectable | +| **rowCountEstimate** | `number` | optional | Approximate row count | + --- @@ -72,6 +82,16 @@ const result = ExternalCatalogSchema.parse(data); | **indexes** | `{ name: string; columns: string[]; unique: boolean }[]` | optional | Remote indexes, when introspectable | | **rowCountEstimate** | `number` | optional | Approximate row count | +### Nested Shape: `ExternalTable.columns[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Remote column name | +| **sqlType** | `string` | ✅ | Raw remote SQL type (e.g. "numeric(10,2)") | +| **nullable** | `boolean` | ✅ | Whether the remote column is nullable | +| **primaryKey** | `boolean` | optional (default: `false`) | Part of the remote primary key | +| **suggestedFieldType** | `string` | optional | ObjectStack field type suggested by the type-compat matrix | + --- diff --git a/content/docs/references/data/field.mdx b/content/docs/references/data/field.mdx index 6484ec166c..68318041a6 100644 --- a/content/docs/references/data/field.mdx +++ b/content/docs/references/data/field.mdx @@ -179,6 +179,72 @@ const result = CurrencyConfigSchema.parse(data); * `tags` * `vector` +### Nested Shape: `Field.storage` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **notNull** | `boolean` | optional | Emit a physical NOT NULL on the column (ADR-0113). Absent = the column stays nullable even under `required: true` — the write contract is enforced at the engine, the only sanctioned write path, not by the database. Declaring this over existing null rows is a destructive migration gated by the schema-drift ceremony. Incompatible with `requiredWhen` (a conditional contract cannot be an unconditional column constraint). | + +### Nested Shape: `Field.options[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string` | ✅ | Display label (human-readable, any case allowed) | +| **value** | `string` | ✅ | Stored value (lowercase machine identifier) | +| **color** | `string` | optional | Color code for badges/charts | +| **default** | `boolean` | optional | Is default option | +| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Per-option visibility predicate (CEL) — option is offered only when TRUE (else omitted). Env: the live `record` plus the host predicate scope, which binds `current_user` — wider than field-level visibleWhen, which has no `current_user`. e.g. P`record.country == 'cn'` or P`'admin' in current_user.positions` | + +### Nested Shape: `Field.inlineColumns[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Child field this column shows — the key the grid reads and writes on each row object (objectui GridColumn.name, #3951). The retired `field` spelling is refused. | +| **label** | `string` | optional | Column header; defaults to the child field's label via hydration. | +| **type** | `Enum<'text' \| 'number' \| 'currency' \| 'date' \| 'datetime' \| 'time' \| 'select' \| 'lookup' \| 'file'>` | optional | Cell control, derived from the child field's type when omitted. Declaring it opts the column out of schema hydration — supply the extras (options / reference / …) yourself. | +| **width** | `number` | optional | Fixed column width in px; omitted columns use type-based role sizing (text flexes, numeric/date/select stay fixed). | +| **required** | `boolean` | optional | Cell is flagged inline-invalid while empty. Computed columns are never required. | +| **options** | `{ label: string; value: string }[]` | optional | Select-cell options for `type: 'select'`; derived from the child field's options when the column declares no `type`. | +| **prefix** | `string` | optional | Currency symbol rendered inside a `currency` cell (default '¥'). | +| **step** | `number` | optional | Input step for numeric cells. | +| **reference** | `string` | optional | Referenced object for `type: 'lookup'` cells; derived from the child lookup field when the column declares no `type`. | +| **displayField** | `string` | optional | Label field shown for a picked lookup record. | +| **idField** | `string` | optional | Id field stored for a picked lookup record. | +| **multiple** | `boolean` | optional | Multi-value column: multi-record lookup, or multi-file upload cell. | +| **accept** | `string[]` | optional | Accepted MIME types / extensions for a `file` cell's picker (e.g. ['image/*', '.pdf']); omit to accept anything. | +| **defaultHidden** | `boolean` | optional | Collapsed into the grid's column chooser by default (not dropped); required columns are never default-hidden. | +| **computed** | `boolean` | optional | Read-only computed column, recomputed live from sibling cells via `expr` and written back into the row. | +| **expr** | `string` | optional | Arithmetic expression for a computed column — a BARE string over `+ - * / %`, parentheses, numeric literals and field refs (`record.qty` or `qty`), evaluated by the grid's own safe evaluator. Deliberately NOT a CEL Expression envelope; `{ dialect, source }` is refused here. | +| **scale** | `integer` | optional | Decimal places to round a computed numeric/currency result to. | +| **autofill** | `boolean` | optional | For `lookup` columns: picking a record copies its same-named fields into sibling columns (a product's unit_price/description). On by default; set false to disable. | +| **readonlyWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — the cell is read-only when TRUE, evaluated per row against the row as `record` plus the header as `parent` (e.g. P`parent.status == 'paid'`). | +| **requiredWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — the cell is required when TRUE. Same `record` + `parent` scope as `readonlyWhen`. | + +### Nested Shape: `Field.summaryOperations` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **object** | `string` | ✅ | Source child object name for roll-up | +| **field** | `string` | ✅ | Field on child object to aggregate (ignored for count) | +| **function** | `Enum<'count' \| 'sum' \| 'min' \| 'max' \| 'avg'>` | ✅ | Aggregation function to apply | +| **relationshipField** | `string` | optional | FK field on the child pointing back to this parent. Auto-detected from the child's lookup/master_detail field referencing this object when omitted; set explicitly only when the child has more than one such reference. | +| **filter** | `any` | optional | Predicate restricting which child rows are aggregated (a query `where` FilterCondition, e.g. `{ status: 'received' }` or `{ type: { $in: ['signup','trial'] } }`). Omit to aggregate all children. Lets one child object feed multiple filtered roll-ups. | + +### Nested Shape: `Field.currencyConfig` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **precision** | `integer` | optional (default: `2`) | Decimal precision (default: 2) | +| **currencyMode** | `Enum<'dynamic' \| 'fixed'>` | optional (default: `"dynamic"`) | Currency mode: dynamic (user selectable) or fixed (single currency) | +| **defaultCurrency** | `string` | optional (default: `"CNY"`) | Default or fixed currency code (ISO 4217, e.g., USD, CNY, EUR) | + +### Nested Shape: `Field.maskingRule` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **keepHead** | `integer` | ✅ | Number of leading characters to leave readable | +| **keepTail** | `integer` | ✅ | Number of trailing characters to leave readable | + --- @@ -304,6 +370,13 @@ Allowed Values: `phone`, `id_card`, `bank_account`, `email`, `name` | **readonlyWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — the cell is read-only when TRUE, evaluated per row against the row as `record` plus the header as `parent` (e.g. P`parent.status == 'paid'`). | | **requiredWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — the cell is required when TRUE. Same `record` + `parent` scope as `readonlyWhen`. | +### Nested Shape: `InlineGridColumn.options[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string` | ✅ | Option label shown in the select cell. | +| **value** | `string` | ✅ | Stored option value; must match the child select field's option values. | + --- diff --git a/content/docs/references/data/hook.mdx b/content/docs/references/data/hook.mdx index 00f63e9b3d..129afa93aa 100644 --- a/content/docs/references/data/hook.mdx +++ b/content/docs/references/data/hook.mdx @@ -44,6 +44,45 @@ const result = HookContextSchema.parse(data); | **api** | `any` | optional | Cross-object data access (IScopedContext — `object(name)` + `transaction(cb)`) | | **user** | `{ id?: string; name?: string; email?: string; organizationId?: string }` | optional | Current user info shortcut | +### Nested Shape: `HookContext.dispatch` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **mode** | `Enum<'record' \| 'per-row'>` | ✅ | 'record' = this call is the caller's whole write; 'per-row' = one of N dispatches for one write | +| **index** | `integer` | ✅ | 0-based position in the per-row fan-out; always 0 when mode is "record" | +| **scope** | `Record` | ✅ | Scratch shared by every dispatch of one caller write, across both phases (same object identity) | + +### Nested Shape: `HookContext.session` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **userId** | `string` | optional | | +| **actor** | `string` | optional | Service-principal label for audit attribution when the caller is not a real user (e.g. svc:flow:``) | +| **organizationId** | `string` | optional | Active organization ID (blessed developer-facing name) | +| **accessToken** | `string` | optional | | +| **isSystem** | `boolean` | optional | True when the call was made with an elevated system context (engine self-writes) | +| **skipTriggers** | `boolean` | optional | True when record-change automation (flow triggers) must be suppressed for this write — e.g. package seed replay. Lifecycle hooks still run. | +| **skipAutomations** | `boolean` | optional | True when metadata-bound automation hooks must be suppressed for this write — e.g. data import with "run automations" unchecked, or import undo. Implies skipTriggers; code-registered system hooks (audit, security) still run. | +| **positions** | `string[]` | optional | Position names held by the caller (ADR-0090 D3; formerly `roles`), copied from ExecutionContext.positions. For hook READS only — e.g. tailoring a message, or branching a business rule the hook runs through its own `ctx.api` channel. Authorization is decided by the security service on the ExecutionContext (permissions / positions / derived posture); this is NOT an authorization input and a hook must not gate a write by testing it. A hook context carries no `services` key, so the sharing service cannot be called from one either — the sharing gates already ran inside the engine before the hook chain. | +| **preserveAudit** | `boolean` | optional | True when this write is a historical import that must KEEP its caller-supplied updated_at/updated_by (and the readonly audit family) instead of being stamped with the import instant (#3493). Server-set, opt-in, absent on normal writes; read by the built-in audit hook. A stamping policy, not an authorization input. | +| **roles** | `never` | optional | [REMOVED] `HookContext.session.roles` was removed in @objectstack/spec 17.0.0 (#5050, ADR-0049 D2) — it was declared, read by two dead exemption branches (removed in #5049), and never produced: ObjectQL's `buildSession()` builds the session field by field and has never written `roles`, so every read resolved `undefined` and a guard keyed on it was dead code that merely LOOKED like an authorization decision. Delete the key. To gate a hook on the caller, read `ctx.session.userId` / `ctx.session.isSystem`; to judge PRIVILEGE, ask the security service, which evaluates the ADR-0095 vocabulary on the execution context — capability grants (`permissions`), placements (`positions`) and the derived posture — never a role-name string comparison (ADR-0090 D3 bans the `role` spelling outright). Nothing to migrate: a HookContext is built per operation by the engine and never stored, so no metadata source carries this key. NOTE an ACTION body's `ctx.session` is a different object and still carries its own `roles` array today; that surface is tracked separately (#5613) and is not what this key was. | + +### Nested Shape: `HookContext.provenance` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **flowRunId** | `string` | optional | Id of the automation flow run performing this write, when it originates from a flow data node. Lets a hook recognize the run that OWNS state that run itself opened — the approvals record lock exempts the run holding the pending request (#3456). | +| **attributedUserId** | `string` | optional | The real human credited for a write whose authorization subject was the SYSTEM — e.g. the admin whose better-auth `update-member-role` call the identity adapter executes as `isSystem` (#4586). ATTRIBUTION ONLY: the audit writer records it as `sys_audit_log.user_id`; no security middleware reads it, and it never becomes the subject the write is authorized as. | + +### Nested Shape: `HookContext.user` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | optional | | +| **name** | `string` | optional | | +| **email** | `string` | optional | | +| **organizationId** | `string` | optional | Active organization ID of the acting user (equals session.organizationId) | + --- diff --git a/content/docs/references/data/mapping.mdx b/content/docs/references/data/mapping.mdx index d8ae5397d8..34ccc2826b 100644 --- a/content/docs/references/data/mapping.mdx +++ b/content/docs/references/data/mapping.mdx @@ -56,6 +56,15 @@ const result = ImportFieldMappingSchema.parse(data); | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `Mapping.fieldMapping[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **source** | `string \| string[]` | ✅ | Source column header(s) | +| **target** | `string \| string[]` | ✅ | Target object field(s) | +| **transform** | `Enum<'none' \| 'constant' \| 'lookup' \| 'split' \| 'join' \| 'javascript' \| 'map'>` | optional (default: `"none"`) | | +| **params** | `{ value?: any; valueMap?: Record; separator?: string }` | optional | | + --- diff --git a/content/docs/references/data/object.mdx b/content/docs/references/data/object.mdx index 981bc79695..40252a92bc 100644 --- a/content/docs/references/data/object.mdx +++ b/content/docs/references/data/object.mdx @@ -87,6 +87,37 @@ const result = ApiMethod.parse(data); | **archive** | `{ after: string; to: string; keep?: string }` | optional | Cold-store archival (LifecycleService Archiver) — audit-class hot→cold hand-off. | | **reclaim** | `boolean` | optional | Run driver space reclamation (SQLite incremental_vacuum) after sweeping this object. Default true for non-record classes. | +### Nested Shape: `Lifecycle.retention` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **maxAge** | `string` | ✅ | Rows older than this (by created_at) are deleted by the Reaper — or archived first when `archive` is set. | +| **onlyWhen** | `Record` | optional | Row filter the retention applies to — per-field equality, `{$in: [...]}` or the null predicate `{$null: true\|false}` (e.g. `{ status: { $in: ["completed", "failed"] } }`). Rows OUTSIDE the filter are retained regardless of age: for tables that interleave live workflow state with terminal history (sys_automation_run). Incompatible with rotation storage and archive, which act on whole shards / age alone. | + +### Nested Shape: `Lifecycle.ttl` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Timestamp field the TTL is measured from (e.g. created_at, expires_at). | +| **expireAfter** | `string` | ✅ | Rows expire this long after `field` and are deleted by the Reaper. | +| **onlyWhen** | `Record` | optional | Row filter the TTL reap applies to — per-field equality, `{$in: [...]}` or the null predicate `{$null: true\|false}` (e.g. `{ revoked_at: { $null: true } }`). Rows OUTSIDE the filter are retained regardless of expiry: for tables that interleave live rows with terminal history a TTL keyed on the same timestamp would otherwise destroy (a sys_session audit tombstone backdates expires_at, so a naive TTL reaps tombstones first). Incompatible with rotation storage, which DROPs whole shards, and with archive, which selects rows by the ttl cutoff alone and does not apply this filter. | + +### Nested Shape: `Lifecycle.storage` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **strategy** | `'rotation'` | ✅ | Time-shard the table. The retained window (`shards` × `unit`) is the same on every dialect; the reclamation is not — SQLite DROPs the oldest shard whole (O(1) reclaim), other dialects reap that same window by age from `created_at`. | +| **shards** | `integer` | ✅ | Number of shards retained; total window = shards × unit. | +| **unit** | `Enum<'day' \| 'week' \| 'month'>` | ✅ | Time width of one shard. | + +### Nested Shape: `Lifecycle.archive` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **after** | `string` | ✅ | Rows older than this are copied to the archive datasource before hot deletion. | +| **to** | `string` | ✅ | Target datasource name for cold storage. When it is not registered, the Archiver skips (audit rows are then retained, never dropped unarchived). | +| **keep** | `string` | optional | How long archived rows are kept in cold storage (undefined = forever). | + --- @@ -153,6 +184,313 @@ const result = ApiMethod.parse(data); | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `Object.userActions` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **create** | `boolean \| { enabled?: boolean; visibleWhen?: string \| object; disabledWhen?: string \| object }` | optional | Show generic "New" button. Boolean, or an object adding visibleWhen/disabledWhen CEL predicates evaluated once per toolbar against the record in scope (the host record on a related list). | +| **import** | `boolean \| { enabled?: boolean; visibleWhen?: string \| object; disabledWhen?: string \| object }` | optional | Show CSV import wizard entry. Boolean, or an object adding visibleWhen/disabledWhen CEL predicates evaluated once per toolbar against the record in scope (the host record on a related list). | +| **edit** | `boolean \| { enabled?: boolean; visibleWhen?: string \| object; disabledWhen?: string \| object }` | optional | Allow inline / form edit of existing rows. Boolean, or an object adding per-record visibleWhen/disabledWhen CEL predicates. | +| **delete** | `boolean \| { enabled?: boolean; visibleWhen?: string \| object; disabledWhen?: string \| object }` | optional | Show row-level delete + bulk delete. Boolean, or an object adding per-record visibleWhen/disabledWhen CEL predicates. | +| **exportCsv** | `boolean` | optional | Show CSV export entry. | + +### Nested Shape: `Object.systemFields` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **tenant** | `boolean` | optional | Inject the organization_id column. Default true (the column is always provisioned; the multi-tenant flag governs only its index). | +| **audit** | `boolean` | optional | Inject the audit columns (created_at/created_by/updated_at/updated_by). Default true. | + +### Nested Shape: `Object.external` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **remoteName** | `string` | optional | Remote table/view name. Defaults to object.name. | +| **remoteSchema** | `string` | optional | Remote schema/database qualifier. | +| **writable** | `boolean` | optional (default: `false`) | Per-object write opt-in (also requires datasource.external.allowWrites). | +| **columnMap** | `Record` | optional | Remote column name → local field name. | +| **introspectedAt** | `string` | optional | Set by `os datasource introspect`; informational. | +| **ignoreColumns** | `string[]` | optional | Remote columns to skip during validation (dev convenience). | + +### Nested Shape: `Object.fields[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional | Machine name (snake_case) | +| **label** | `string` | optional | Human readable label | +| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>` | ✅ | Field Data Type | +| **description** | `string` | optional | Tooltip/Help text | +| **format** | `string` | optional | Format string (e.g. email, phone) | +| **required** | `boolean` | optional (default: `false`) | Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. On a multi-value lookup (`multiple: true`) required means NON-EMPTY array — an emptied required set fails validation loudly; `[]` does not satisfy it (#9447, maintainer ruling 2026-08-18). NOT a column constraint — the physical NOT NULL is a separate explicit opt-in (`storage.notNull`), so tightening this on a deployed object is safe: existing null rows stay readable, and editable as long as the write does not touch this field. | +| **storage** | `{ notNull?: boolean }` | optional | Physical storage constraints (ADR-0113). Owns the DDL the write contract deliberately does not imply. Absent = no storage-level constraint requested. | +| **searchable** | `boolean` | optional (default: `false`) | Is searchable | +| **multiple** | `boolean` | optional (default: `false`) | Allow multiple values (Stores as Array/JSON). Applicable for select, lookup, file, image. An emptied multi-value lookup reads back as `[]`, never `null` — the rule binds every writer (cascade repair, form clears, API writes), not just cascade repair (#9447, maintainer ruling 2026-08-18). | +| **unique** | `boolean \| 'global' \| 'organization'` | optional (default: `false`) | Unique constraint and its scope (ADR-0120). 'organization' = one holder per organization (NULL-safe composite with the organization key part on organization-scoped objects) — prefer this explicit spelling in new code; true = same per-organization scope (positional synonym, stays valid); 'global' = one holder across the whole installation. 'tenant'/'org' are rejected — the word is 'organization' | +| **defaultValue** | `any` | optional | Default applied on INSERT when the field is omitted or null (`''` is a real value, not absence). Three legal shapes (#7127), discriminated in the engine's own order: a CEL Expression envelope `{ dialect: 'cel', source: 'today()' }` (accepted structurally; result type is a runtime concern); a runtime TOKEN — `NOW()` on `datetime`/`date`/`time` only, `current_user` on `user` or `lookup` with `reference: 'sys_user'` only, neither on a multi-value field; or a LITERAL, which must satisfy this field's own stored value contract (ADR-0104 D1 `valueSchemaFor`). Anything else is refused at parse time with a prescriptive message. | +| **maxLength** | `integer` | optional | Max character length (positive integer). Only authorable on types that store a bounded string: text, textarea, email, url, phone, password, markdown, html, richtext, code, signature, qrcode. | +| **minLength** | `integer` | optional | Min character length (positive integer; `minLength: 0` is refused — express "no minimum" by omitting the key). Only authorable on types that store a bounded string: text, textarea, email, url, phone, password, markdown, html, richtext, code, signature, qrcode. | +| **precision** | `integer` | optional | Total digits (non-negative integer) | +| **scale** | `integer` | optional | Decimal places (non-negative integer) | +| **min** | `number` | optional | Minimum value | +| **max** | `number` | optional | Maximum value | +| **useGrouping** | `boolean` | optional | Digit-grouping presentation hint for `number` fields (#7768) — maps to `Intl.NumberFormat`'s `useGrouping`. Absent = renderer decides (interim heuristic today, locale default eventually); `false` = author opts out of grouping (e.g. a year or other ordinal/identifier integer); `true` = author pins grouping on. | +| **accept** | `string[]` | optional | Permitted upload types for media fields, as MIME types or extensions (e.g. ["image/*", ".pdf"]). Offered to the file picker AND enforced on write. | +| **maxSize** | `integer` | optional | Maximum permitted file size in BYTES for media fields. Enforced on write against the stored file size, not just checked in the browser. | +| **options** | `{ label: string; value: string; color?: string; default?: boolean; … }[]` | optional | Static options for select/multiselect | +| **reference** | `string` | optional | Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects. | +| **referenceVia** | `string` | optional | Declares this text field as the id half of a polymorphic pointer pair (ADR-0052 §5 ActivityPointer): the value is a record id of the object named by the SIBLING FIELD this key names — e.g. `record_id` with `referenceVia: 'object_name'`. The sibling must be a declared field on the same object holding an object machine name. Text fields only; mutually exclusive with `reference` (a static and a per-record target contradict). Enforced today at seed load: the value resolves as a natural key against the object the sibling column names, and an unresolvable pointer is refused loudly instead of stored verbatim. Adds no referential integrity or $expand behavior. | +| **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted | +| **inlineEdit** | `boolean \| Enum<'grid' \| 'form'>` | optional | Edit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form. | +| **inlineTitle** | `string` | optional | Title for the inline master-detail grid | +| **inlineColumns** | `{ name: string; label?: string; type?: Enum<'text' \| 'number' \| 'currency' \| 'date' \| 'datetime' \| 'time' \| 'select' \| 'lookup' \| 'file'>; width?: number; … }[]` | optional | Explicit columns for the inline grid (derived from the child object when omitted). Each entry is a strict, name-keyed column (`{ name, label?, type?, … }` — objectui GridColumn, #3951); identity-only entries (`{ name }`) hydrate everything else from the child object's fields. Unknown keys and the retired `field` spelling are refused at parse. | +| **inlineAmountField** | `string` | optional | Numeric child field summed for the inline grid total | +| **relatedList** | `boolean \| 'primary'` | optional | Show this child collection as a related list on the parent's detail page (read-side mirror of inlineEdit). false = suppress; true/absent = shown (stacked under the shared "Related" tab); 'primary' = core relationship, promoted to its own tab. Prominence intent, not a layout switch (ADR-0085). | +| **relatedListTitle** | `string` | optional | Title for the detail-page related list | +| **relatedListColumns** | `string[]` | optional | Explicit columns for the detail-page related list, as child field names (e.g. ['name', 'status']); derived from the child object (highlightFields → field walk) when omitted. Strings only — labels, cell types and formatting always derive from the child object's field definitions; column objects are refused at parse. | +| **relatedListFilter** | `any` | optional | Declarative default filter for the detail-page related list: AND-composed with the parent-relationship condition `{ [referenceField]: parentId }` — an authored constraint, never a user-editable suggestion. The related-list tab badge count honors the same composed filter, so counts match the visible rows. Canonical Query-DSL FilterCondition (the same dialect as a query `where`), e.g. `{ status: { $ne: 'deleted' } }` to hide soft-deleted children. | +| **displayField** | `string` | optional | Field shown as each candidate's label in the picker/popover (defaults to the referenced object's name/title). | +| **descriptionField** | `string` | optional | Secondary field shown under the label in the quick-select popover. | +| **lookupColumns** | `(string \| { field: string; label?: string; width?: string; type?: string })[]` | optional | Explicit columns for the record-picker table; auto-derived from the referenced object when omitted. | +| **lookupPageSize** | `integer` | optional | Rows per page in the record-picker dialog (default 10). | +| **lookupFilters** | `{ field: string; operator: Enum<'eq' \| 'ne' \| 'gt' \| 'lt' \| 'gte' \| 'lte' \| 'contains' \| 'in' \| 'notIn'>; value: any }[]` | optional | Base filters restricting which records are selectable (e.g. only active). The structured, picker-honoured lookup filter. | +| **dependsOn** | `(string \| { field: string; param?: string })[]` | optional | Declares that this field's available values depend on the value of other field(s) on the same record — the form gates the field until they are set and re-evaluates as they change. For `lookup`/`master_detail` it scopes the candidate query (string = same local/remote key; `{field,param}` when the remote filter key differs — the `{field,param}` form is lookup-only). For `select`/`multiselect`/`radio` the actual per-option rule lives in each option's `visibleWhen`; list the referenced fields here (string form) so the option list gates and refreshes with the parent. | +| **allowCreate** | `boolean` | optional | Allow inline quick-create from the record picker: when no match exists the user can create a record from the typed text (optimistic dataSource.create with the display field). Best for simple objects whose only required field is the display field. | +| **expression** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Formula expression (CEL). e.g. F`record.amount * 0.1` | +| **returnType** | `Enum<'number' \| 'text' \| 'boolean' \| 'date'>` | optional | Inferred value type of a formula field (number/text/boolean/date) | +| **summaryOperations** | `{ object: string; field: string; function: Enum<'count' \| 'sum' \| 'min' \| 'max' \| 'avg'>; relationshipField?: string; … }` | optional | Roll-up summary definition. The engine recomputes the value when child records are inserted/updated/deleted. | +| **language** | `string` | optional | Programming language for syntax highlighting (e.g., javascript, python, sql) | +| **step** | `number` | optional | Step increment for slider (default: 1) | +| **currencyConfig** | `{ precision?: integer; currencyMode?: Enum<'dynamic' \| 'fixed'>; defaultCurrency?: string }` | optional | Configuration for currency field type | +| **dimensions** | `integer` | optional | Vector dimensionality (e.g., 1536 for OpenAI embeddings) | +| **trackHistory** | `boolean` | optional | Render this field's value changes as human-readable entries on the record activity timeline (ADR-0052 §5b). Opt-in per field. | +| **group** | `string` | optional | Field group name for organizing fields in forms and layouts (e.g., "contact_info", "billing", "system") | +| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is shown only when TRUE (else hidden). e.g. P`record.type == 'invoice'` | +| **readonlyWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is read-only when TRUE. e.g. P`record.status == 'paid'` | +| **requiredWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is required when TRUE. The only slot; the `conditionalRequired` alias was removed in protocol 17 (#3855). | +| **conditionalRequired** | `never` | optional | [REMOVED] `conditionalRequired` was removed in @objectstack/spec 17 (#3855) — use `requiredWhen`. Rename the key; the value (a CEL predicate) is unchanged. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **widget** | `string` | optional | Form widget override — names a registered field component (resolved as `field:`) to render this field instead of the `type` default. Degrades to the `type` renderer when unregistered. e.g. "object-ref", "filter-condition", "recipient-picker". | +| **hidden** | `boolean` | optional (default: `false`) | Hidden from default UI | +| **internal** | `boolean` | optional | [#7728] Never return this field's value on the generic data path — the engine OMITS the key from `find`/`findOne` results, the 201 create body and the by-id update body, on the default projection AND when a client names the field in `?select=`. Storage, filtering and indexing are untouched, so a server-side verifier can still match on the column and a purpose-built mint route can still return the value once at creation. The read protection for ADR-0100's third credential channel (auth-subsystem one-way hashes on `text` columns). Omission, not masking: a mask signals 'a value is set', which carries no information on a `required` column. | +| **readonly** | `boolean` | optional (default: `false`) | Read-only — never editable in forms, AND server-enforced on BOTH write paths: a non-system write to this field is silently dropped from the payload on UPDATE (#2948/#3003) and on INSERT (#3043; a create can no longer directly seed e.g. `approval_status: "approved"`), symmetric with `readonlyWhen`. A stripped INSERT field still falls back to its `defaultValue`. Exempt from the strip on BOTH paths: `isSystem` writes (seed replay, migration). Exempt on the UPDATE path ONLY: an opt-in "historical" import (`preserveAudit`, #3493) — which admits a whitelist (the audit/timestamp family plus author-declared business `readonly` fields). On INSERT the exemption does NOT apply (#6640): a non-system create that requests `preserveAudit` still has its readonly fields stripped, and is warned loudly that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. A normal (non-system) import is NOT system-context and still strips. | +| **requiredPermissions** | `string[]` | optional | [ADR-0066 D3] Capabilities required to read/edit this field (mask on read, deny on write; AND-gate). | +| **maskingRule** | `Enum<'phone' \| 'id_card' \| 'bank_account' \| 'email' \| 'name'> \| { keepHead: integer; keepTail: integer }` | optional | [#8993] Partial masking rule enforced by the runtime FieldMasker (single channel — API, UI, export and AI context all see the same masked value). A named preset ('phone' 138****5678, 'id_card' keep 6+4, 'bank_account' keep last 4, 'email' j***@example.com, 'name' keep first char) or `{ keepHead, keepTail }`. Masked for every non-system caller unless the field's `requiredPermissions` are ALL held (that evaluation is the unmask gate); a permission set marking the field non-readable still deletes it entirely. Deterministic, length-preserving output; masked callers cannot filter/sort/group/aggregate on the field. | +| **ackPlaintextMasking** | `boolean` | optional | [ADR-0100] Affirm a generic `password` field's plaintext-at-rest / masked-on-read contract is intended, silencing the author-time warning (#3420). No effect on non-password fields. | +| **system** | `boolean` | optional | Auto-injected system/audit field (e.g. created_at, updated_by, organization_id). Tools that surface system fields separately from author-declared business fields should branch on this flag. | +| **sortable** | `boolean` | optional (default: `true`) | Whether field is sortable in list views | +| **inlineHelpText** | `string` | optional | Help text displayed below the field in forms | +| **placeholder** | `string` | optional | Placeholder text rendered inside the empty input (the HTML placeholder attribute); disappears once a value is entered. Distinct from `inlineHelpText` (always-visible help rendered beside/under the input) and `description` (tooltip/developer documentation). | +| **autonumberFormat** | `string` | optional (default: `"{0000}"`) | Auto-number format: literal text + `{0000}` counter, `{YYYY}`/`{MM}`/`{DD}`/`{YYYYMMDD}` date tokens (business tz), and `{field_name}` interpolation. Counter resets per rendered prefix (e.g. AD`{YYYYMMDD}``{0000}` resets daily). Omitted on an `autonumber` field ⇒ the contract default `{0000}` (#6555). | +| **externalId** | `boolean` | optional (default: `false`) | Is external ID for upsert operations | +| **_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. | + +### Nested Shape: `Object.indexes[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional | Index name (auto-generated if not provided) | +| **fields** | `string[]` | ✅ | Fields included in the index | +| **unique** | `boolean \| 'global' \| 'organization'` | optional (default: `false`) | Whether the index enforces uniqueness, and at which scope (ADR-0120). 'global' = materialized over exactly `fields`, no organization column injected — one holder across the whole installation; 'organization' = the driver prepends the NULL-safe organization key part (COALESCE(organization_id, '__global__')) at registration — one holder per organization; bare true = deprecated positional spelling of 'global' (warned in 17.x by lint unique/unscoped-declared-index, rejected at protocol 18, #5082) — state the scope. 'tenant'/'org' are rejected — the word is 'organization' | +| **type** | `never` | optional | [REMOVED] `indexes[].type` was removed in @objectstack/spec 17.0.0 (#5248, ADR-0049) — no driver ever read it. `SqlDriver.syncDeclaredIndexes` creates every declared index through knex's `table.index()` / `table.unique()`, which cannot express an access method, so the value changed no DDL; its `.default('btree')` merely made an inert knob show up in every parse output. Delete the key. The index method is the driver/dialect's decision (Postgres defaults to B-tree; `gin`/`gist`/`fulltext` are dialect-specific and are chosen by a database-layer migration when a workload actually needs one). Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **partial** | `never` | optional | [REMOVED] `indexes[].partial` was removed in @objectstack/spec 17.0.0 (#5248, #4943, ADR-0049) — no driver ever emitted the `WHERE` clause, so a declared partial index was materialized as a FULL index and the predicate silently did nothing. Delete the key. Partial indexes are built at the database layer, not the declaration surface: issue `CREATE [UNIQUE] INDEX … WHERE ` from a runtime migration (this is what `metadata-protocol`'s `ensureOverlayIndex` already does for `sys_metadata`). Drift detection is unaffected — it reads partiality back from the database's own DDL, never from this key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | + +### Nested Shape: `Object.fieldGroups[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **key** | `string` | ✅ | Group machine key (snake_case). Referenced by Field.group. | +| **label** | `string` | ✅ | Group display label | +| **icon** | `string` | optional | Icon name (Lucide/Material) for the group header | +| **description** | `string` | optional | Optional description shown under the group header | +| **collapse** | `Enum<'none' \| 'expanded' \| 'collapsed'>` | optional (default: `"none"`) | [ADR-0085] Section collapse behaviour: 'none' (always open, no toggle), 'expanded' (collapsible, starts open), 'collapsed' (collapsible, starts closed). | +| **defaultExpanded** | `boolean` | optional | [DEPRECATED → collapse] true → 'expanded', false → 'collapsed'. | +| **collapsible** | `boolean` | optional | [DEPRECATED → collapse] Boolean pair with `collapsed`; use the `collapse` enum. | +| **collapsed** | `boolean` | optional | [DEPRECATED → collapse] Boolean pair with `collapsible`; use the `collapse` enum. | + +### Nested Shape: `Object.tenancy` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | ✅ | Enable multi-tenancy for this object | +| **tenantField** | `string` | optional | Column this object is tenant-scoped by. Omit it unless the tenant column genuinely is not the platform's: when undeclared the driver falls back to `organization_id`, the kernel-injected column the RLS predicates and `tenantPolicy()` also assume. A declared name is honoured only when the object really has that field — otherwise the same `organization_id` fallback applies. No default is materialized here on purpose (#5315). | +| **organizationField** | `string` | optional | STAMP-ONLY (#8778, widened by cloud#1395): column carrying the organization a row is ABOUT, consulted by the three sanctioned platform-row writers — audit stamping, the approval-row writer (`plugin-approvals`), and the automation-run recorder (`service-automation`) — via the shared `resolveRecordOrganizationField` resolver in `@objectstack/metadata-core`. It does NOT tenant-scope anything — no read path (`applyTenantScope`, `injectTenantOnInsert`, `computeTenantLayer0Filter`) reads it, so declaring it never walls the object and never hides rows. Declare it only when the organization a row belongs to lives under a column that deliberately is NOT the tenant column: `sys_api_key` is the shipped example — a credential table that must stay unwalled (`enabled: false`) while history/revocation audit rows stamp the organization of the key they describe (`active_organization_id`). Ordinary tenant objects omit it; their stamp column is resolved from `tenantField` / `organization_id` already. Honoured only when the object really has the field, like `tenantField`. | + +### Nested Shape: `Object.access` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **default** | `Enum<'public' \| 'private'>` | optional (default: `"public"`) | Default exposure posture: public (covered by wildcard grants) \| private (needs explicit grant; exempt from wildcard RLS). | + +### Nested Shape: `Object.requiredPermissions` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **read** | `string[]` | optional | Capabilities required to read (find/findOne/count/aggregate). | +| **create** | `string[]` | optional | Capabilities required to create (insert). | +| **update** | `string[]` | optional | Capabilities required to update (update/transfer/restore). | +| **delete** | `string[]` | optional | Capabilities required to delete (delete/purge). | + +### Nested Shape: `Object.lifecycle` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **class** | `Enum<'record' \| 'audit' \| 'telemetry' \| 'transient' \| 'event'>` | ✅ | Persistence contract: record (business truth, permanent) \| audit (compliance ledger) \| telemetry (high-freq log) \| transient (ephemeral state) \| event (bus messages). | +| **retention** | `{ maxAge: string; onlyWhen?: Record }` | optional | Age-based retention window enforced by the LifecycleService Reaper. | +| **ttl** | `{ field: string; expireAfter: string; onlyWhen?: Record }` | optional | Per-row TTL auto-expiry (transient/event classes). | +| **storage** | `{ strategy: 'rotation'; shards: integer; unit: Enum<'day' \| 'week' \| 'month'> }` | optional | Physical storage strategy for high-frequency telemetry (LifecycleService Rotator). | +| **archive** | `{ after: string; to: string; keep?: string }` | optional | Cold-store archival (LifecycleService Archiver) — audit-class hot→cold hand-off. | +| **reclaim** | `boolean` | optional | Run driver space reclamation (SQLite incremental_vacuum) after sweeping this object. Default true for non-record classes. | + +### Nested Shape: `Object.activityMilestones[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field to watch (typically a status/stage select). | +| **value** | `string` | ✅ | The value the field must transition INTO to fire the milestone. | +| **summary** | `string` | ✅ | Activity summary template; `{field}` tokens interpolate the record value. e.g. "Deal won: `{name}`". | +| **type** | `string` | optional | Activity type for the emitted row (default "completed"). | + +### Nested Shape: `Object.listViews[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional | Internal view name (lowercase snake_case) | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **type** | `Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>` | optional (default: `"grid"`) | | +| **data** | `{ provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source configuration (defaults to "object" provider) | +| **columns** | `string[] \| { field: string; label?: string \| Record; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | ✅ | Fields to display as columns | +| **filter** | `{ field: string; operator?: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Filter criteria (JSON Rules) | +| **sort** | `string \| { field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | | +| **searchableFields** | `string[]` | optional | Fields enabled for search | +| **filterableFields** | `string[]` | optional | Legacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters | +| **resizable** | `boolean` | optional | Enable column resizing | +| **compactToolbar** | `boolean` | optional | Collapse Group/Color/Density/Hide-fields into a single View settings popover | +| **selection** | `{ type?: Enum<'none' \| 'single' \| 'multiple'> }` | optional | Row selection configuration | +| **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; view?: string; preventNavigation?: boolean; openNewTab?: boolean; … }` | optional | Configuration for item click navigation (page, drawer, modal, etc.) | +| **pagination** | `{ pageSize?: integer; pageSizeOptions?: integer[] }` | optional | Pagination configuration | +| **kanban** | `{ groupByField: string; summarizeField?: string; columns: string[] }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout | +| **calendar** | `{ startDateField: string; endDateField?: string; titleField: string; colorField?: string }` | optional | Calendar configuration — applies when the view renders as a calendar layout | +| **gantt** | `{ startDateField: string; endDateField: string; titleField: string; progressField?: string; … } & Record` | optional | Gantt-timeline configuration — applies when the view renders as a gantt layout | +| **gallery** | `{ coverField?: string; coverFit?: Enum<'cover' \| 'contain'>; cardSize?: Enum<'small' \| 'medium' \| 'large'>; titleField?: string; … }` | optional | Gallery/card view configuration | +| **timeline** | `{ startDateField: string; endDateField?: string; titleField: string; groupByField?: string; … }` | optional | Timeline view configuration | +| **chart** | `{ chartType?: Enum<'bar' \| 'line' \| 'pie' \| 'area' \| 'scatter'>; dataset: string; dimensions?: string[]; values: string[] }` | optional | List chart view configuration | +| **map** | `{ latitudeField?: string; longitudeField?: string; locationField?: string; titleField?: string; … }` | optional | Map configuration — applies when the view renders as a map layout | +| **tree** | `{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer } & Record` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout | +| **description** | `string \| Record` | optional | View description for documentation/tooltips | +| **sharing** | `{ type?: Enum<'personal' \| 'collaborative'>; lockedBy?: string }` | optional | View sharing and access configuration | +| **rowHeight** | `Enum<'compact' \| 'short' \| 'medium' \| 'tall' \| 'extra_tall'>` | optional | Row height / density setting | +| **grouping** | `{ fields: object[] }` | optional | Group records by one or more fields | +| **rowColor** | `{ field: string; colors?: Record }` | optional | Color rows based on field value | +| **hiddenFields** | `string[]` | optional | Fields to hide in this specific view | +| **fieldOrder** | `string[]` | optional | Explicit field display order for this view | +| **rowActions** | `string[]` | optional | Actions available for individual row items | +| **bulkActions** | `string[]` | optional | Actions available when multiple rows are selected | +| **bulkActionDefs** | `{ name: string; label?: string; icon?: string; variant?: Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'outline'>; … }[]` | optional | Rich bulk action definitions (schema-driven, executed via BulkActionDialog). Use a def for a mass data-plane mutation ('update' with a `patch` / 'delete') that no action expresses, or for an `operation: 'custom'` + `execution: 'aggregate'` entry (objectui#3139) that dispatches the action it NAMES once for the whole selection — the renderer injects `params._selectedIds: string[]` (read that on the server, not `recordId`) so a single call can produce one aggregate artifact (zip of QR codes, merged PDF, batch print). Aggregate results are all-or-nothing: a handler that cannot cover the whole selection must reject, and per-row retry is replaced by re-running the action. `batchSize` does not apply (the call is never chunked); set `maxRecords` on defs whose server work is expensive. For the PER-RECORD dispatch use `bulkActions: ['']` instead — the bare-string form, promoted with the action's own label, params and `visible`; a 'custom' def without `execution: 'aggregate'` has no dispatcher and is refused at parse time (#4457). Toolbar url/api actions can also interpolate the current selection via `${ctx.selection.ids}` / `${ctx.selection.count}`. | +| **conditionalFormatting** | `{ condition: string \| object; style: Record }[]` | optional | Conditional formatting rules for list rows | +| **inlineEdit** | `boolean` | optional | Allow inline editing of records directly in the list view | +| **exportOptions** | `Enum<'csv' \| 'xlsx' \| 'json'>[] \| { formats?: Enum<'csv' \| 'xlsx' \| 'json'>[]; maxRecords?: integer; includeHeaders?: boolean; fileNamePrefix?: string; … }` | optional | Export configuration for the list toolbar export menu: `{ formats?, maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }`. A bare format array is the legacy spelling and lifts to `{ formats: [...] }` at parse. | +| **userActions** | `{ sort?: boolean; search?: boolean; filter?: boolean; refresh?: boolean; … }` | optional | User action toggles for the view toolbar | +| **appearance** | `{ showDescription?: boolean; allowedVisualizations?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>[] }` | optional | Appearance and visualization configuration | +| **tabs** | `{ name: string; label?: string \| Record; icon?: string; view?: string; … }[]` | optional | Tab definitions for multi-tab view interface | +| **addRecord** | `{ enabled?: boolean; position?: Enum<'top' \| 'bottom' \| 'both'>; mode?: Enum<'inline' \| 'form' \| 'modal'>; formView?: string }` | optional | Add record entry point configuration | +| **showRecordCount** | `boolean` | optional | Show record count at the bottom of the list | +| **allowPrinting** | `boolean` | optional | Allow users to print the view | +| **emptyState** | `{ title?: string \| Record; message?: string \| Record; icon?: string }` | optional | Empty state configuration when no records found | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes for the list view | +| **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **striped** | `never` | optional | [REMOVED] `view.striped` 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, so authoring it was a parse-clean no-op. There is no authorable striped-rows switch; delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **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. | +| **userFilters** | `{ element?: Enum<'dropdown' \| 'toggle'>; fields?: object[] }` | optional | | + +### Nested Shape: `Object.enable` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **trackHistory** | `boolean` | optional (default: `false`) | Show the record History tab (audit-trail UI). Pair with per-field trackHistory to pick which field diffs are summarized; audit capture itself is always on for compliance | +| **searchable** | `boolean` | optional (default: `true`) | Index records for global search | +| **apiEnabled** | `boolean` | optional (default: `true`) | Expose object via automatic APIs | +| **apiMethods** | `Enum<'get' \| 'list' \| 'create' \| 'update' \| 'delete' \| 'bulk'>[]` | optional | Whitelist of allowed API operations (six primitives; undefined = all, [] = none) | +| **files** | `boolean` | optional (default: `false`) | Generic record Attachments panel (sys_attachment). Opt-in: true surfaces the panel and permits attachments to target this object; otherwise any write that makes an attachment target it is rejected (403 FILES_DISABLED) — a create and an update that re-points an existing attachment alike. Field.file/Field.image are independent | +| **feeds** | `boolean` | optional (default: `true`) | Record comments/collaboration feed. Default on; explicit false hides the feed UI and rejects any write that makes a comment target this object (403 FEEDS_DISABLED) — a new comment and an update that re-threads an existing one alike | +| **activities** | `boolean` | optional (default: `true`) | Record activity timeline (sys_activity mirror of CRUD). Default on; explicit false stops mirroring and hides the timeline | +| **clone** | `boolean` | optional (default: `true`) | Allow record deep cloning | + +### Nested Shape: `Object.publicSharing` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `false`) | Allow records of this object to be published via share link | +| **allowedAudiences** | `Enum<'public' \| 'link_only' \| 'signed_in' \| 'email'>[]` | optional | Audiences callers may select when creating a link | +| **allowedPermissions** | `Enum<'view' \| 'comment' \| 'edit'>[]` | optional | Permission levels selectable on the share dialog | +| **maxExpiryDays** | `integer` | optional | Reject links with expiry beyond this many days | +| **redactFields** | `string[]` | optional | Field names removed from records served via a share token | +| **eligibility** | `string` | optional | CEL expression that must evaluate to true on the target record | + +### Nested Shape: `Object.actions[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Machine name (lowercase snake_case) | +| **label** | `string \| Record` | ✅ | Display label | +| **description** | `string \| Record` | optional | Explanatory line shown under the title in the action's param dialog. Carries the confirm question for an action that collects params (one dialog, not two — #7278). Not the LLM-facing `ai.description`. | +| **objectName** | `string` | optional | Target object this action belongs to. When set, the action is auto-merged into the object's actions array by defineStack(). | +| **icon** | `string` | optional | Icon name | +| **locations** | `Enum<'list_toolbar' \| 'list_item' \| 'record_header' \| 'record_more' \| …>[]` | optional | Locations where this action is visible | +| **component** | `Enum<'action:button' \| 'action:icon' \| 'action:menu' \| 'action:group'>` | optional | Visual component override | +| **type** | `Enum<'script' \| 'url' \| 'modal' \| 'flow' \| 'api' \| 'form'>` | optional (default: `"script"`) | Action functionality type | +| **target** | `string` | optional | URL, Script Name, Flow ID, or API Endpoint. Supports $`{param.X}` and $`{ctx.X}` interpolation. | +| **openIn** | `Enum<'self' \| 'new-tab'>` | optional | For type:'url' — where to open `target`. 'new-tab' opens a new browser tab; 'self' navigates in place. When omitted, external/absolute URLs open in a new tab and relative URLs navigate in place. Static execution option — keep it OUT of `params` (which is user-input-collection only). | +| **body** | `{ language: 'expression'; source: string } \| { language: 'js'; source: string; capabilities?: Enum<'api.read' \| 'api.write' \| 'api.transaction' \| 'crypto.uuid' \| 'log'>[]; timeoutMs?: integer; … }` | optional | Action body — expression (L1) or sandboxed JS (L2). Only used when type is `script`. | +| **execute** | `never` | optional | [REMOVED] `execute` was removed in @objectstack/spec 17 (#3855) — use `target`. Rename the key; the value (a handler / flow / URL ref) is unchanged. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **params** | `{ name?: string; field?: string; objectOverride?: string; label?: string \| Record; … }[]` | optional | Input parameters required from user — an ActionParam[] DEFINITION array, never a payload map (a static request body goes in `bodyExtra`). | +| **variant** | `Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'link'>` | optional | Button visual variant for styling (primary = highlighted, danger = destructive, ghost = transparent) | +| **order** | `number` | optional | Sort order within a location group (lower = higher). Promotes/demotes an action toward the record_header primary button; stable, so actions without `order` keep their registration order. | +| **confirmText** | `string \| Record` | optional | Confirmation message before execution. On a registered action, pairing this with a non-empty `params` is refused (#7428) — that opens a second dialog for one decision; put the question on `description` instead. Correct on a param-LESS action, where the confirm is the only dialog there is. | +| **successMessage** | `string \| Record` | optional | Success message to show after execution | +| **errorMessage** | `string \| Record` | optional | Error message to show when the action fails (overrides the raw error). | +| **refreshAfter** | `boolean` | optional (default: `false`) | Refresh view after execution | +| **undoable** | `boolean` | optional | Offer an Undo affordance after this single-record update action succeeds. | +| **resultDialog** | `{ title?: string \| Record; description?: string \| Record; acknowledge?: string \| Record; format?: Enum<'qrcode' \| 'code-list' \| 'secret' \| 'text' \| 'json'>; … }` | optional | Render API response in a one-shot reveal dialog (suppresses successMessage when set). | +| **visible** | `boolean \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate — `true`/`false` literal, CEL string, or `{dialect, source}` envelope. The action is offered when it evaluates TRUE. Omit = always visible. | +| **requiresFeature** | `Enum<'twoFactor' \| 'organization' \| 'multiOrgEnabled' \| 'degradedTenancy' \| …>` | optional | Public auth feature flag gating this action; lowered into `visible` at parse time. | +| **disabled** | `boolean \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Disabled predicate — `true`/`false` literal, CEL string, or `{dialect, source}` envelope. The action is shown but refused when it evaluates TRUE. Omit = never disabled. | +| **requiredPermissions** | `string[]` | optional | [ADR-0066 D4] Capabilities required to invoke this action. Enforced with 403 on the platform action route (script/flow/modal + MCP) and mirrored as a UI hide; a `type: api` action pointed at a custom endpoint must re-check it there. | +| **shortcut** | `never` | optional | [REMOVED] `action.shortcut` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — it never triggered anything: no keydown listener feeds ActionEngine.getShortcuts(), and objectui's keyboard stack (useKeyboardShortcuts) is hand-registered and never consults action metadata. Delete the key. For a real shortcut, register the key in the Console keyboard stack and have its handler invoke the action by name. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **bulkEnabled** | `never` | optional | [REMOVED] `action.bulkEnabled` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — the multi-select toolbar is driven by the LIST VIEW's `bulkActions` / `bulkActionDefs`, never by this flag, so setting it changed nothing. Delete the key and declare the action in the view's `bulkActions` instead. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **ai** | `{ exposed?: boolean; description?: string; category?: Enum<'data' \| 'action' \| 'flow' \| 'integration' \| 'vector_search' \| 'analytics' \| 'utility'>; paramHints?: Record; … }` | optional | AI exposure (opt-in). Set ai.exposed=true + ai.description to make this callable by agents. | +| **recordIdParam** | `string` | optional | Body key to inject the row id into when running from a list_item context. | +| **recordIdField** | `string` | optional | Row field whose value seeds recordIdParam. Defaults to "id". | +| **bodyShape** | `'flat' \| { wrap: string }` | optional | Body wrapping: flat (default) or `{ wrap: key }` to nest user-collected params under a key. | +| **method** | `Enum<'POST' \| 'PATCH' \| 'PUT' \| 'DELETE'>` | optional | HTTP method for type:"api" actions. Defaults to POST. | +| **bodyExtra** | `Record` | optional | Static request-body fields for a type:"api" action, merged last (overrides user params). `{{page.}}` tokens are resolved by the runtime. This — not `params` — is where a payload goes. | +| **mode** | `Enum<'create' \| 'edit' \| 'delete' \| 'custom'>` | optional | Semantic mode of the action. | +| **opensInNewTab** | `boolean` | optional | Open the action result in a new tab. The renderer pre-opens the tab synchronously on click (popup-blocker-safe) and navigates it to the handler's redirectUrl. | +| **newTabUrl** | `string` | optional | Direct new-tab URL template (`{recordId}` placeholder). When set with opensInNewTab, the renderer navigates the pre-opened tab here immediately — no action POST. The endpoint must enforce auth itself. | +| **onSuccess** | `{ navigate: string; openIn?: Enum<'self' \| 'newTab'> }` | optional | Post-success navigation for type:'api' and type:'script' actions (#9566/#9474). `navigate` is a route/URL template interpolating $`{param.*}`, $`{ctx.*}` and $`{result.*}` (the server response); `openIn` defaults 'self'. The handler-return convention (`{ redirectUrl }` without openIn) keeps its 17.0.0 new-tab behavior. | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **_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. | + +### Nested Shape: `Object.protection` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | ✅ | Lock policy — none \| no-overlay \| no-delete \| full. | +| **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | +| **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | + --- @@ -200,6 +538,93 @@ const result = ApiMethod.parse(data); | **indexes** | `{ name?: string; fields: string[]; unique?: boolean \| 'global' \| 'organization' }[]` | optional | Additional indexes to merge into the target object | | **priority** | `integer` | optional (default: `200`) | Merge priority (higher = applied later) | +### Nested Shape: `ObjectExtension.fields[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional | Machine name (snake_case) | +| **label** | `string` | optional | Human readable label | +| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>` | ✅ | Field Data Type | +| **description** | `string` | optional | Tooltip/Help text | +| **format** | `string` | optional | Format string (e.g. email, phone) | +| **required** | `boolean` | optional (default: `false`) | Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. On a multi-value lookup (`multiple: true`) required means NON-EMPTY array — an emptied required set fails validation loudly; `[]` does not satisfy it (#9447, maintainer ruling 2026-08-18). NOT a column constraint — the physical NOT NULL is a separate explicit opt-in (`storage.notNull`), so tightening this on a deployed object is safe: existing null rows stay readable, and editable as long as the write does not touch this field. | +| **storage** | `{ notNull?: boolean }` | optional | Physical storage constraints (ADR-0113). Owns the DDL the write contract deliberately does not imply. Absent = no storage-level constraint requested. | +| **searchable** | `boolean` | optional (default: `false`) | Is searchable | +| **multiple** | `boolean` | optional (default: `false`) | Allow multiple values (Stores as Array/JSON). Applicable for select, lookup, file, image. An emptied multi-value lookup reads back as `[]`, never `null` — the rule binds every writer (cascade repair, form clears, API writes), not just cascade repair (#9447, maintainer ruling 2026-08-18). | +| **unique** | `boolean \| 'global' \| 'organization'` | optional (default: `false`) | Unique constraint and its scope (ADR-0120). 'organization' = one holder per organization (NULL-safe composite with the organization key part on organization-scoped objects) — prefer this explicit spelling in new code; true = same per-organization scope (positional synonym, stays valid); 'global' = one holder across the whole installation. 'tenant'/'org' are rejected — the word is 'organization' | +| **defaultValue** | `any` | optional | Default applied on INSERT when the field is omitted or null (`''` is a real value, not absence). Three legal shapes (#7127), discriminated in the engine's own order: a CEL Expression envelope `{ dialect: 'cel', source: 'today()' }` (accepted structurally; result type is a runtime concern); a runtime TOKEN — `NOW()` on `datetime`/`date`/`time` only, `current_user` on `user` or `lookup` with `reference: 'sys_user'` only, neither on a multi-value field; or a LITERAL, which must satisfy this field's own stored value contract (ADR-0104 D1 `valueSchemaFor`). Anything else is refused at parse time with a prescriptive message. | +| **maxLength** | `integer` | optional | Max character length (positive integer). Only authorable on types that store a bounded string: text, textarea, email, url, phone, password, markdown, html, richtext, code, signature, qrcode. | +| **minLength** | `integer` | optional | Min character length (positive integer; `minLength: 0` is refused — express "no minimum" by omitting the key). Only authorable on types that store a bounded string: text, textarea, email, url, phone, password, markdown, html, richtext, code, signature, qrcode. | +| **precision** | `integer` | optional | Total digits (non-negative integer) | +| **scale** | `integer` | optional | Decimal places (non-negative integer) | +| **min** | `number` | optional | Minimum value | +| **max** | `number` | optional | Maximum value | +| **useGrouping** | `boolean` | optional | Digit-grouping presentation hint for `number` fields (#7768) — maps to `Intl.NumberFormat`'s `useGrouping`. Absent = renderer decides (interim heuristic today, locale default eventually); `false` = author opts out of grouping (e.g. a year or other ordinal/identifier integer); `true` = author pins grouping on. | +| **accept** | `string[]` | optional | Permitted upload types for media fields, as MIME types or extensions (e.g. ["image/*", ".pdf"]). Offered to the file picker AND enforced on write. | +| **maxSize** | `integer` | optional | Maximum permitted file size in BYTES for media fields. Enforced on write against the stored file size, not just checked in the browser. | +| **options** | `{ label: string; value: string; color?: string; default?: boolean; … }[]` | optional | Static options for select/multiselect | +| **reference** | `string` | optional | Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects. | +| **referenceVia** | `string` | optional | Declares this text field as the id half of a polymorphic pointer pair (ADR-0052 §5 ActivityPointer): the value is a record id of the object named by the SIBLING FIELD this key names — e.g. `record_id` with `referenceVia: 'object_name'`. The sibling must be a declared field on the same object holding an object machine name. Text fields only; mutually exclusive with `reference` (a static and a per-record target contradict). Enforced today at seed load: the value resolves as a natural key against the object the sibling column names, and an unresolvable pointer is refused loudly instead of stored verbatim. Adds no referential integrity or $expand behavior. | +| **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted | +| **inlineEdit** | `boolean \| Enum<'grid' \| 'form'>` | optional | Edit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form. | +| **inlineTitle** | `string` | optional | Title for the inline master-detail grid | +| **inlineColumns** | `{ name: string; label?: string; type?: Enum<'text' \| 'number' \| 'currency' \| 'date' \| 'datetime' \| 'time' \| 'select' \| 'lookup' \| 'file'>; width?: number; … }[]` | optional | Explicit columns for the inline grid (derived from the child object when omitted). Each entry is a strict, name-keyed column (`{ name, label?, type?, … }` — objectui GridColumn, #3951); identity-only entries (`{ name }`) hydrate everything else from the child object's fields. Unknown keys and the retired `field` spelling are refused at parse. | +| **inlineAmountField** | `string` | optional | Numeric child field summed for the inline grid total | +| **relatedList** | `boolean \| 'primary'` | optional | Show this child collection as a related list on the parent's detail page (read-side mirror of inlineEdit). false = suppress; true/absent = shown (stacked under the shared "Related" tab); 'primary' = core relationship, promoted to its own tab. Prominence intent, not a layout switch (ADR-0085). | +| **relatedListTitle** | `string` | optional | Title for the detail-page related list | +| **relatedListColumns** | `string[]` | optional | Explicit columns for the detail-page related list, as child field names (e.g. ['name', 'status']); derived from the child object (highlightFields → field walk) when omitted. Strings only — labels, cell types and formatting always derive from the child object's field definitions; column objects are refused at parse. | +| **relatedListFilter** | `any` | optional | Declarative default filter for the detail-page related list: AND-composed with the parent-relationship condition `{ [referenceField]: parentId }` — an authored constraint, never a user-editable suggestion. The related-list tab badge count honors the same composed filter, so counts match the visible rows. Canonical Query-DSL FilterCondition (the same dialect as a query `where`), e.g. `{ status: { $ne: 'deleted' } }` to hide soft-deleted children. | +| **displayField** | `string` | optional | Field shown as each candidate's label in the picker/popover (defaults to the referenced object's name/title). | +| **descriptionField** | `string` | optional | Secondary field shown under the label in the quick-select popover. | +| **lookupColumns** | `(string \| { field: string; label?: string; width?: string; type?: string })[]` | optional | Explicit columns for the record-picker table; auto-derived from the referenced object when omitted. | +| **lookupPageSize** | `integer` | optional | Rows per page in the record-picker dialog (default 10). | +| **lookupFilters** | `{ field: string; operator: Enum<'eq' \| 'ne' \| 'gt' \| 'lt' \| 'gte' \| 'lte' \| 'contains' \| 'in' \| 'notIn'>; value: any }[]` | optional | Base filters restricting which records are selectable (e.g. only active). The structured, picker-honoured lookup filter. | +| **dependsOn** | `(string \| { field: string; param?: string })[]` | optional | Declares that this field's available values depend on the value of other field(s) on the same record — the form gates the field until they are set and re-evaluates as they change. For `lookup`/`master_detail` it scopes the candidate query (string = same local/remote key; `{field,param}` when the remote filter key differs — the `{field,param}` form is lookup-only). For `select`/`multiselect`/`radio` the actual per-option rule lives in each option's `visibleWhen`; list the referenced fields here (string form) so the option list gates and refreshes with the parent. | +| **allowCreate** | `boolean` | optional | Allow inline quick-create from the record picker: when no match exists the user can create a record from the typed text (optimistic dataSource.create with the display field). Best for simple objects whose only required field is the display field. | +| **expression** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Formula expression (CEL). e.g. F`record.amount * 0.1` | +| **returnType** | `Enum<'number' \| 'text' \| 'boolean' \| 'date'>` | optional | Inferred value type of a formula field (number/text/boolean/date) | +| **summaryOperations** | `{ object: string; field: string; function: Enum<'count' \| 'sum' \| 'min' \| 'max' \| 'avg'>; relationshipField?: string; … }` | optional | Roll-up summary definition. The engine recomputes the value when child records are inserted/updated/deleted. | +| **language** | `string` | optional | Programming language for syntax highlighting (e.g., javascript, python, sql) | +| **step** | `number` | optional | Step increment for slider (default: 1) | +| **currencyConfig** | `{ precision?: integer; currencyMode?: Enum<'dynamic' \| 'fixed'>; defaultCurrency?: string }` | optional | Configuration for currency field type | +| **dimensions** | `integer` | optional | Vector dimensionality (e.g., 1536 for OpenAI embeddings) | +| **trackHistory** | `boolean` | optional | Render this field's value changes as human-readable entries on the record activity timeline (ADR-0052 §5b). Opt-in per field. | +| **group** | `string` | optional | Field group name for organizing fields in forms and layouts (e.g., "contact_info", "billing", "system") | +| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is shown only when TRUE (else hidden). e.g. P`record.type == 'invoice'` | +| **readonlyWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is read-only when TRUE. e.g. P`record.status == 'paid'` | +| **requiredWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is required when TRUE. The only slot; the `conditionalRequired` alias was removed in protocol 17 (#3855). | +| **conditionalRequired** | `never` | optional | [REMOVED] `conditionalRequired` was removed in @objectstack/spec 17 (#3855) — use `requiredWhen`. Rename the key; the value (a CEL predicate) is unchanged. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **widget** | `string` | optional | Form widget override — names a registered field component (resolved as `field:`) to render this field instead of the `type` default. Degrades to the `type` renderer when unregistered. e.g. "object-ref", "filter-condition", "recipient-picker". | +| **hidden** | `boolean` | optional (default: `false`) | Hidden from default UI | +| **internal** | `boolean` | optional | [#7728] Never return this field's value on the generic data path — the engine OMITS the key from `find`/`findOne` results, the 201 create body and the by-id update body, on the default projection AND when a client names the field in `?select=`. Storage, filtering and indexing are untouched, so a server-side verifier can still match on the column and a purpose-built mint route can still return the value once at creation. The read protection for ADR-0100's third credential channel (auth-subsystem one-way hashes on `text` columns). Omission, not masking: a mask signals 'a value is set', which carries no information on a `required` column. | +| **readonly** | `boolean` | optional (default: `false`) | Read-only — never editable in forms, AND server-enforced on BOTH write paths: a non-system write to this field is silently dropped from the payload on UPDATE (#2948/#3003) and on INSERT (#3043; a create can no longer directly seed e.g. `approval_status: "approved"`), symmetric with `readonlyWhen`. A stripped INSERT field still falls back to its `defaultValue`. Exempt from the strip on BOTH paths: `isSystem` writes (seed replay, migration). Exempt on the UPDATE path ONLY: an opt-in "historical" import (`preserveAudit`, #3493) — which admits a whitelist (the audit/timestamp family plus author-declared business `readonly` fields). On INSERT the exemption does NOT apply (#6640): a non-system create that requests `preserveAudit` still has its readonly fields stripped, and is warned loudly that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. A normal (non-system) import is NOT system-context and still strips. | +| **requiredPermissions** | `string[]` | optional | [ADR-0066 D3] Capabilities required to read/edit this field (mask on read, deny on write; AND-gate). | +| **maskingRule** | `Enum<'phone' \| 'id_card' \| 'bank_account' \| 'email' \| 'name'> \| { keepHead: integer; keepTail: integer }` | optional | [#8993] Partial masking rule enforced by the runtime FieldMasker (single channel — API, UI, export and AI context all see the same masked value). A named preset ('phone' 138****5678, 'id_card' keep 6+4, 'bank_account' keep last 4, 'email' j***@example.com, 'name' keep first char) or `{ keepHead, keepTail }`. Masked for every non-system caller unless the field's `requiredPermissions` are ALL held (that evaluation is the unmask gate); a permission set marking the field non-readable still deletes it entirely. Deterministic, length-preserving output; masked callers cannot filter/sort/group/aggregate on the field. | +| **ackPlaintextMasking** | `boolean` | optional | [ADR-0100] Affirm a generic `password` field's plaintext-at-rest / masked-on-read contract is intended, silencing the author-time warning (#3420). No effect on non-password fields. | +| **system** | `boolean` | optional | Auto-injected system/audit field (e.g. created_at, updated_by, organization_id). Tools that surface system fields separately from author-declared business fields should branch on this flag. | +| **sortable** | `boolean` | optional (default: `true`) | Whether field is sortable in list views | +| **inlineHelpText** | `string` | optional | Help text displayed below the field in forms | +| **placeholder** | `string` | optional | Placeholder text rendered inside the empty input (the HTML placeholder attribute); disappears once a value is entered. Distinct from `inlineHelpText` (always-visible help rendered beside/under the input) and `description` (tooltip/developer documentation). | +| **autonumberFormat** | `string` | optional (default: `"{0000}"`) | Auto-number format: literal text + `{0000}` counter, `{YYYY}`/`{MM}`/`{DD}`/`{YYYYMMDD}` date tokens (business tz), and `{field_name}` interpolation. Counter resets per rendered prefix (e.g. AD`{YYYYMMDD}``{0000}` resets daily). Omitted on an `autonumber` field ⇒ the contract default `{0000}` (#6555). | +| **externalId** | `boolean` | optional (default: `false`) | Is external ID for upsert operations | +| **_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. | + +### Nested Shape: `ObjectExtension.indexes[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional | Index name (auto-generated if not provided) | +| **fields** | `string[]` | ✅ | Fields included in the index | +| **unique** | `boolean \| 'global' \| 'organization'` | optional (default: `false`) | Whether the index enforces uniqueness, and at which scope (ADR-0120). 'global' = materialized over exactly `fields`, no organization column injected — one holder across the whole installation; 'organization' = the driver prepends the NULL-safe organization key part (COALESCE(organization_id, '__global__')) at registration — one holder per organization; bare true = deprecated positional spelling of 'global' (warned in 17.x by lint unique/unscoped-declared-index, rejected at protocol 18, #5082) — state the scope. 'tenant'/'org' are rejected — the word is 'organization' | +| **type** | `never` | optional | [REMOVED] `indexes[].type` was removed in @objectstack/spec 17.0.0 (#5248, ADR-0049) — no driver ever read it. `SqlDriver.syncDeclaredIndexes` creates every declared index through knex's `table.index()` / `table.unique()`, which cannot express an access method, so the value changed no DDL; its `.default('btree')` merely made an inert knob show up in every parse output. Delete the key. The index method is the driver/dialect's decision (Postgres defaults to B-tree; `gin`/`gist`/`fulltext` are dialect-specific and are chosen by a database-layer migration when a workload actually needs one). Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **partial** | `never` | optional | [REMOVED] `indexes[].partial` was removed in @objectstack/spec 17.0.0 (#5248, #4943, ADR-0049) — no driver ever emitted the `WHERE` clause, so a declared partial index was materialized as a FULL index and the predicate silently did nothing. Delete the key. Partial indexes are built at the database layer, not the declaration surface: issue `CREATE [UNIQUE] INDEX … WHERE ` from a runtime migration (this is what `metadata-protocol`'s `ensureOverlayIndex` already does for `sys_metadata`). Drift detection is unaffected — it reads partiality back from the database's own DDL, never from this key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | + --- diff --git a/content/docs/references/data/query.mdx b/content/docs/references/data/query.mdx index f954194b91..9005249418 100644 --- a/content/docs/references/data/query.mdx +++ b/content/docs/references/data/query.mdx @@ -143,6 +143,37 @@ Type: `string` | **distinct** | `never` | optional | [REMOVED] `query.distinct` was removed in @objectstack/spec 17 (#4286, ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded `total`/`hasMore` to a page-local estimate while still returning duplicate rows. Delete the key; `QueryBuilder.distinct()` was removed with it, and the count suppression is gone (`total` is truthful again). For unique values of one column use the SQL/memory drivers' `distinct(object, field)` door; for unique combinations, `groupBy`; for a deduplicated count, the `count_distinct` aggregation. | | **expand** | `Record` | optional | Recursive relation loading map. Keys are lookup/master_detail field names; values are nested QueryAST objects that control select (`fields`) and filter (`where`, AND-merged with the batch $in), plus further expansion on the related object. The engine resolves expand via batch $in queries (driver-agnostic) with a default max depth of 3; per-parent `limit`/`offset`/`orderBy` are NOT applied on this path. | +### Nested Shape: `Query.search` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **query** | `string` | ✅ | Search query text | +| **fields** | `string[]` | optional | Fields to search in (if not specified, searches all text fields) | +| **fuzzy** | `boolean` | optional (default: `false`) | [EXPERIMENTAL — not enforced] Fuzzy matching (tolerate typos). The ADR-0061 expansion reads only `query` + `fields`; no executor receives this flag (#4286). | +| **operator** | `Enum<'and' \| 'or'>` | optional (default: `"or"`) | [EXPERIMENTAL — not enforced] Logical operator between terms. The ADR-0061 expansion applies its own term semantics; no executor receives this flag (#4286). | +| **boost** | `Record` | optional | [EXPERIMENTAL — not enforced] Field-specific relevance boosting (field name -> boost factor). No executor scores results (#4286). | +| **minScore** | `number` | optional | [EXPERIMENTAL — not enforced] Minimum relevance score threshold. No executor scores results (#4286). | +| **language** | `string` | optional | [EXPERIMENTAL — not enforced] Language for text analysis (e.g., "en", "zh", "es"). No executor selects an analyzer (#4286). | +| **highlight** | `boolean` | optional (default: `false`) | [EXPERIMENTAL — not enforced] Search result highlighting. No executor emits highlights (#4286). | + +### Nested Shape: `Query.aggregations[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **function** | `Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>` | ✅ | Aggregation function | +| **field** | `string` | optional | Field to aggregate (optional for COUNT(*)) | +| **alias** | `string` | ✅ | Result column alias | +| **distinct** | `never` | optional | [REMOVED] `query.aggregations[].distinct` was removed in @objectstack/spec 17 (#6815, ADR-0049) — exactly ONE of the six faces that read an aggregation honoured it. The objectql in-memory fallback deduplicated the values before applying the function, while `driver-sql`, `driver-turso`, `driver-mongodb`, `driver-memory` and the service-analytics SQL builder all ignored it — so `{ function: 'sum', field: 'amount', distinct: true }` answered a DEDUPLICATED sum when the engine fell back in memory and an ordinary sum on every SQL datasource: one query, two numbers, chosen by which backend happened to serve it. Both answers are plausible, so nothing surfaced the divergence. Delete the key. For a deduplicated COUNT the live spelling is the `count_distinct` aggregation function, which every SQL face compiles to `COUNT(DISTINCT field)` (#6409) and the in-memory fallback computes identically. `SUM(DISTINCT …)` / `AVG(DISTINCT …)` get no replacement: no backend ever computed them here, and a per-row measure that needs deduplicating is a modelling problem to fix in the data, not a flag on the read. | +| **filter** | `any` | optional | Per-aggregation filter (SQL FILTER (WHERE …) semantics): narrows the source rows THIS aggregation reads, leaving sibling aggregations unfiltered. Enforced by engine.aggregate (#10576): lowered in memory for drivers without native conditional aggregation; a driver reached directly refuses rather than silently dropping it. | + +### Nested Shape: `Query.groupBy[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field to group by | +| **dateGranularity** | `Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>` | optional | Bucket date values into uniform periods (day/week/month/quarter/year) | +| **alias** | `string` | optional | Alias for the projected group value | + --- diff --git a/content/docs/references/data/seed-loader.mdx b/content/docs/references/data/seed-loader.mdx index 05114189f3..490d0ca592 100644 --- a/content/docs/references/data/seed-loader.mdx +++ b/content/docs/references/data/seed-loader.mdx @@ -53,6 +53,16 @@ Complete object dependency graph for seed data loading | **insertOrder** | `string[]` | ✅ | Topologically sorted insert order | | **circularDependencies** | `string[][]` | optional (default: `[]`) | Circular dependency chains (e.g., [["a", "b", "a"]]) | +### Nested Shape: `ObjectDependencyGraph.nodes[number]` + +Object node in the seed data dependency graph + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **object** | `string` | ✅ | Object name (snake_case) | +| **dependsOn** | `string[]` | ✅ | Objects this object depends on | +| **references** | `{ field: string; targetObject: string; targetField: string; fieldType: Enum<'lookup' \| 'master_detail' \| 'user'>; … }[]` | ✅ | Field-level reference details | + --- @@ -68,6 +78,18 @@ Object node in the seed data dependency graph | **dependsOn** | `string[]` | ✅ | Objects this object depends on | | **references** | `{ field: string; targetObject: string; targetField: string; fieldType: Enum<'lookup' \| 'master_detail' \| 'user'>; … }[]` | ✅ | Field-level reference details | +### Nested Shape: `ObjectDependencyNode.references[number]` + +Describes how a field reference is resolved during seed loading + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Source field name containing the reference value | +| **targetObject** | `string` | ✅ | Target object name (snake_case) | +| **targetField** | `string` | optional (default: `"name"`) | Field on target object used for matching | +| **fieldType** | `Enum<'lookup' \| 'master_detail' \| 'user'>` | ✅ | Relationship field type | +| **multiple** | `boolean` | optional | Field stores an array of references (multiple: true) | + --- @@ -142,6 +164,20 @@ Result of loading a single dataset | **summariesStale** | `integer` | optional (default: `0`) | Roll-up summary values left stale by writes for this dataset | | **errors** | `{ sourceObject: string; field: string; targetObject: string; targetField: string; … }[]` | optional (default: `[]`) | Reference resolution errors | +### Nested Shape: `SeedLoadResult.errors[number]` + +Actionable error for a failed reference resolution + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **sourceObject** | `string` | ✅ | Object with the broken reference | +| **field** | `string` | ✅ | Field name with unresolved reference | +| **targetObject** | `string` | ✅ | Target object searched for the reference | +| **targetField** | `string` | ✅ | ExternalId field used for matching | +| **attemptedValue** | `any` | ✅ | Value that failed to resolve | +| **recordIndex** | `integer` | ✅ | Index of the record in the dataset | +| **message** | `string` | ✅ | Human-readable error description | + --- @@ -163,6 +199,13 @@ Seed data loader configuration | **organizationId** | `string` | optional | Target organization id for per-tenant seed replay | | **identity** | `{ user?: object; org?: object }` | optional | Identity bound to os.user / os.org when resolving CEL seed values | +### Nested Shape: `SeedLoaderConfig.identity` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **user** | `{ id: string; role?: string; email?: string }` | optional | Subject bound to os.user in seed CEL expressions | +| **org** | `{ id: string; tier?: string }` | optional | Organization bound to os.org in seed CEL expressions | + --- @@ -177,6 +220,37 @@ Seed loader request with datasets and configuration | **seeds** | `{ object: string; externalId: string \| string[]; mode: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | ✅ | Seeds to load | | **config** | `{ dryRun: boolean; haltOnError: boolean; multiPass: boolean; defaultMode: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; … }` | optional (has default) | Loader configuration | +### Nested Shape: `SeedLoaderRequest.seeds[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **object** | `string` | ✅ | Target Object Name | +| **externalId** | `string \| string[]` | optional (default: `"name"`) | Field (or composite list of fields) matched for the uniqueness check | +| **mode** | `Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>` | optional (default: `"upsert"`) | Conflict resolution strategy | +| **env** | `Enum<'prod' \| 'dev' \| 'test'>[]` | optional (default: `["prod","dev","test"]`) | Applicable environments | +| **records** | `Record[]` | ✅ | Data records | +| **_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. | + +### Nested Shape: `SeedLoaderRequest.config` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **dryRun** | `boolean` | optional (default: `false`) | Validate references without writing data | +| **haltOnError** | `boolean` | optional (default: `false`) | Stop on first reference resolution error | +| **multiPass** | `boolean` | optional (default: `true`) | Enable multi-pass loading for circular dependencies | +| **defaultMode** | `Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>` | optional (default: `"upsert"`) | Default conflict resolution strategy | +| **batchSize** | `integer` | optional (default: `1000`) | Maximum records per batch insert/upsert | +| **transaction** | `boolean` | optional (default: `false`) | Wrap entire load in a transaction (all-or-nothing) | +| **env** | `Enum<'prod' \| 'dev' \| 'test'>` | optional | Only load datasets matching this environment | +| **organizationId** | `string` | optional | Target organization id for per-tenant seed replay | +| **identity** | `{ user?: object; org?: object }` | optional | Identity bound to os.user / os.org when resolving CEL seed values | + --- @@ -195,6 +269,64 @@ Complete seed loader result | **errors** | `{ sourceObject: string; field: string; targetObject: string; targetField: string; … }[]` | ✅ | All reference resolution errors | | **summary** | `{ objectsProcessed: integer; totalRecords: integer; totalInserted: integer; totalUpdated: integer; … }` | ✅ | Summary statistics | +### Nested Shape: `SeedLoaderResult.dependencyGraph` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **nodes** | `{ object: string; dependsOn: string[]; references: object[] }[]` | ✅ | All objects in the dependency graph | +| **insertOrder** | `string[]` | ✅ | Topologically sorted insert order | +| **circularDependencies** | `string[][]` | optional (default: `[]`) | Circular dependency chains (e.g., [["a", "b", "a"]]) | + +### Nested Shape: `SeedLoaderResult.results[number]` + +Result of loading a single dataset + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **object** | `string` | ✅ | Object that was loaded | +| **mode** | `Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>` | ✅ | Import mode used | +| **inserted** | `integer` | ✅ | Records inserted | +| **updated** | `integer` | ✅ | Records updated | +| **skipped** | `integer` | ✅ | Records skipped | +| **errored** | `integer` | ✅ | Records with errors | +| **total** | `integer` | ✅ | Total records in dataset | +| **referencesResolved** | `integer` | ✅ | References resolved via externalId | +| **referencesDeferred** | `integer` | ✅ | References deferred to second pass | +| **referencesDropped** | `integer` | optional (default: `0`) | Reference fields dropped from records that were still written | +| **summariesStale** | `integer` | optional (default: `0`) | Roll-up summary values left stale by writes for this dataset | +| **errors** | `{ sourceObject: string; field: string; targetObject: string; targetField: string; … }[]` | optional (default: `[]`) | Reference resolution errors | + +### Nested Shape: `SeedLoaderResult.errors[number]` + +Actionable error for a failed reference resolution + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **sourceObject** | `string` | ✅ | Object with the broken reference | +| **field** | `string` | ✅ | Field name with unresolved reference | +| **targetObject** | `string` | ✅ | Target object searched for the reference | +| **targetField** | `string` | ✅ | ExternalId field used for matching | +| **attemptedValue** | `any` | ✅ | Value that failed to resolve | +| **recordIndex** | `integer` | ✅ | Index of the record in the dataset | +| **message** | `string` | ✅ | Human-readable error description | + +### Nested Shape: `SeedLoaderResult.summary` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **objectsProcessed** | `integer` | ✅ | Total objects processed | +| **totalRecords** | `integer` | ✅ | Total records across all objects | +| **totalInserted** | `integer` | ✅ | Total records inserted | +| **totalUpdated** | `integer` | ✅ | Total records updated | +| **totalSkipped** | `integer` | ✅ | Total records skipped | +| **totalErrored** | `integer` | ✅ | Total records with errors | +| **totalReferencesResolved** | `integer` | ✅ | Total references resolved | +| **totalReferencesDeferred** | `integer` | ✅ | Total references deferred | +| **totalReferencesDropped** | `integer` | optional (default: `0`) | Total reference fields dropped from written records | +| **totalSummariesStale** | `integer` | optional (default: `0`) | Total roll-up summary values left stale across the load | +| **circularDependencyCount** | `integer` | ✅ | Circular dependency chains detected | +| **durationMs** | `number` | ✅ | Load duration in milliseconds | + --- diff --git a/content/docs/references/identity/position.mdx b/content/docs/references/identity/position.mdx index 16ed1c35f6..7641e31840 100644 --- a/content/docs/references/identity/position.mdx +++ b/content/docs/references/identity/position.mdx @@ -74,6 +74,14 @@ const result = PositionSchema.parse(data); | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `Position.protection` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | ✅ | Lock policy — none \| no-overlay \| no-delete \| full. | +| **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | +| **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | + --- diff --git a/content/docs/references/identity/scim.mdx b/content/docs/references/identity/scim.mdx index bfe6a7c7d9..4a10b5f2b6 100644 --- a/content/docs/references/identity/scim.mdx +++ b/content/docs/references/identity/scim.mdx @@ -114,6 +114,16 @@ const result = SCIMAddressSchema.parse(data); | **operations** | `{ method: Enum<'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'>; path: string; bulkId?: string; data?: Record; … }[]` | ✅ | Bulk operations to execute (minimum 1) | | **failOnErrors** | `integer` | optional | Stop processing after this many errors | +### Nested Shape: `SCIMBulkRequest.operations[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **method** | `Enum<'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'>` | ✅ | HTTP method for the bulk operation | +| **path** | `string` | ✅ | Resource endpoint path (e.g. /Users, /Groups/`{id}`) | +| **bulkId** | `string` | optional | Client-assigned ID for cross-referencing between operations | +| **data** | `Record` | optional | Request body for POST/PUT/PATCH operations | +| **version** | `string` | optional | ETag for optimistic concurrency control | + --- @@ -126,6 +136,16 @@ const result = SCIMAddressSchema.parse(data); | **schemas** | `'urn:ietf:params:scim:api:messages:2.0:BulkResponse'[]` | optional (default: `["urn:ietf:params:scim:api:messages:2.0:BulkResponse"]`) | SCIM schema URIs (BulkResponse) | | **operations** | `{ method: Enum<'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'>; bulkId?: string; location?: string; status: string; … }[]` | ✅ | Results for each bulk operation | +### Nested Shape: `SCIMBulkResponse.operations[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **method** | `Enum<'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'>` | ✅ | HTTP method that was executed | +| **bulkId** | `string` | optional | Client-assigned bulk operation ID | +| **location** | `string` | optional | URL of the created or modified resource | +| **status** | `string` | ✅ | HTTP status code as string (e.g. "201", "400") | +| **response** | `any` | optional | Response body (typically present for errors) | + --- @@ -171,6 +191,14 @@ const result = SCIMAddressSchema.parse(data); | **department** | `string` | optional | Department | | **manager** | `{ value: string; $ref?: string; displayName?: string }` | optional | Manager reference | +### Nested Shape: `SCIMEnterpriseUser.manager` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **value** | `string` | ✅ | Manager ID | +| **$ref** | `string` | optional | Manager URI | +| **displayName** | `string` | optional | Manager name | + --- @@ -201,6 +229,25 @@ const result = SCIMAddressSchema.parse(data); | **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 | +### Nested Shape: `SCIMGroup.members[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **value** | `string` | ✅ | Member ID | +| **$ref** | `string` | optional | URI reference to the member | +| **type** | `Enum<'User' \| 'Group'>` | optional | Member type | +| **display** | `string` | optional | Member display name | + +### Nested Shape: `SCIMGroup.meta` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **resourceType** | `string` | optional | Resource type | +| **created** | `string` | optional | Creation timestamp | +| **lastModified** | `string` | optional | Last modification timestamp | +| **location** | `string` | optional | Resource location URI | +| **version** | `string` | optional | Entity tag (ETag) for concurrency control | + --- @@ -300,6 +347,14 @@ const result = SCIMAddressSchema.parse(data); | **schemas** | `string[]` | optional (default: `["urn:ietf:params:scim:api:messages:2.0:PatchOp"]`) | SCIM schema URIs | | **Operations** | `{ op: Enum<'add' \| 'remove' \| 'replace'>; path?: string; value?: any }[]` | ✅ | Patch operations | +### Nested Shape: `SCIMPatchRequest.Operations[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **op** | `Enum<'add' \| 'remove' \| 'replace'>` | ✅ | Operation type | +| **path** | `string` | optional | Attribute path (optional for add) | +| **value** | `any` | optional | Value to set | + --- @@ -350,6 +405,78 @@ const result = SCIMAddressSchema.parse(data); | **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: `SCIMUser.name` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **formatted** | `string` | optional | Formatted full name | +| **familyName** | `string` | optional | Family name (last name) | +| **givenName** | `string` | optional | Given name (first name) | +| **middleName** | `string` | optional | Middle name | +| **honorificPrefix** | `string` | optional | Honorific prefix (Mr., Ms., Dr.) | +| **honorificSuffix** | `string` | optional | Honorific suffix (Jr., Sr.) | + +### Nested Shape: `SCIMUser.emails[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **value** | `string` | ✅ | Email address | +| **type** | `Enum<'work' \| 'home' \| 'other'>` | optional | Email type | +| **display** | `string` | optional | Display label | +| **primary** | `boolean` | optional (default: `false`) | Primary email indicator | + +### Nested Shape: `SCIMUser.phoneNumbers[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **value** | `string` | ✅ | Phone number | +| **type** | `Enum<'work' \| 'home' \| 'mobile' \| 'fax' \| 'pager' \| 'other'>` | optional | Phone number type | +| **display** | `string` | optional | Display label | +| **primary** | `boolean` | optional (default: `false`) | Primary phone indicator | + +### Nested Shape: `SCIMUser.addresses[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **formatted** | `string` | optional | Formatted address | +| **streetAddress** | `string` | optional | Street address | +| **locality** | `string` | optional | City/Locality | +| **region** | `string` | optional | State/Region | +| **postalCode** | `string` | optional | Postal code | +| **country** | `string` | optional | Country | +| **type** | `Enum<'work' \| 'home' \| 'other'>` | optional | Address type | +| **primary** | `boolean` | optional (default: `false`) | Primary address indicator | + +### Nested Shape: `SCIMUser.groups[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **value** | `string` | ✅ | Group ID | +| **$ref** | `string` | optional | URI reference to the group | +| **display** | `string` | optional | Group display name | +| **type** | `Enum<'direct' \| 'indirect'>` | optional | Membership type | + +### Nested Shape: `SCIMUser.meta` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **resourceType** | `string` | optional | Resource type | +| **created** | `string` | optional | Creation timestamp | +| **lastModified** | `string` | optional | Last modification timestamp | +| **location** | `string` | optional | Resource location URI | +| **version** | `string` | optional | Entity tag (ETag) for concurrency control | + +### Nested Shape: `SCIMUser.urn:ietf:params:scim:schemas:extension:enterprise:2.0:User` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **employeeNumber** | `string` | optional | Employee number | +| **costCenter** | `string` | optional | Cost center | +| **organization** | `string` | optional | Organization | +| **division** | `string` | optional | Division | +| **department** | `string` | optional | Department | +| **manager** | `{ value: string; $ref?: string; displayName?: string }` | optional | Manager reference | + --- diff --git a/content/docs/references/integration/connector.mdx b/content/docs/references/integration/connector.mdx index 19cb0d621d..b396992797 100644 --- a/content/docs/references/integration/connector.mdx +++ b/content/docs/references/integration/connector.mdx @@ -191,6 +191,108 @@ Circuit breaker configuration | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `Connector.actions[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **key** | `string` | ✅ | Action key (machine name) | +| **label** | `string` | ✅ | Human readable label | +| **description** | `string` | optional | | +| **inputSchema** | `Record` | optional | Input parameters schema (JSON Schema) | +| **outputSchema** | `Record` | optional | Output schema (JSON Schema) | +| **effect** | `Enum<'read' \| 'write'>` | optional | What the action does upstream: 'read' never mutates (reports acted:0); 'write' does (a successful dispatch reports acted:1). Omit when the effect is not knowable — the step is then reported as unmeasured, not as zero | + +### Nested Shape: `Connector.triggers[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **key** | `string` | ✅ | Trigger key | +| **label** | `string` | ✅ | Trigger label | +| **description** | `string` | optional | | +| **type** | `Enum<'polling' \| 'webhook'>` | ✅ | Trigger type | +| **interval** | `number` | optional | Polling interval in seconds | + +### Nested Shape: `Connector.syncConfig` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **strategy** | `Enum<'full' \| 'incremental' \| 'upsert' \| 'append_only'>` | optional (default: `"incremental"`) | Synchronization strategy | +| **direction** | `Enum<'import' \| 'export' \| 'bidirectional'>` | optional (default: `"import"`) | Sync direction | +| **schedule** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Cron expression for scheduled sync — cron`0 */15 * * *` | +| **realtimeSync** | `boolean` | optional (default: `false`) | Enable real-time sync | +| **timestampField** | `string` | optional | Field to track last modification time | +| **conflictResolution** | `Enum<'source_wins' \| 'target_wins' \| 'latest_wins' \| 'manual'>` | optional (default: `"latest_wins"`) | Conflict resolution strategy | +| **batchSize** | `number` | optional (default: `1000`) | Records per batch | +| **deleteMode** | `Enum<'hard_delete' \| 'soft_delete' \| 'ignore'>` | optional (default: `"soft_delete"`) | Delete handling mode | +| **filters** | `Record` | optional | Filter criteria for selective sync | + +### Nested Shape: `Connector.fieldMappings[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **source** | `string` | ✅ | Source field name | +| **target** | `string` | ✅ | Target field name | +| **transform** | `never` | optional | [REMOVED] `FieldMapping.transform` — authored as `connector.fieldMappings[].transform` and `externalLookup.fieldMappings[].transform` — was removed in @objectstack/spec 17.0.0 (#5552, ADR-0049), and the whole `FieldMappingTransform` union went with it (`constant` / `cast` / `lookup` / `javascript` / `map`) — no runtime ever executed any of the five, and the `javascript` member advertised `dialect: "js"`, a dialect retired in #3278. Delete the key. The transform pipeline that IS enforced is the import mapping's: `mapping.fieldMapping[].transform` (a string enum — `none`/`constant`/`map`/`split`/`join`/`lookup` — with its settings in `params`), applied by the REST import path, which rejects `javascript` with a 400 rather than pretending to run it. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **defaultValue** | `any` | optional | Default if source is null/undefined | +| **dataType** | `Enum<'string' \| 'number' \| 'boolean' \| 'date' \| 'datetime' \| 'json' \| 'array'>` | optional | Target data type | +| **required** | `boolean` | optional (default: `false`) | Field is required | +| **syncMode** | `Enum<'read_only' \| 'write_only' \| 'bidirectional'>` | optional (default: `"bidirectional"`) | Sync mode | + +### Nested Shape: `Connector.webhooks[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Webhook name, unique per organization (lowercase snake_case) | +| **label** | `string` | optional | Human-readable webhook label | +| **object** | `string` | optional | Object whose record events (create/update/delete, bulk_update/bulk_delete) trigger this webhook | +| **triggers** | `Enum<'create' \| 'update' \| 'delete' \| 'bulk_update' \| 'bulk_delete'>[]` | optional | Events that trigger execution | +| **url** | `string` | ✅ | External webhook endpoint URL | +| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'>` | optional (default: `"POST"`) | HTTP method | +| **headers** | `Record` | optional | Custom HTTP headers | +| **timeoutMs** | `integer` | optional (default: `30000`) | Request timeout in milliseconds | +| **secret** | `string` | optional | Signing secret for HMAC signature verification | +| **isActive** | `boolean` | optional (default: `true`) | Whether webhook is active | +| **description** | `string` | optional | Webhook description | +| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this webhook. | +| **_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. | +| **events** | `Enum<'record.created' \| 'record.updated' \| 'record.deleted' \| 'sync.started' \| …>[]` | optional | Connector events to subscribe to (not yet enforced — no runtime dispatches these; see #3197) | +| **signatureAlgorithm** | `Enum<'hmac_sha256' \| 'hmac_sha512' \| 'none'>` | optional (default: `"hmac_sha256"`) | Webhook signature algorithm | + +### Nested Shape: `Connector.retryConfig` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **strategy** | `Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>` | optional (default: `"exponential_backoff"`) | Retry strategy | +| **maxAttempts** | `number` | optional (default: `3`) | Maximum retry attempts | +| **initialDelayMs** | `number` | optional (default: `1000`) | Initial retry delay in ms | +| **maxDelayMs** | `number` | optional (default: `60000`) | Maximum retry delay in ms | +| **backoffMultiplier** | `number` | optional (default: `2`) | Exponential backoff multiplier | +| **retryableStatusCodes** | `number[]` | optional (default: `[408,429,500,502,503,504]`) | HTTP status codes to retry | +| **retryOnNetworkError** | `boolean` | optional (default: `true`) | Retry on network errors | +| **jitter** | `boolean` | optional (default: `true`) | Add jitter to retry delays | + +### Nested Shape: `Connector.errorMapping` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **rules** | `{ sourceCode: string \| number; sourceMessage?: string; targetCode: string; targetCategory: Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| …>; … }[]` | ✅ | Error mapping rules | +| **defaultCategory** | `Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| …>` | optional (default: `"integration_error"`) | Default category for unmapped errors | +| **unmappedBehavior** | `Enum<'passthrough' \| 'generic_error' \| 'throw'>` | ✅ | What to do with unmapped errors | +| **logUnmapped** | `boolean` | optional (default: `true`) | Log unmapped errors | + +### Nested Shape: `Connector.health` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **healthCheck** | `{ enabled: boolean; intervalMs?: number; timeoutMs?: number; endpoint?: string; … }` | optional | Health check configuration | +| **circuitBreaker** | `{ enabled: boolean; failureThreshold?: number; resetTimeoutMs?: number; halfOpenMaxRequests?: number; … }` | optional | Circuit breaker configuration | + --- @@ -282,6 +384,30 @@ Connector health configuration | **healthCheck** | `{ enabled: boolean; intervalMs: number; timeoutMs: number; endpoint?: string; … }` | optional | Health check configuration | | **circuitBreaker** | `{ enabled: boolean; failureThreshold: number; resetTimeoutMs: number; halfOpenMaxRequests: number; … }` | optional | Circuit breaker configuration | +### Nested Shape: `ConnectorHealth.healthCheck` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | ✅ | Enable health checks | +| **intervalMs** | `number` | optional (default: `60000`) | Health check interval in milliseconds | +| **timeoutMs** | `number` | optional (default: `5000`) | Health check timeout in milliseconds | +| **endpoint** | `string` | optional | Health check endpoint path | +| **method** | `Enum<'GET' \| 'HEAD' \| 'OPTIONS'>` | optional | HTTP method for health check | +| **expectedStatus** | `number` | optional (default: `200`) | Expected HTTP status code | +| **unhealthyThreshold** | `number` | optional (default: `3`) | Consecutive failures before marking unhealthy | +| **healthyThreshold** | `number` | optional (default: `1`) | Consecutive successes before marking healthy | + +### Nested Shape: `ConnectorHealth.circuitBreaker` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | ✅ | Enable circuit breaker | +| **failureThreshold** | `number` | optional (default: `5`) | Failures before opening circuit | +| **resetTimeoutMs** | `number` | optional (default: `30000`) | Time in open state before half-open | +| **halfOpenMaxRequests** | `number` | optional (default: `1`) | Requests allowed in half-open state | +| **monitoringWindow** | `number` | optional (default: `60000`) | Rolling window for failure count in ms | +| **fallbackStrategy** | `Enum<'cache' \| 'default_value' \| 'error' \| 'queue'>` | optional | Fallback strategy when circuit is open | + --- @@ -513,6 +639,108 @@ Connector type | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `DeclarativeConnectorEntry.actions[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **key** | `string` | ✅ | Action key (machine name) | +| **label** | `string` | ✅ | Human readable label | +| **description** | `string` | optional | | +| **inputSchema** | `Record` | optional | Input parameters schema (JSON Schema) | +| **outputSchema** | `Record` | optional | Output schema (JSON Schema) | +| **effect** | `Enum<'read' \| 'write'>` | optional | What the action does upstream: 'read' never mutates (reports acted:0); 'write' does (a successful dispatch reports acted:1). Omit when the effect is not knowable — the step is then reported as unmeasured, not as zero | + +### Nested Shape: `DeclarativeConnectorEntry.triggers[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **key** | `string` | ✅ | Trigger key | +| **label** | `string` | ✅ | Trigger label | +| **description** | `string` | optional | | +| **type** | `Enum<'polling' \| 'webhook'>` | ✅ | Trigger type | +| **interval** | `number` | optional | Polling interval in seconds | + +### Nested Shape: `DeclarativeConnectorEntry.syncConfig` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **strategy** | `Enum<'full' \| 'incremental' \| 'upsert' \| 'append_only'>` | optional (default: `"incremental"`) | Synchronization strategy | +| **direction** | `Enum<'import' \| 'export' \| 'bidirectional'>` | optional (default: `"import"`) | Sync direction | +| **schedule** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Cron expression for scheduled sync — cron`0 */15 * * *` | +| **realtimeSync** | `boolean` | optional (default: `false`) | Enable real-time sync | +| **timestampField** | `string` | optional | Field to track last modification time | +| **conflictResolution** | `Enum<'source_wins' \| 'target_wins' \| 'latest_wins' \| 'manual'>` | optional (default: `"latest_wins"`) | Conflict resolution strategy | +| **batchSize** | `number` | optional (default: `1000`) | Records per batch | +| **deleteMode** | `Enum<'hard_delete' \| 'soft_delete' \| 'ignore'>` | optional (default: `"soft_delete"`) | Delete handling mode | +| **filters** | `Record` | optional | Filter criteria for selective sync | + +### Nested Shape: `DeclarativeConnectorEntry.fieldMappings[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **source** | `string` | ✅ | Source field name | +| **target** | `string` | ✅ | Target field name | +| **transform** | `never` | optional | [REMOVED] `FieldMapping.transform` — authored as `connector.fieldMappings[].transform` and `externalLookup.fieldMappings[].transform` — was removed in @objectstack/spec 17.0.0 (#5552, ADR-0049), and the whole `FieldMappingTransform` union went with it (`constant` / `cast` / `lookup` / `javascript` / `map`) — no runtime ever executed any of the five, and the `javascript` member advertised `dialect: "js"`, a dialect retired in #3278. Delete the key. The transform pipeline that IS enforced is the import mapping's: `mapping.fieldMapping[].transform` (a string enum — `none`/`constant`/`map`/`split`/`join`/`lookup` — with its settings in `params`), applied by the REST import path, which rejects `javascript` with a 400 rather than pretending to run it. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **defaultValue** | `any` | optional | Default if source is null/undefined | +| **dataType** | `Enum<'string' \| 'number' \| 'boolean' \| 'date' \| 'datetime' \| 'json' \| 'array'>` | optional | Target data type | +| **required** | `boolean` | optional (default: `false`) | Field is required | +| **syncMode** | `Enum<'read_only' \| 'write_only' \| 'bidirectional'>` | optional (default: `"bidirectional"`) | Sync mode | + +### Nested Shape: `DeclarativeConnectorEntry.webhooks[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Webhook name, unique per organization (lowercase snake_case) | +| **label** | `string` | optional | Human-readable webhook label | +| **object** | `string` | optional | Object whose record events (create/update/delete, bulk_update/bulk_delete) trigger this webhook | +| **triggers** | `Enum<'create' \| 'update' \| 'delete' \| 'bulk_update' \| 'bulk_delete'>[]` | optional | Events that trigger execution | +| **url** | `string` | ✅ | External webhook endpoint URL | +| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'>` | optional (default: `"POST"`) | HTTP method | +| **headers** | `Record` | optional | Custom HTTP headers | +| **timeoutMs** | `integer` | optional (default: `30000`) | Request timeout in milliseconds | +| **secret** | `string` | optional | Signing secret for HMAC signature verification | +| **isActive** | `boolean` | optional (default: `true`) | Whether webhook is active | +| **description** | `string` | optional | Webhook description | +| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this webhook. | +| **_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. | +| **events** | `Enum<'record.created' \| 'record.updated' \| 'record.deleted' \| 'sync.started' \| …>[]` | optional | Connector events to subscribe to (not yet enforced — no runtime dispatches these; see #3197) | +| **signatureAlgorithm** | `Enum<'hmac_sha256' \| 'hmac_sha512' \| 'none'>` | optional (default: `"hmac_sha256"`) | Webhook signature algorithm | + +### Nested Shape: `DeclarativeConnectorEntry.retryConfig` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **strategy** | `Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>` | optional (default: `"exponential_backoff"`) | Retry strategy | +| **maxAttempts** | `number` | optional (default: `3`) | Maximum retry attempts | +| **initialDelayMs** | `number` | optional (default: `1000`) | Initial retry delay in ms | +| **maxDelayMs** | `number` | optional (default: `60000`) | Maximum retry delay in ms | +| **backoffMultiplier** | `number` | optional (default: `2`) | Exponential backoff multiplier | +| **retryableStatusCodes** | `number[]` | optional (default: `[408,429,500,502,503,504]`) | HTTP status codes to retry | +| **retryOnNetworkError** | `boolean` | optional (default: `true`) | Retry on network errors | +| **jitter** | `boolean` | optional (default: `true`) | Add jitter to retry delays | + +### Nested Shape: `DeclarativeConnectorEntry.errorMapping` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **rules** | `{ sourceCode: string \| number; sourceMessage?: string; targetCode: string; targetCategory: Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| …>; … }[]` | ✅ | Error mapping rules | +| **defaultCategory** | `Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| …>` | optional (default: `"integration_error"`) | Default category for unmapped errors | +| **unmappedBehavior** | `Enum<'passthrough' \| 'generic_error' \| 'throw'>` | ✅ | What to do with unmapped errors | +| **logUnmapped** | `boolean` | optional (default: `true`) | Log unmapped errors | + +### Nested Shape: `DeclarativeConnectorEntry.health` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **healthCheck** | `{ enabled: boolean; intervalMs?: number; timeoutMs?: number; endpoint?: string; … }` | optional | Health check configuration | +| **circuitBreaker** | `{ enabled: boolean; failureThreshold?: number; resetTimeoutMs?: number; halfOpenMaxRequests?: number; … }` | optional | Circuit breaker configuration | + --- @@ -529,6 +757,20 @@ Error mapping configuration | **unmappedBehavior** | `Enum<'passthrough' \| 'generic_error' \| 'throw'>` | ✅ | What to do with unmapped errors | | **logUnmapped** | `boolean` | optional (default: `true`) | Log unmapped errors | +### Nested Shape: `ErrorMappingConfig.rules[number]` + +Error mapping rule + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **sourceCode** | `string \| number` | ✅ | External system error code | +| **sourceMessage** | `string` | optional | Pattern to match against error message | +| **targetCode** | `string` | ✅ | ObjectStack standard error code | +| **targetCategory** | `Enum<'validation' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| …>` | ✅ | Error category | +| **severity** | `Enum<'low' \| 'medium' \| 'high' \| 'critical'>` | ✅ | Error severity level | +| **retryable** | `boolean` | ✅ | Whether the error is retryable | +| **userMessage** | `string` | optional | Human-readable message to show users | + --- @@ -631,6 +873,14 @@ Synchronization strategy | **events** | `Enum<'record.created' \| 'record.updated' \| 'record.deleted' \| 'sync.started' \| 'sync.completed' \| 'sync.failed' \| 'auth.expired' \| 'rate_limit.exceeded'>[]` | optional | Connector events to subscribe to (not yet enforced — no runtime dispatches these; see #3197) | | **signatureAlgorithm** | `Enum<'hmac_sha256' \| 'hmac_sha512' \| 'none'>` | optional (default: `"hmac_sha256"`) | Webhook signature algorithm | +### Nested Shape: `WebhookConfig.protection` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | ✅ | Lock policy — none \| no-overlay \| no-delete \| full. | +| **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | +| **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | + --- diff --git a/content/docs/references/kernel/cli-extension.mdx b/content/docs/references/kernel/cli-extension.mdx index 7d1657c0c5..c48f11b1e0 100644 --- a/content/docs/references/kernel/cli-extension.mdx +++ b/content/docs/references/kernel/cli-extension.mdx @@ -104,6 +104,14 @@ oclif plugin configuration section | **commands** | `{ strategy?: Enum<'pattern' \| 'explicit' \| 'single'>; target?: string; glob?: string }` | optional | Command discovery configuration | | **topicSeparator** | `string` | optional | Character separating topic and command names | +### Nested Shape: `OclifPluginConfig.commands` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **strategy** | `Enum<'pattern' \| 'explicit' \| 'single'>` | optional | Command discovery strategy | +| **target** | `string` | optional | Target directory for command files | +| **glob** | `string` | optional | Glob pattern for command file matching | + --- diff --git a/content/docs/references/kernel/context.mdx b/content/docs/references/kernel/context.mdx index ae8646f53c..6c5a9a1f4e 100644 --- a/content/docs/references/kernel/context.mdx +++ b/content/docs/references/kernel/context.mdx @@ -40,6 +40,17 @@ const result = KernelContextSchema.parse(data); | **features** | `Record` | optional (default: `{}`) | Global feature toggles | | **previewMode** | `{ autoLogin: boolean; simulatedRole: Enum<'admin' \| 'user' \| 'viewer'>; simulatedUserName: string; readOnly: boolean; … }` | optional | Preview/demo mode configuration (used when mode is "preview") | +### Nested Shape: `KernelContext.previewMode` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **autoLogin** | `boolean` | optional (default: `true`) | Auto-login as simulated user, skipping login/registration pages | +| **simulatedRole** | `Enum<'admin' \| 'user' \| 'viewer'>` | optional (default: `"admin"`) | Permission role for the simulated preview user | +| **simulatedUserName** | `string` | optional (default: `"Preview User"`) | Display name for the simulated preview user | +| **readOnly** | `boolean` | optional (default: `false`) | Restrict the preview session to read-only operations | +| **expiresInSeconds** | `integer` | optional (default: `0`) | Preview session duration in seconds (0 = no expiration) | +| **bannerMessage** | `string` | optional | Banner message displayed in the UI during preview mode | + --- @@ -97,6 +108,29 @@ Tenant-aware kernel runtime context | **tenantDbUrl** | `string` | ✅ | Tenant database connection URL | | **tenantQuotas** | `{ maxUsers?: integer; maxStorage?: integer; apiRateLimit?: integer; maxObjects?: integer; … }` | optional | Tenant resource quotas | +### Nested Shape: `TenantRuntimeContext.previewMode` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **autoLogin** | `boolean` | optional (default: `true`) | Auto-login as simulated user, skipping login/registration pages | +| **simulatedRole** | `Enum<'admin' \| 'user' \| 'viewer'>` | optional (default: `"admin"`) | Permission role for the simulated preview user | +| **simulatedUserName** | `string` | optional (default: `"Preview User"`) | Display name for the simulated preview user | +| **readOnly** | `boolean` | optional (default: `false`) | Restrict the preview session to read-only operations | +| **expiresInSeconds** | `integer` | optional (default: `0`) | Preview session duration in seconds (0 = no expiration) | +| **bannerMessage** | `string` | optional | Banner message displayed in the UI during preview mode | + +### Nested Shape: `TenantRuntimeContext.tenantQuotas` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **maxUsers** | `integer` | optional | Maximum number of users | +| **maxStorage** | `integer` | optional | Maximum storage in bytes | +| **apiRateLimit** | `integer` | optional | API requests per minute | +| **maxObjects** | `integer` | optional | Maximum number of custom objects | +| **maxRecordsPerObject** | `integer` | optional | Maximum records per object | +| **maxDeploymentsPerDay** | `integer` | optional | Maximum deployments per day | +| **maxStorageBytes** | `integer` | optional | Maximum storage in bytes | + --- diff --git a/content/docs/references/kernel/dependency-resolution.mdx b/content/docs/references/kernel/dependency-resolution.mdx index c670ff4ac9..505e90b5e4 100644 --- a/content/docs/references/kernel/dependency-resolution.mdx +++ b/content/docs/references/kernel/dependency-resolution.mdx @@ -56,6 +56,29 @@ Complete dependency resolution result | **installOrder** | `string[]` | ✅ | Topologically sorted package IDs for installation | | **circularDependencies** | `string[][]` | optional | Circular dependency chains detected (e.g. [["A", "B", "A"]]) | +### Nested Shape: `DependencyResolutionResult.dependencies[number]` + +Resolution result for a single dependency + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **packageId** | `string` | ✅ | Dependency package identifier | +| **requiredRange** | `string` | ✅ | SemVer range required (e.g. "^2.0.0") | +| **resolvedVersion** | `string` | optional | Actual version resolved from registry | +| **installedVersion** | `string` | optional | Currently installed version | +| **status** | `Enum<'satisfied' \| 'needs_install' \| 'needs_upgrade' \| 'conflict'>` | ✅ | Resolution status | +| **conflictReason** | `string` | optional | Explanation of the conflict | + +### Nested Shape: `DependencyResolutionResult.requiredActions[number]` + +Action required before installation can proceed + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'install' \| 'upgrade' \| 'confirm_conflict'>` | ✅ | Type of action required | +| **packageId** | `string` | ✅ | Target package identifier | +| **description** | `string` | ✅ | Human-readable action description | + --- diff --git a/content/docs/references/kernel/events-bus.mdx b/content/docs/references/kernel/events-bus.mdx index 8f3e0ff9ca..a30957ddc7 100644 --- a/content/docs/references/kernel/events-bus.mdx +++ b/content/docs/references/kernel/events-bus.mdx @@ -50,6 +50,107 @@ const result = EventBusConfigSchema.parse(data); | **eventTypes** | `{ name: string; version: string; schema?: any; description?: string; … }[]` | optional | Event type definitions | | **handlers** | `{ id?: string; eventName: string; handler: any; priority: integer; … }[]` | optional | Global event handlers | +### Nested Shape: `EventBusConfig.persistence` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `false`) | Enable event persistence | +| **retention** | `integer` | ✅ | Days to retain persisted events | +| **filter** | `any` | optional | Optional filter function to select which events to persist | +| **storage** | `Enum<'database' \| 'file' \| 's3' \| 'custom'>` | optional (default: `"database"`) | Storage backend for persisted events | + +### Nested Shape: `EventBusConfig.queue` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional (default: `"events"`) | Event queue name | +| **concurrency** | `integer` | optional (default: `10`) | Max concurrent event handlers | +| **retryPolicy** | `{ maxRetries: integer; backoffStrategy: Enum<'fixed' \| 'linear' \| 'exponential'>; initialDelayMs: integer; maxDelayMs: integer }` | optional | Default retry policy for events | +| **deadLetterQueue** | `string` | optional | Dead letter queue name for failed events | +| **priorityEnabled** | `boolean` | optional (default: `true`) | Process events based on priority | + +### Nested Shape: `EventBusConfig.eventSourcing` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `false`) | Enable event sourcing | +| **snapshotInterval** | `integer` | optional (default: `100`) | Create snapshot every N events | +| **snapshotRetention** | `integer` | optional (default: `10`) | Number of snapshots to retain | +| **retention** | `integer` | optional (default: `365`) | Days to retain events | +| **aggregateTypes** | `string[]` | optional | Aggregate types to enable event sourcing for | +| **storage** | `{ type: Enum<'database' \| 'file' \| 's3' \| 'eventstore'>; options?: Record }` | optional | Event store configuration | + +### Nested Shape: `EventBusConfig.replay` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | Enable event replay capability | + +### Nested Shape: `EventBusConfig.webhooks[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | optional | Unique webhook identifier | +| **eventPattern** | `string` | ✅ | Event name pattern (supports wildcards) | +| **url** | `string` | ✅ | Webhook endpoint URL | +| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'PATCH'>` | optional (default: `"POST"`) | HTTP method | +| **headers** | `Record` | optional | HTTP headers | +| **authentication** | `{ type: Enum<'none' \| 'bearer' \| 'basic' \| 'api-key'>; credentials?: Record }` | optional | Authentication configuration | +| **retryPolicy** | `{ maxRetries: integer; backoffStrategy: Enum<'fixed' \| 'linear' \| 'exponential'>; initialDelayMs: integer; maxDelayMs: integer }` | optional | Retry policy | +| **timeoutMs** | `integer` | optional (default: `30000`) | Request timeout in milliseconds | +| **transform** | `any` | optional | Transform event before sending | +| **enabled** | `boolean` | optional (default: `true`) | Whether webhook is enabled | + +### Nested Shape: `EventBusConfig.messageQueue` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **provider** | `Enum<'kafka' \| 'rabbitmq' \| 'aws-sqs' \| 'redis-pubsub' \| 'google-pubsub' \| 'azure-service-bus'>` | ✅ | Message queue provider | +| **topic** | `string` | ✅ | Topic or queue name | +| **eventPattern** | `string` | optional (default: `"*"`) | Event name pattern to publish (supports wildcards) | +| **partitionKey** | `string` | optional | JSON path for partition key (e.g., "metadata.tenantId") | +| **format** | `Enum<'json' \| 'avro' \| 'protobuf'>` | optional (default: `"json"`) | Message serialization format | +| **includeMetadata** | `boolean` | optional (default: `true`) | Include event metadata in message | +| **compression** | `Enum<'none' \| 'gzip' \| 'snappy' \| 'lz4'>` | optional (default: `"none"`) | Message compression | +| **batchSize** | `integer` | optional (default: `1`) | Batch size for publishing | +| **flushIntervalMs** | `integer` | optional (default: `1000`) | Flush interval for batching | + +### Nested Shape: `EventBusConfig.realtime` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | Enable real-time notifications | +| **protocol** | `Enum<'websocket' \| 'sse' \| 'long-polling'>` | optional (default: `"websocket"`) | Real-time protocol | +| **eventPattern** | `string` | optional (default: `"*"`) | Event pattern to broadcast | +| **userFilter** | `boolean` | optional (default: `true`) | Filter events by user | +| **tenantFilter** | `boolean` | optional (default: `true`) | Filter events by tenant | +| **channels** | `{ name: string; eventPattern: string; filter?: any }[]` | optional | Named channels for event broadcasting | +| **rateLimit** | `{ maxEventsPerSecond: integer; windowMs: integer }` | optional | Rate limiting configuration | + +### Nested Shape: `EventBusConfig.eventTypes[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Event type name (lowercase with dots) | +| **version** | `string` | optional (default: `"1.0.0"`) | Event schema version | +| **schema** | `any` | optional | JSON Schema for event payload validation | +| **description** | `string` | optional | Event type description | +| **deprecated** | `boolean` | optional (default: `false`) | Whether this event type is deprecated | +| **tags** | `string[]` | optional | Event type tags | + +### Nested Shape: `EventBusConfig.handlers[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | optional | Unique handler identifier | +| **eventName** | `string` | ✅ | Name of event to handle (supports wildcards like user.*) | +| **handler** | `any` | ✅ | Handler function | +| **priority** | `integer` | optional (default: `0`) | Execution priority (lower numbers execute first) | +| **async** | `boolean` | optional (default: `true`) | Execute in background (true) or block (false) | +| **retry** | `{ maxRetries: integer; backoffMs: integer; backoffMultiplier: number }` | optional | Retry policy for failed handlers | +| **timeoutMs** | `integer` | optional | Handler timeout in milliseconds | +| **filter** | `any` | optional | Optional filter to determine if handler should execute | + --- diff --git a/content/docs/references/kernel/events-core.mdx b/content/docs/references/kernel/events-core.mdx index a7dceaba2a..756f829cc2 100644 --- a/content/docs/references/kernel/events-core.mdx +++ b/content/docs/references/kernel/events-core.mdx @@ -36,6 +36,19 @@ const result = EventSchema.parse(data); | **payload** | `any` | ✅ | Event payload schema | | **metadata** | `{ source: string; timestamp: string; userId?: string; tenantId?: string; … }` | ✅ | Event metadata | +### Nested Shape: `Event.metadata` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **source** | `string` | ✅ | Event source (e.g., plugin name, system component) | +| **timestamp** | `string` | ✅ | ISO 8601 datetime when event was created | +| **userId** | `string` | optional | User who triggered the event | +| **tenantId** | `string` | optional | Tenant identifier for multi-tenant systems | +| **correlationId** | `string` | optional | Correlation ID for event tracing | +| **causationId** | `string` | optional | ID of the event that caused this event | +| **priority** | `Enum<'critical' \| 'high' \| 'normal' \| 'low' \| 'background'>` | optional (default: `"normal"`) | Event priority | +| **cluster** | `{ scope: Enum<'local' \| 'cluster' \| 'tenant'>; deliverySemantics?: Enum<'best-effort' \| 'at-least-once' \| 'exactly-once'>; partitionKey?: string }` | optional | Per-emit cluster routing & delivery options. See cluster-semantics.mdx §4. | + --- @@ -54,6 +67,14 @@ const result = EventSchema.parse(data); | **priority** | `Enum<'critical' \| 'high' \| 'normal' \| 'low' \| 'background'>` | optional (default: `"normal"`) | Event priority | | **cluster** | `{ scope: Enum<'local' \| 'cluster' \| 'tenant'>; deliverySemantics?: Enum<'best-effort' \| 'at-least-once' \| 'exactly-once'>; partitionKey?: string }` | optional | Per-emit cluster routing & delivery options. See cluster-semantics.mdx §4. | +### Nested Shape: `EventMetadata.cluster` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **scope** | `Enum<'local' \| 'cluster' \| 'tenant'>` | optional (default: `"local"`) | Delivery scope. Default `local` for backward compatibility. | +| **deliverySemantics** | `Enum<'best-effort' \| 'at-least-once' \| 'exactly-once'>` | optional | Delivery guarantee. Default depends on scope. | +| **partitionKey** | `string` | optional | Stable key that guarantees emit-order delivery for same-key events. | + --- diff --git a/content/docs/references/kernel/events-dlq.mdx b/content/docs/references/kernel/events-dlq.mdx index ae57986e14..cb569af9b6 100644 --- a/content/docs/references/kernel/events-dlq.mdx +++ b/content/docs/references/kernel/events-dlq.mdx @@ -38,6 +38,23 @@ const result = DeadLetterQueueEntrySchema.parse(data); | **lastFailedAt** | `string` | ✅ | When event last failed | | **failedHandler** | `string` | optional | Handler ID that failed | +### Nested Shape: `DeadLetterQueueEntry.event` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | optional | Unique event identifier | +| **name** | `string` | ✅ | Event name (lowercase with dots, e.g., user.created, order.paid) | +| **payload** | `any` | ✅ | Event payload schema | +| **metadata** | `{ source: string; timestamp: string; userId?: string; tenantId?: string; … }` | ✅ | Event metadata | + +### Nested Shape: `DeadLetterQueueEntry.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **message** | `string` | ✅ | Error message | +| **stack** | `string` | optional | Error stack trace | +| **code** | `string` | optional | Error code | + --- @@ -55,6 +72,24 @@ const result = DeadLetterQueueEntrySchema.parse(data); | **processedAt** | `string` | optional | When event was processed | | **totalDurationMs** | `integer` | optional | Total processing time | +### Nested Shape: `EventLogEntry.event` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | optional | Unique event identifier | +| **name** | `string` | ✅ | Event name (lowercase with dots, e.g., user.created, order.paid) | +| **payload** | `any` | ✅ | Event payload schema | +| **metadata** | `{ source: string; timestamp: string; userId?: string; tenantId?: string; … }` | ✅ | Event metadata | + +### Nested Shape: `EventLogEntry.handlersExecuted[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **handlerId** | `string` | ✅ | Handler identifier | +| **status** | `Enum<'success' \| 'failed' \| 'timeout'>` | ✅ | Handler execution status | +| **durationMs** | `integer` | optional | Execution duration | +| **error** | `string` | optional | Error message if failed | + --- diff --git a/content/docs/references/kernel/events-handlers.mdx b/content/docs/references/kernel/events-handlers.mdx index 65483d91a1..2355ebeb0c 100644 --- a/content/docs/references/kernel/events-handlers.mdx +++ b/content/docs/references/kernel/events-handlers.mdx @@ -39,6 +39,14 @@ const result = EventHandlerSchema.parse(data); | **timeoutMs** | `integer` | optional | Handler timeout in milliseconds | | **filter** | `any` | optional | Optional filter to determine if handler should execute | +### Nested Shape: `EventHandler.retry` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **maxRetries** | `integer` | optional (default: `3`) | Maximum retry attempts | +| **backoffMs** | `integer` | optional (default: `1000`) | Initial backoff delay | +| **backoffMultiplier** | `number` | optional (default: `2`) | Backoff multiplier | + --- diff --git a/content/docs/references/kernel/events-integrations.mdx b/content/docs/references/kernel/events-integrations.mdx index 05fb1d7035..8b11fe2929 100644 --- a/content/docs/references/kernel/events-integrations.mdx +++ b/content/docs/references/kernel/events-integrations.mdx @@ -68,6 +68,22 @@ const result = EventMessageQueueConfigSchema.parse(data); | **transform** | `any` | optional | Transform event before sending | | **enabled** | `boolean` | optional (default: `true`) | Whether webhook is enabled | +### Nested Shape: `EventWebhookConfig.authentication` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'none' \| 'bearer' \| 'basic' \| 'api-key'>` | ✅ | Auth type | +| **credentials** | `Record` | optional | Auth credentials | + +### Nested Shape: `EventWebhookConfig.retryPolicy` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **maxRetries** | `integer` | optional (default: `3`) | Max retry attempts | +| **backoffStrategy** | `Enum<'fixed' \| 'linear' \| 'exponential'>` | optional (default: `"exponential"`) | | +| **initialDelayMs** | `integer` | optional (default: `1000`) | Initial retry delay | +| **maxDelayMs** | `integer` | optional (default: `60000`) | Max retry delay | + --- @@ -85,6 +101,21 @@ const result = EventMessageQueueConfigSchema.parse(data); | **channels** | `{ name: string; eventPattern: string; filter?: any }[]` | optional | Named channels for event broadcasting | | **rateLimit** | `{ maxEventsPerSecond: integer; windowMs: integer }` | optional | Rate limiting configuration | +### Nested Shape: `RealTimeNotificationConfig.channels[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Channel name | +| **eventPattern** | `string` | ✅ | Event pattern for channel | +| **filter** | `any` | optional | Additional filter function | + +### Nested Shape: `RealTimeNotificationConfig.rateLimit` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **maxEventsPerSecond** | `integer` | ✅ | Max events per second per client | +| **windowMs** | `integer` | optional (default: `1000`) | Rate limit window | + --- diff --git a/content/docs/references/kernel/events-queue.mdx b/content/docs/references/kernel/events-queue.mdx index 9ff14f6ff0..ef9ce7a14d 100644 --- a/content/docs/references/kernel/events-queue.mdx +++ b/content/docs/references/kernel/events-queue.mdx @@ -46,6 +46,15 @@ const result = EventQueueConfigSchema.parse(data); | **deadLetterQueue** | `string` | optional | Dead letter queue name for failed events | | **priorityEnabled** | `boolean` | optional (default: `true`) | Process events based on priority | +### Nested Shape: `EventQueueConfig.retryPolicy` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **maxRetries** | `integer` | optional (default: `3`) | Max retries for failed events | +| **backoffStrategy** | `Enum<'fixed' \| 'linear' \| 'exponential'>` | optional (default: `"exponential"`) | Backoff strategy | +| **initialDelayMs** | `integer` | optional (default: `1000`) | Initial retry delay | +| **maxDelayMs** | `integer` | optional (default: `60000`) | Maximum retry delay | + --- @@ -78,6 +87,13 @@ const result = EventQueueConfigSchema.parse(data); | **aggregateTypes** | `string[]` | optional | Aggregate types to enable event sourcing for | | **storage** | `{ type: Enum<'database' \| 'file' \| 's3' \| 'eventstore'>; options?: Record }` | optional | Event store configuration | +### Nested Shape: `EventSourcingConfig.storage` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'database' \| 'file' \| 's3' \| 'eventstore'>` | optional (default: `"database"`) | Storage backend | +| **options** | `Record` | optional | Storage-specific options | + --- diff --git a/content/docs/references/kernel/manifest.mdx b/content/docs/references/kernel/manifest.mdx index aec996fedf..cbea312efd 100644 --- a/content/docs/references/kernel/manifest.mdx +++ b/content/docs/references/kernel/manifest.mdx @@ -52,6 +52,91 @@ const result = ManifestSchema.parse(data); | **packaging** | `Enum<'bundled' \| 'manifest-deps'>` | optional | Dependency packaging strategy (ADR-0025 §3.3) | | **integrity** | `Record` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) | +### Nested Shape: `Manifest.permissions` + +Structured plugin permission grants (ADR-0025 §3.2) + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **services** | `string[]` | optional | Platform services the plugin may resolve (e.g. "object", "http") | +| **hooks** | `string[]` | optional | Lifecycle hooks the plugin may register (e.g. "record.beforeInsert") | +| **network** | `string[]` | optional | Network hosts the plugin may reach (e.g. "api.acme.com") | +| **fs** | `string[]` | optional | Filesystem paths the plugin may access | + +### Nested Shape: `Manifest.configuration` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **title** | `string` | optional | | +| **properties** | `Record; default?: any; description?: string; required?: boolean; … }>` | ✅ | Map of configuration keys to their definitions | + +### Nested Shape: `Manifest.contributes` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **kinds** | `{ id: string; description?: string }[]` | optional | Metadata kind identifiers this package registers | +| **events** | `never` | optional | [REMOVED] `manifest.contributes.events` was removed in @objectstack/spec 17 (#10724, ADR-0049 enforce-or-remove) — nothing ever read the list: its only in-repo author already subscribed imperatively in plugin code, so the declaration was decorative. Delete the key. Subscribe to system events in the plugin itself — `ctx.hook('kernel:ready', …)` (or the events service) from `init`/`start` is the enforced channel; record lifecycle hooks register on the data engine. | +| **menus** | `never` | optional | [REMOVED] `manifest.contributes.menus` was removed in @objectstack/spec 17 (#10724, ADR-0049 enforce-or-remove) — no renderer ever read it; two alias maps already redirected this spelling to `navigation`. Delete the key. Declare navigation in the app's `navigation` tree, or inject items into another package's app via `manifest.navigationContributions` (ADR-0029 D7), which the engine registers. | +| **themes** | `never` | optional | [REMOVED] `manifest.contributes.themes` was removed in @objectstack/spec 17 (#10724, ADR-0049 enforce-or-remove) — it never had an effect: theme registration reaches the registry only through the stack-level `themes` collection (a `ThemeSchema` surface, unrelated to this `{ id, label, path }` shape), never through `contributes.themes`. Delete the key; declare themes in the stack `themes` collection instead. | +| **translations** | `never` | optional | [REMOVED] `manifest.contributes.translations` was removed in @objectstack/spec 17 (#10724, ADR-0049 enforce-or-remove) — no loader ever read these `{ locale, path }` entries; authoring them registered no translations. Delete the key. Declare translations as `translation` metadata: `defineTranslationBundle({ … })` in the stack's `translations` collection (`defineStack({ translations: […] })`), which the engine registers and the i18n pipeline serves. | +| **actions** | `never` | optional | [REMOVED] `manifest.contributes.actions` was removed in @objectstack/spec 17 (#10724, ADR-0049 enforce-or-remove) — nothing ever read it; actions declared here were never invocable. Delete the key. Declare actions in the stack `actions` collection (registered by the engine) or register imperatively via `engine.registerAction`. | +| **drivers** | `never` | optional | [REMOVED] `manifest.contributes.drivers` was removed in @objectstack/spec 17 (#10724, ADR-0049 enforce-or-remove) — it never had an effect: a storage driver is wired by registering a kernel SERVICE named `driver.*` (the objectql plugin picks it up and calls `registerDriver`), and its only in-repo author was registered that way, not by this declaration. Delete the key. | +| **fieldTypes** | `never` | optional | [REMOVED] `manifest.contributes.fieldTypes` was removed in @objectstack/spec 17 (#10724, ADR-0049 enforce-or-remove) — there is no `registerFieldType` seam anywhere: the declaration advertised an extension point the platform does not have, so authoring it configured nothing. Delete the key. The field-type vocabulary is the spec `FieldType` enum; extending it is a spec change, not a manifest declaration. | +| **functions** | `never` | optional | [REMOVED] `manifest.contributes.functions` was removed in @objectstack/spec 17 (#10724, ADR-0049 enforce-or-remove) — nothing ever read it; ObjectQL functions declared here were never registered. Delete the key. Declare functions on the stack (`defineStack({ functions: […] })`), which the hook binder registers via `engine.registerFunction`. | +| **routes** | `{ prefix: string; service: string; methods?: string[] }[]` | optional | API route contributions to HttpDispatcher | +| **commands** | `never` | optional | [REMOVED] `manifest.contributes.commands` was removed in @objectstack/spec 17 (#10724, ADR-0049 enforce-or-remove) — the CLI never resolved commands from this declaration: commands are auto-discovered through oclif's native plugin system (the plugin package declares an `oclif` section in its own `package.json`; see `cli-extension.zod.ts`), and the `objectstack.config.ts` plugins array no longer determines CLI commands. Delete the key. | + +### Nested Shape: `Manifest.data[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **object** | `string` | ✅ | Target Object Name | +| **externalId** | `string \| string[]` | optional (default: `"name"`) | Field (or composite list of fields) matched for the uniqueness check | +| **mode** | `Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>` | optional (default: `"upsert"`) | Conflict resolution strategy | +| **env** | `Enum<'prod' \| 'dev' \| 'test'>[]` | optional (default: `["prod","dev","test"]`) | Applicable environments | +| **records** | `Record[]` | ✅ | Data records | +| **_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. | + +### Nested Shape: `Manifest.capabilities` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **implements** | `{ protocol: object; conformance?: Enum<'full' \| 'partial' \| 'experimental' \| 'deprecated'>; implementedFeatures?: string[]; features?: object[]; … }[]` | optional | List of protocols this plugin conforms to | +| **provides** | `{ id: string; name: string; description?: string; version: object; … }[]` | optional | Services/APIs this plugin offers to others | +| **requires** | `{ pluginId: string; version: string; optional?: boolean; reason?: string; … }[]` | optional | Required plugins and their capabilities | +| **extensionPoints** | `{ id: string; name: string; description?: string; type: Enum<'action' \| 'hook' \| 'widget' \| 'provider' \| 'transformer' \| 'validator' \| 'decorator'>; … }[]` | optional | Points where other plugins can extend this plugin | +| **extensions** | `{ targetPluginId: string; extensionPointId: string; implementation: string; priority?: integer }[]` | optional | Extensions contributed to other plugins | + +### Nested Shape: `Manifest.navigationContributions[number]` + +A navigation contribution: a package injecting nav items into an app it does not own (ADR-0029 D7) + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **app** | `string` | ✅ | Target app name to contribute navigation into (e.g. "setup") | +| **group** | `string` | optional | Target group nav-item id to append into (e.g. "group_integrations"); omit to append at the app top level | +| **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: `Manifest.engine` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **objectstack** | `string` | ✅ | ObjectStack platform version requirement (SemVer range, e.g. ">=3.0.0") | + +### Nested Shape: `Manifest.engines` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **platform** | `string` | optional | ObjectStack platform release range (SemVer, e.g. ">=4.0 <5") | +| **protocol** | `string` | optional | Runtime/metadata protocol range, checked first (ADR §3.10 #3) | + --- diff --git a/content/docs/references/kernel/metadata-customization.mdx b/content/docs/references/kernel/metadata-customization.mdx index f3dc26deab..bcf5239ad7 100644 --- a/content/docs/references/kernel/metadata-customization.mdx +++ b/content/docs/references/kernel/metadata-customization.mdx @@ -122,6 +122,26 @@ const result = CustomizationOriginSchema.parse(data); | **autoResolved** | `{ path: string; resolution: string; description?: string }[]` | optional | Summary of auto-resolved changes | | **stats** | `{ totalFields: integer; unchanged: integer; autoResolved: integer; conflicts: integer }` | optional | | +### Nested Shape: `MergeResult.conflicts[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **path** | `string` | ✅ | JSON path to the conflicting field | +| **baseValue** | `any` | ✅ | Value in the old package version | +| **incomingValue** | `any` | ✅ | Value in the new package version | +| **customValue** | `any` | ✅ | Customer customized value | +| **suggestedResolution** | `Enum<'keep-custom' \| 'accept-incoming' \| 'manual'>` | ✅ | Suggested resolution strategy | +| **reason** | `string` | optional | Explanation for the suggested resolution | + +### Nested Shape: `MergeResult.stats` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **totalFields** | `integer` | ✅ | Total fields evaluated | +| **unchanged** | `integer` | ✅ | Fields with no changes | +| **autoResolved** | `integer` | ✅ | Fields auto-resolved | +| **conflicts** | `integer` | ✅ | Fields with conflicts | + --- @@ -161,6 +181,16 @@ const result = CustomizationOriginSchema.parse(data); | **updatedAt** | `string` | optional | | | **updatedBy** | `string` | optional | | +### Nested Shape: `MetadataOverlay.changes[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **path** | `string` | ✅ | JSON path to the changed field | +| **originalValue** | `any` | optional | Original value from the package | +| **currentValue** | `any` | ✅ | Current customized value | +| **changedBy** | `string` | optional | User or admin who made this change | +| **changedAt** | `string` | optional | Timestamp of the change | + --- diff --git a/content/docs/references/kernel/metadata-loader.mdx b/content/docs/references/kernel/metadata-loader.mdx index 14dd2fdbff..dd18be8103 100644 --- a/content/docs/references/kernel/metadata-loader.mdx +++ b/content/docs/references/kernel/metadata-loader.mdx @@ -56,6 +56,37 @@ const result = MetadataFallbackStrategySchema.parse(data); | **loaderOptions** | `Record` | optional | Loader-specific configuration | | **persistence** | `{ writable: boolean; overlayWritable: boolean }` | optional | Persistence write gates | +### Nested Shape: `MetadataManagerConfig.cache` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | Enable caching | +| **ttl** | `integer` | optional (default: `3600`) | Cache TTL in seconds | +| **maxSize** | `integer` | optional | Max cache size in bytes | +| **databaseLoader** | `{ enabled: boolean; maxSize: integer; ttl: integer }` | optional | DatabaseLoader read-through cache | + +### Nested Shape: `MetadataManagerConfig.watchOptions` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ignored** | `string[]` | optional | Patterns to ignore | +| **persistent** | `boolean` | optional (default: `true`) | Keep process running | +| **ignoreInitial** | `boolean` | optional (default: `true`) | Ignore initial add events | + +### Nested Shape: `MetadataManagerConfig.validation` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **strict** | `boolean` | optional (default: `true`) | Strict validation | +| **throwOnError** | `boolean` | optional (default: `true`) | Throw on validation error | + +### Nested Shape: `MetadataManagerConfig.persistence` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **writable** | `boolean` | optional (default: `true`) | Allow base metadata writes via register() | +| **overlayWritable** | `boolean` | optional (default: `true`) | Allow overlay writes via saveOverlay() | + --- diff --git a/content/docs/references/kernel/metadata-plugin.mdx b/content/docs/references/kernel/metadata-plugin.mdx index 11967a909c..09aa14ab6d 100644 --- a/content/docs/references/kernel/metadata-plugin.mdx +++ b/content/docs/references/kernel/metadata-plugin.mdx @@ -68,6 +68,14 @@ const result = MetadataBulkResultSchema.parse(data); | **failed** | `integer` | ✅ | Failed items | | **errors** | `{ type: string; name: string; error: string }[]` | optional | Per-item errors | +### Nested Shape: `MetadataBulkResult.errors[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `string` | ✅ | Metadata type | +| **name** | `string` | ✅ | Item name | +| **error** | `string` | ✅ | Error message | + --- @@ -102,6 +110,42 @@ const result = MetadataBulkResultSchema.parse(data); | **cacheMaxItems** | `integer` | optional (default: `10000`) | Max items in memory cache | | **bootstrap** | `Enum<'eager' \| 'lazy' \| 'artifact-only'>` | optional (default: `"eager"`) | How metadata is primed at plugin start (eager / lazy / artifact-only) | +### Nested Shape: `MetadataPluginConfig.storage` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **datasource** | `string` | optional | Datasource name reference for database persistence | +| **tableName** | `string` | optional (default: `"sys_metadata"`) | Database table name for metadata storage | +| **fallback** | `Enum<'filesystem' \| 'memory' \| 'none'>` | optional (default: `"none"`) | Fallback strategy when datasource is unavailable | +| **rootDir** | `string` | optional | Root directory path | +| **formats** | `Enum<'yaml' \| 'json' \| 'typescript' \| 'javascript'>[]` | optional (default: `["typescript","json","yaml"]`) | Enabled formats | +| **cache** | `{ enabled: boolean; ttl: integer; maxSize?: integer; databaseLoader?: object }` | optional | Cache settings | +| **watch** | `boolean` | optional (default: `false`) | Enable file watching | +| **watchOptions** | `{ ignored?: string[]; persistent: boolean; ignoreInitial: boolean }` | optional | File watcher options | +| **validation** | `{ strict: boolean; throwOnError: boolean }` | optional | Validation settings | +| **loaderOptions** | `Record` | optional | Loader-specific configuration | +| **persistence** | `{ writable: boolean; overlayWritable: boolean }` | optional | Persistence write gates | + +### Nested Shape: `MetadataPluginConfig.customizationPolicies[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **metadataType** | `string` | ✅ | Metadata type (e.g. "object", "view") | +| **allowCustomization** | `boolean` | optional (default: `true`) | | +| **lockedFields** | `string[]` | optional | Field paths that cannot be customized | +| **customizableFields** | `string[]` | optional | Field paths that can be customized (whitelist) | +| **allowAddFields** | `boolean` | optional (default: `true`) | Whether admins can add new fields to package objects | +| **allowDeleteFields** | `boolean` | optional (default: `false`) | Whether admins can delete package-delivered fields | + +### Nested Shape: `MetadataPluginConfig.mergeStrategy` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **defaultStrategy** | `Enum<'keep-custom' \| 'accept-incoming' \| 'three-way-merge'>` | optional (default: `"three-way-merge"`) | Default merge strategy | +| **alwaysAcceptIncoming** | `string[]` | optional | Field paths that always accept package updates | +| **alwaysKeepCustom** | `string[]` | optional | Field paths where customer customizations always win | +| **autoResolveNonConflicting** | `boolean` | optional (default: `true`) | Auto-resolve changes that do not conflict | + --- @@ -119,6 +163,33 @@ const result = MetadataBulkResultSchema.parse(data); | **capabilities** | `{ crud: boolean; query: boolean; overlay: boolean; watch: boolean; … }` | ✅ | Plugin capabilities | | **config** | `{ storage: object; customizationPolicies?: object[]; mergeStrategy?: object; enableEvents: boolean; … }` | optional | Plugin configuration | +### Nested Shape: `MetadataPluginManifest.capabilities` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **crud** | `boolean` | optional (default: `true`) | Supports metadata CRUD | +| **query** | `boolean` | optional (default: `true`) | Supports metadata query | +| **overlay** | `boolean` | optional (default: `true`) | Supports customization overlays | +| **watch** | `boolean` | optional (default: `false`) | Supports file watching | +| **importExport** | `boolean` | optional (default: `true`) | Supports import/export | +| **validation** | `boolean` | optional (default: `true`) | Supports schema validation | +| **versioning** | `boolean` | optional (default: `false`) | Supports version history | +| **events** | `boolean` | optional (default: `true`) | Emits metadata events | + +### Nested Shape: `MetadataPluginManifest.config` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **storage** | `{ datasource?: string; tableName: string; fallback: Enum<'filesystem' \| 'memory' \| 'none'>; rootDir?: string; … }` | ✅ | Storage backend configuration | +| **customizationPolicies** | `{ metadataType: string; allowCustomization: boolean; lockedFields?: string[]; customizableFields?: string[]; … }[]` | optional | Default customization policies per type | +| **mergeStrategy** | `{ defaultStrategy: Enum<'keep-custom' \| 'accept-incoming' \| 'three-way-merge'>; alwaysAcceptIncoming?: string[]; alwaysKeepCustom?: string[]; autoResolveNonConflicting: boolean }` | optional | Merge strategy for package upgrades | +| **additionalTypes** | `never` | optional | [REMOVED] `config.additionalTypes` was removed from `MetadataPluginConfig` in @objectstack/spec 17 (#8586, ADR-0049 enforce-or-remove) — it never had an effect: the only production writer of the metadata type registry is `setTypeRegistry(DEFAULT_METADATA_TYPE_REGISTRY)`, which replaces the array outright, so nothing ever merged these entries and the live type set was exactly the built-in registry whatever you declared here. Delete the key. There is no declared-kind channel: a kind enters the live metadata-type set as a side effect of registering an ITEM of that kind (`SchemaRegistry.registerItem` during app/manifest registration, or `MetadataManager.register` at runtime); bind its schema with `registerMetadataTypeSchema(type, schema)` from your plugin's `init(ctx)` so `GET /api/v1/meta` serves a real JSON Schema for it. | +| **enableEvents** | `boolean` | optional (default: `true`) | Emit metadata change events | +| **validateOnWrite** | `boolean` | optional (default: `true`) | Validate metadata on write | +| **enableVersioning** | `boolean` | optional (default: `false`) | Track metadata version history | +| **cacheMaxItems** | `integer` | optional (default: `10000`) | Max items in memory cache | +| **bootstrap** | `Enum<'eager' \| 'lazy' \| 'artifact-only'>` | optional (default: `"eager"`) | How metadata is primed at plugin start (eager / lazy / artifact-only) | + --- @@ -154,6 +225,19 @@ const result = MetadataBulkResultSchema.parse(data); | **page** | `integer` | ✅ | Current page | | **pageSize** | `integer` | ✅ | Page size | +### Nested Shape: `MetadataQueryResult.items[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `string` | ✅ | Metadata type | +| **name** | `string` | ✅ | Item name | +| **namespace** | `string` | optional | Namespace | +| **label** | `string` | optional | Display label | +| **scope** | `Enum<'system' \| 'platform' \| 'user'>` | optional | | +| **state** | `Enum<'draft' \| 'active' \| 'archived' \| 'deprecated'>` | optional | | +| **packageId** | `string` | optional | | +| **updatedAt** | `string` | optional | | + --- @@ -241,6 +325,56 @@ const result = MetadataBulkResultSchema.parse(data); * `tool` * `skill` +### Nested Shape: `MetadataTypeRegistryEntry.actions[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Machine name (lowercase snake_case) | +| **label** | `string \| Record` | ✅ | Display label | +| **description** | `string \| Record` | optional | Explanatory line shown under the title in the action's param dialog. Carries the confirm question for an action that collects params (one dialog, not two — #7278). Not the LLM-facing `ai.description`. | +| **objectName** | `string` | optional | Target object this action belongs to. When set, the action is auto-merged into the object's actions array by defineStack(). | +| **icon** | `string` | optional | Icon name | +| **locations** | `Enum<'list_toolbar' \| 'list_item' \| 'record_header' \| 'record_more' \| …>[]` | optional | Locations where this action is visible | +| **component** | `Enum<'action:button' \| 'action:icon' \| 'action:menu' \| 'action:group'>` | optional | Visual component override | +| **type** | `Enum<'script' \| 'url' \| 'modal' \| 'flow' \| 'api' \| 'form'>` | optional (default: `"script"`) | Action functionality type | +| **target** | `string` | optional | URL, Script Name, Flow ID, or API Endpoint. Supports $`{param.X}` and $`{ctx.X}` interpolation. | +| **openIn** | `Enum<'self' \| 'new-tab'>` | optional | For type:'url' — where to open `target`. 'new-tab' opens a new browser tab; 'self' navigates in place. When omitted, external/absolute URLs open in a new tab and relative URLs navigate in place. Static execution option — keep it OUT of `params` (which is user-input-collection only). | +| **body** | `{ language: 'expression'; source: string } \| { language: 'js'; source: string; capabilities?: Enum<'api.read' \| 'api.write' \| 'api.transaction' \| 'crypto.uuid' \| 'log'>[]; timeoutMs?: integer; … }` | optional | Action body — expression (L1) or sandboxed JS (L2). Only used when type is `script`. | +| **execute** | `never` | optional | [REMOVED] `execute` was removed in @objectstack/spec 17 (#3855) — use `target`. Rename the key; the value (a handler / flow / URL ref) is unchanged. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **params** | `{ name?: string; field?: string; objectOverride?: string; label?: string \| Record; … }[]` | optional | Input parameters required from user — an ActionParam[] DEFINITION array, never a payload map (a static request body goes in `bodyExtra`). | +| **variant** | `Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'link'>` | optional | Button visual variant for styling (primary = highlighted, danger = destructive, ghost = transparent) | +| **order** | `number` | optional | Sort order within a location group (lower = higher). Promotes/demotes an action toward the record_header primary button; stable, so actions without `order` keep their registration order. | +| **confirmText** | `string \| Record` | optional | Confirmation message before execution. On a registered action, pairing this with a non-empty `params` is refused (#7428) — that opens a second dialog for one decision; put the question on `description` instead. Correct on a param-LESS action, where the confirm is the only dialog there is. | +| **successMessage** | `string \| Record` | optional | Success message to show after execution | +| **errorMessage** | `string \| Record` | optional | Error message to show when the action fails (overrides the raw error). | +| **refreshAfter** | `boolean` | optional (default: `false`) | Refresh view after execution | +| **undoable** | `boolean` | optional | Offer an Undo affordance after this single-record update action succeeds. | +| **resultDialog** | `{ title?: string \| Record; description?: string \| Record; acknowledge?: string \| Record; format?: Enum<'qrcode' \| 'code-list' \| 'secret' \| 'text' \| 'json'>; … }` | optional | Render API response in a one-shot reveal dialog (suppresses successMessage when set). | +| **visible** | `boolean \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate — `true`/`false` literal, CEL string, or `{dialect, source}` envelope. The action is offered when it evaluates TRUE. Omit = always visible. | +| **requiresFeature** | `Enum<'twoFactor' \| 'organization' \| 'multiOrgEnabled' \| 'degradedTenancy' \| …>` | optional | Public auth feature flag gating this action; lowered into `visible` at parse time. | +| **disabled** | `boolean \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Disabled predicate — `true`/`false` literal, CEL string, or `{dialect, source}` envelope. The action is shown but refused when it evaluates TRUE. Omit = never disabled. | +| **requiredPermissions** | `string[]` | optional | [ADR-0066 D4] Capabilities required to invoke this action. Enforced with 403 on the platform action route (script/flow/modal + MCP) and mirrored as a UI hide; a `type: api` action pointed at a custom endpoint must re-check it there. | +| **shortcut** | `never` | optional | [REMOVED] `action.shortcut` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — it never triggered anything: no keydown listener feeds ActionEngine.getShortcuts(), and objectui's keyboard stack (useKeyboardShortcuts) is hand-registered and never consults action metadata. Delete the key. For a real shortcut, register the key in the Console keyboard stack and have its handler invoke the action by name. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **bulkEnabled** | `never` | optional | [REMOVED] `action.bulkEnabled` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — the multi-select toolbar is driven by the LIST VIEW's `bulkActions` / `bulkActionDefs`, never by this flag, so setting it changed nothing. Delete the key and declare the action in the view's `bulkActions` instead. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **ai** | `{ exposed?: boolean; description?: string; category?: Enum<'data' \| 'action' \| 'flow' \| 'integration' \| 'vector_search' \| 'analytics' \| 'utility'>; paramHints?: Record; … }` | optional | AI exposure (opt-in). Set ai.exposed=true + ai.description to make this callable by agents. | +| **recordIdParam** | `string` | optional | Body key to inject the row id into when running from a list_item context. | +| **recordIdField** | `string` | optional | Row field whose value seeds recordIdParam. Defaults to "id". | +| **bodyShape** | `'flat' \| { wrap: string }` | optional | Body wrapping: flat (default) or `{ wrap: key }` to nest user-collected params under a key. | +| **method** | `Enum<'POST' \| 'PATCH' \| 'PUT' \| 'DELETE'>` | optional | HTTP method for type:"api" actions. Defaults to POST. | +| **bodyExtra** | `Record` | optional | Static request-body fields for a type:"api" action, merged last (overrides user params). `{{page.}}` tokens are resolved by the runtime. This — not `params` — is where a payload goes. | +| **mode** | `Enum<'create' \| 'edit' \| 'delete' \| 'custom'>` | optional | Semantic mode of the action. | +| **opensInNewTab** | `boolean` | optional | Open the action result in a new tab. The renderer pre-opens the tab synchronously on click (popup-blocker-safe) and navigates it to the handler's redirectUrl. | +| **newTabUrl** | `string` | optional | Direct new-tab URL template (`{recordId}` placeholder). When set with opensInNewTab, the renderer navigates the pre-opened tab here immediately — no action POST. The endpoint must enforce auth itself. | +| **onSuccess** | `{ navigate: string; openIn?: Enum<'self' \| 'newTab'> }` | optional | Post-success navigation for type:'api' and type:'script' actions (#9566/#9474). `navigate` is a route/URL template interpolating $`{param.*}`, $`{ctx.*}` and $`{result.*}` (the server response); `openIn` defaults 'self'. The handler-return convention (`{ redirectUrl }` without openIn) keeps its 17.0.0 new-tab behavior. | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **_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. | + --- @@ -254,6 +388,21 @@ const result = MetadataBulkResultSchema.parse(data); | **errors** | `{ path: string; message: string; code?: string }[]` | optional | Validation errors | | **warnings** | `{ path: string; message: string }[]` | optional | Validation warnings | +### Nested Shape: `MetadataValidationResult.errors[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **path** | `string` | ✅ | JSON path to the invalid field | +| **message** | `string` | ✅ | Error description | +| **code** | `string` | optional | Error code | + +### Nested Shape: `MetadataValidationResult.warnings[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **path** | `string` | ✅ | JSON path to the field | +| **message** | `string` | ✅ | Warning description | + --- diff --git a/content/docs/references/kernel/package-artifact.mdx b/content/docs/references/kernel/package-artifact.mdx index 46772d7c18..f5565a8562 100644 --- a/content/docs/references/kernel/package-artifact.mdx +++ b/content/docs/references/kernel/package-artifact.mdx @@ -163,6 +163,33 @@ Package artifact structure and metadata | **checksums** | `{ algorithm: Enum<'sha256' \| 'sha384' \| 'sha512'>; files: Record }` | optional | SHA256 checksums for artifact integrity verification | | **signature** | `{ algorithm: Enum<'RSA-SHA256' \| 'RSA-SHA384' \| 'RSA-SHA512' \| 'ECDSA-SHA256'>; publicKeyRef: string; signature: string; signedAt?: string; … }` | optional | Digital signature for artifact authenticity verification | +### Nested Shape: `PackageArtifact.files[number]` + +A single file entry within the artifact + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **path** | `string` | ✅ | Relative file path within the artifact | +| **size** | `integer` | ✅ | File size in bytes | +| **category** | `Enum<'objects' \| 'views' \| 'pages' \| 'flows' \| 'dashboards' \| 'permissions' \| …>` | optional | Metadata category this file belongs to | + +### Nested Shape: `PackageArtifact.checksums` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **algorithm** | `Enum<'sha256' \| 'sha384' \| 'sha512'>` | optional (default: `"sha256"`) | Hash algorithm used for checksums | +| **files** | `Record` | ✅ | File path to hash value mapping | + +### Nested Shape: `PackageArtifact.signature` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **algorithm** | `Enum<'RSA-SHA256' \| 'RSA-SHA384' \| 'RSA-SHA512' \| 'ECDSA-SHA256'>` | optional (default: `"RSA-SHA256"`) | Signing algorithm used | +| **publicKeyRef** | `string` | ✅ | Public key reference (URL or fingerprint) for signature verification | +| **signature** | `string` | ✅ | Base64-encoded digital signature | +| **signedAt** | `string` | optional | ISO 8601 timestamp of when the artifact was signed | +| **signedBy** | `string` | optional | Identity of the signer (publisher ID or email) | + --- diff --git a/content/docs/references/kernel/package-registry.mdx b/content/docs/references/kernel/package-registry.mdx index d5db967319..7bc51a41af 100644 --- a/content/docs/references/kernel/package-registry.mdx +++ b/content/docs/references/kernel/package-registry.mdx @@ -67,6 +67,23 @@ Disable package response | **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Disabled package details | | **message** | `string` | optional | Disable status message | +### Nested Shape: `DisablePackageResponse.package` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Full package manifest | +| **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | +| **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | +| **installedAt** | `string` | optional | Installation timestamp | +| **updatedAt** | `string` | optional | Last update timestamp | +| **installedVersion** | `string` | optional | Currently installed version for quick access | +| **previousVersion** | `string` | optional | Version before the last upgrade | +| **statusChangedAt** | `string` | optional | Status change timestamp | +| **errorMessage** | `string` | optional | Error message when status is error | +| **settings** | `Record` | optional | User-provided configuration settings | +| **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history | +| **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package | + --- @@ -94,6 +111,23 @@ Enable package response | **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Enabled package details | | **message** | `string` | optional | Enable status message | +### Nested Shape: `EnablePackageResponse.package` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Full package manifest | +| **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | +| **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | +| **installedAt** | `string` | optional | Installation timestamp | +| **updatedAt** | `string` | optional | Last update timestamp | +| **installedVersion** | `string` | optional | Currently installed version for quick access | +| **previousVersion** | `string` | optional | Version before the last upgrade | +| **statusChangedAt** | `string` | optional | Status change timestamp | +| **errorMessage** | `string` | optional | Error message when status is error | +| **settings** | `Record` | optional | User-provided configuration settings | +| **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history | +| **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package | + --- @@ -120,6 +154,23 @@ Get package response | :--- | :--- | :--- | :--- | | **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Package details | +### Nested Shape: `GetPackageResponse.package` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Full package manifest | +| **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | +| **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | +| **installedAt** | `string` | optional | Installation timestamp | +| **updatedAt** | `string` | optional | Last update timestamp | +| **installedVersion** | `string` | optional | Currently installed version for quick access | +| **previousVersion** | `string` | optional | Version before the last upgrade | +| **statusChangedAt** | `string` | optional | Status change timestamp | +| **errorMessage** | `string` | optional | Error message when status is error | +| **settings** | `Record` | optional | User-provided configuration settings | +| **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history | +| **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package | + --- @@ -136,6 +187,35 @@ Install package request | **enableOnInstall** | `boolean` | optional (default: `true`) | Whether to enable immediately after install | | **platformVersion** | `string` | optional | Current platform version for compatibility verification | +### Nested Shape: `InstallPackageRequest.manifest` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique package identifier (reverse domain style) | +| **namespace** | `string` | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") | +| **defaultDatasource** | `string` | optional (default: `"default"`) | Default datasource for all objects in this package | +| **version** | `string` | ✅ | Package version (semantic versioning) | +| **type** | `Enum<'plugin' \| 'ui' \| 'driver' \| 'server' \| 'app' \| 'theme' \| 'agent' \| 'objectql' \| …>` | ✅ | Type of package | +| **scope** | `Enum<'cloud' \| 'system' \| 'project'>` | optional (default: `"project"`) | Deployment scope: cloud \| system \| project | +| **name** | `string` | ✅ | Human-readable package name | +| **description** | `string` | optional | Package description | +| **permissions** | `string[] \| { services?: string[]; hooks?: string[]; network?: string[]; fs?: string[] }` | optional | Required permissions: legacy string[] or structured plugin block (ADR-0025 §3.2) | +| **objects** | `string[]` | optional | Glob patterns for ObjectQL schemas files | +| **datasources** | `string[]` | optional | Glob patterns for Datasource definitions | +| **dependencies** | `Record` | optional | Package dependencies | +| **configuration** | `{ title?: string; properties: Record }` | optional | Plugin configuration settings | +| **contributes** | `{ kinds?: object[]; routes?: object[] }` | optional | Platform contributions | +| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Initial seed data (prefer top-level data field) | +| **capabilities** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability | +| **extensions** | `Record` | optional | Extension points and contributions | +| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: (object \| … +8 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages | +| **loading** | `never` | optional | [REMOVED] `manifest.loading` was removed in @objectstack/spec 17.0.0 (#4914, ADR-0049 enforce-or-remove) — the entire block (`strategy`, `preload`, `codeSplitting`, `dynamicImport`, `initialization`, `dependencyResolution`, `hotReload`, `caching`, `sandboxing`, `monitoring`) had no runtime reader in any repo, so authoring it configured nothing. Delete the key. Plugins are composed at boot — `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder`); the set is fixed until the process restarts. ⚠️ `loading.sandboxing` in particular never isolated anything: it did not run plugins in a process, vm, iframe or web-worker, and `allowedServices` gated no call. If you were relying on it for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced. | +| **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | +| **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) | +| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier (ADR-0025 §3.6) | +| **packaging** | `Enum<'bundled' \| 'manifest-deps'>` | optional | Dependency packaging strategy (ADR-0025 §3.3) | +| **integrity** | `Record` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) | + --- @@ -151,6 +231,33 @@ Install package response | **message** | `string` | optional | Installation status message | | **dependencyResolution** | `{ dependencies: object[]; canProceed: boolean; requiredActions: object[]; installOrder: string[]; … }` | optional | Dependency resolution result from install analysis | +### Nested Shape: `InstallPackageResponse.package` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Full package manifest | +| **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | +| **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | +| **installedAt** | `string` | optional | Installation timestamp | +| **updatedAt** | `string` | optional | Last update timestamp | +| **installedVersion** | `string` | optional | Currently installed version for quick access | +| **previousVersion** | `string` | optional | Version before the last upgrade | +| **statusChangedAt** | `string` | optional | Status change timestamp | +| **errorMessage** | `string` | optional | Error message when status is error | +| **settings** | `Record` | optional | User-provided configuration settings | +| **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history | +| **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package | + +### Nested Shape: `InstallPackageResponse.dependencyResolution` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **dependencies** | `{ packageId: string; requiredRange: string; resolvedVersion?: string; installedVersion?: string; … }[]` | ✅ | Resolution result for each dependency | +| **canProceed** | `boolean` | ✅ | Whether installation can proceed | +| **requiredActions** | `{ type: Enum<'install' \| 'upgrade' \| 'confirm_conflict'>; packageId: string; description: string }[]` | ✅ | Actions required before proceeding | +| **installOrder** | `string[]` | ✅ | Topologically sorted package IDs for installation | +| **circularDependencies** | `string[][]` | optional | Circular dependency chains detected (e.g. [["A", "B", "A"]]) | + --- @@ -175,6 +282,45 @@ Installed package with runtime lifecycle state | **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history | | **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package | +### Nested Shape: `InstalledPackage.manifest` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique package identifier (reverse domain style) | +| **namespace** | `string` | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") | +| **defaultDatasource** | `string` | optional (default: `"default"`) | Default datasource for all objects in this package | +| **version** | `string` | ✅ | Package version (semantic versioning) | +| **type** | `Enum<'plugin' \| 'ui' \| 'driver' \| 'server' \| 'app' \| 'theme' \| 'agent' \| 'objectql' \| …>` | ✅ | Type of package | +| **scope** | `Enum<'cloud' \| 'system' \| 'project'>` | optional (default: `"project"`) | Deployment scope: cloud \| system \| project | +| **name** | `string` | ✅ | Human-readable package name | +| **description** | `string` | optional | Package description | +| **permissions** | `string[] \| { services?: string[]; hooks?: string[]; network?: string[]; fs?: string[] }` | optional | Required permissions: legacy string[] or structured plugin block (ADR-0025 §3.2) | +| **objects** | `string[]` | optional | Glob patterns for ObjectQL schemas files | +| **datasources** | `string[]` | optional | Glob patterns for Datasource definitions | +| **dependencies** | `Record` | optional | Package dependencies | +| **configuration** | `{ title?: string; properties: Record }` | optional | Plugin configuration settings | +| **contributes** | `{ kinds?: object[]; routes?: object[] }` | optional | Platform contributions | +| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Initial seed data (prefer top-level data field) | +| **capabilities** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability | +| **extensions** | `Record` | optional | Extension points and contributions | +| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: (object \| … +8 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages | +| **loading** | `never` | optional | [REMOVED] `manifest.loading` was removed in @objectstack/spec 17.0.0 (#4914, ADR-0049 enforce-or-remove) — the entire block (`strategy`, `preload`, `codeSplitting`, `dynamicImport`, `initialization`, `dependencyResolution`, `hotReload`, `caching`, `sandboxing`, `monitoring`) had no runtime reader in any repo, so authoring it configured nothing. Delete the key. Plugins are composed at boot — `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder`); the set is fixed until the process restarts. ⚠️ `loading.sandboxing` in particular never isolated anything: it did not run plugins in a process, vm, iframe or web-worker, and `allowedServices` gated no call. If you were relying on it for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced. | +| **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | +| **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) | +| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier (ADR-0025 §3.6) | +| **packaging** | `Enum<'bundled' \| 'manifest-deps'>` | optional | Dependency packaging strategy (ADR-0025 §3.3) | +| **integrity** | `Record` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) | + +### Nested Shape: `InstalledPackage.upgradeHistory[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **fromVersion** | `string` | ✅ | Version before upgrade | +| **toVersion** | `string` | ✅ | Version after upgrade | +| **upgradedAt** | `string` | ✅ | Upgrade timestamp | +| **status** | `Enum<'success' \| 'failed' \| 'rolled_back'>` | ✅ | Upgrade outcome | +| **migrationLog** | `string[]` | optional | Migration step logs | + --- @@ -204,6 +350,25 @@ List packages response | **packages** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }[]` | ✅ | List of installed packages | | **total** | `number` | ✅ | Total package count | +### Nested Shape: `ListPackagesResponse.packages[number]` + +Installed package with runtime lifecycle state + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Full package manifest | +| **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | +| **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | +| **installedAt** | `string` | optional | Installation timestamp | +| **updatedAt** | `string` | optional | Last update timestamp | +| **installedVersion** | `string` | optional | Currently installed version for quick access | +| **previousVersion** | `string` | optional | Version before the last upgrade | +| **statusChangedAt** | `string` | optional | Status change timestamp | +| **errorMessage** | `string` | optional | Error message when status is error | +| **settings** | `Record` | optional | User-provided configuration settings | +| **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history | +| **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package | + --- diff --git a/content/docs/references/kernel/package-upgrade.mdx b/content/docs/references/kernel/package-upgrade.mdx index 1dd29772e7..65d6fc6b24 100644 --- a/content/docs/references/kernel/package-upgrade.mdx +++ b/content/docs/references/kernel/package-upgrade.mdx @@ -136,6 +136,35 @@ Upgrade package request | **dryRun** | `boolean` | optional (default: `false`) | Preview upgrade without making changes | | **skipValidation** | `boolean` | optional (default: `false`) | Skip pre-upgrade compatibility checks | +### Nested Shape: `UpgradePackageRequest.manifest` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique package identifier (reverse domain style) | +| **namespace** | `string` | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") | +| **defaultDatasource** | `string` | optional (default: `"default"`) | Default datasource for all objects in this package | +| **version** | `string` | ✅ | Package version (semantic versioning) | +| **type** | `Enum<'plugin' \| 'ui' \| 'driver' \| 'server' \| 'app' \| 'theme' \| 'agent' \| 'objectql' \| …>` | ✅ | Type of package | +| **scope** | `Enum<'cloud' \| 'system' \| 'project'>` | optional (default: `"project"`) | Deployment scope: cloud \| system \| project | +| **name** | `string` | ✅ | Human-readable package name | +| **description** | `string` | optional | Package description | +| **permissions** | `string[] \| { services?: string[]; hooks?: string[]; network?: string[]; fs?: string[] }` | optional | Required permissions: legacy string[] or structured plugin block (ADR-0025 §3.2) | +| **objects** | `string[]` | optional | Glob patterns for ObjectQL schemas files | +| **datasources** | `string[]` | optional | Glob patterns for Datasource definitions | +| **dependencies** | `Record` | optional | Package dependencies | +| **configuration** | `{ title?: string; properties: Record }` | optional | Plugin configuration settings | +| **contributes** | `{ kinds?: object[]; routes?: object[] }` | optional | Platform contributions | +| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Initial seed data (prefer top-level data field) | +| **capabilities** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability | +| **extensions** | `Record` | optional | Extension points and contributions | +| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: (object \| … +8 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages | +| **loading** | `never` | optional | [REMOVED] `manifest.loading` was removed in @objectstack/spec 17.0.0 (#4914, ADR-0049 enforce-or-remove) — the entire block (`strategy`, `preload`, `codeSplitting`, `dynamicImport`, `initialization`, `dependencyResolution`, `hotReload`, `caching`, `sandboxing`, `monitoring`) had no runtime reader in any repo, so authoring it configured nothing. Delete the key. Plugins are composed at boot — `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder`); the set is fixed until the process restarts. ⚠️ `loading.sandboxing` in particular never isolated anything: it did not run plugins in a process, vm, iframe or web-worker, and `allowedServices` gated no call. If you were relying on it for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced. | +| **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | +| **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) | +| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier (ADR-0025 §3.6) | +| **packaging** | `Enum<'bundled' \| 'manifest-deps'>` | optional | Dependency packaging strategy (ADR-0025 §3.3) | +| **integrity** | `Record` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) | + --- @@ -155,6 +184,22 @@ Upgrade package response | **errorMessage** | `string` | optional | Error message if upgrade failed | | **message** | `string` | optional | Human-readable status message | +### Nested Shape: `UpgradePackageResponse.plan` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **packageId** | `string` | ✅ | Package identifier | +| **fromVersion** | `string` | ✅ | Currently installed version | +| **toVersion** | `string` | ✅ | Target upgrade version | +| **impactLevel** | `Enum<'none' \| 'low' \| 'medium' \| 'high' \| 'critical'>` | ✅ | Severity assessment from none (seamless) to critical (breaking changes) | +| **changes** | `{ type: string; name: string; changeType: Enum<'added' \| 'modified' \| 'removed' \| 'renamed'>; hasConflict: boolean; … }[]` | ✅ | All metadata changes | +| **affectedCustomizations** | `integer` | optional (default: `0`) | Count of customizations that may be affected | +| **requiresMigration** | `boolean` | optional (default: `false`) | Whether data migration scripts are needed | +| **migrationScripts** | `string[]` | optional | Paths to migration scripts | +| **dependencyUpgrades** | `{ packageId: string; fromVersion: string; toVersion: string }[]` | optional | Dependent packages that also need upgrading | +| **estimatedDuration** | `integer` | optional | Estimated upgrade duration in seconds | +| **summary** | `string` | optional | Human-readable upgrade summary | + --- @@ -198,6 +243,19 @@ Upgrade analysis plan generated before execution | **estimatedDuration** | `integer` | optional | Estimated upgrade duration in seconds | | **summary** | `string` | optional | Human-readable upgrade summary | +### Nested Shape: `UpgradePlan.changes[number]` + +Single metadata change between package versions + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `string` | ✅ | Metadata type | +| **name** | `string` | ✅ | Metadata name | +| **changeType** | `Enum<'added' \| 'modified' \| 'removed' \| 'renamed'>` | ✅ | Category of metadata modification (added, modified, removed, or renamed) | +| **hasConflict** | `boolean` | optional (default: `false`) | Whether this change may conflict with customizations | +| **summary** | `string` | optional | Human-readable change summary | +| **previousName** | `string` | optional | Previous name if renamed | + --- @@ -220,6 +278,35 @@ Pre-upgrade state snapshot for rollback capability | **createdAt** | `string` | ✅ | Snapshot creation timestamp | | **expiresAt** | `string` | optional | Snapshot expiry timestamp | +### Nested Shape: `UpgradeSnapshot.previousManifest` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique package identifier (reverse domain style) | +| **namespace** | `string` | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") | +| **defaultDatasource** | `string` | optional (default: `"default"`) | Default datasource for all objects in this package | +| **version** | `string` | ✅ | Package version (semantic versioning) | +| **type** | `Enum<'plugin' \| 'ui' \| 'driver' \| 'server' \| 'app' \| 'theme' \| 'agent' \| 'objectql' \| …>` | ✅ | Type of package | +| **scope** | `Enum<'cloud' \| 'system' \| 'project'>` | optional (default: `"project"`) | Deployment scope: cloud \| system \| project | +| **name** | `string` | ✅ | Human-readable package name | +| **description** | `string` | optional | Package description | +| **permissions** | `string[] \| { services?: string[]; hooks?: string[]; network?: string[]; fs?: string[] }` | optional | Required permissions: legacy string[] or structured plugin block (ADR-0025 §3.2) | +| **objects** | `string[]` | optional | Glob patterns for ObjectQL schemas files | +| **datasources** | `string[]` | optional | Glob patterns for Datasource definitions | +| **dependencies** | `Record` | optional | Package dependencies | +| **configuration** | `{ title?: string; properties: Record }` | optional | Plugin configuration settings | +| **contributes** | `{ kinds?: object[]; routes?: object[] }` | optional | Platform contributions | +| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Initial seed data (prefer top-level data field) | +| **capabilities** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability | +| **extensions** | `Record` | optional | Extension points and contributions | +| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: (object \| … +8 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages | +| **loading** | `never` | optional | [REMOVED] `manifest.loading` was removed in @objectstack/spec 17.0.0 (#4914, ADR-0049 enforce-or-remove) — the entire block (`strategy`, `preload`, `codeSplitting`, `dynamicImport`, `initialization`, `dependencyResolution`, `hotReload`, `caching`, `sandboxing`, `monitoring`) had no runtime reader in any repo, so authoring it configured nothing. Delete the key. Plugins are composed at boot — `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder`); the set is fixed until the process restarts. ⚠️ `loading.sandboxing` in particular never isolated anything: it did not run plugins in a process, vm, iframe or web-worker, and `allowedServices` gated no call. If you were relying on it for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced. | +| **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | +| **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) | +| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier (ADR-0025 §3.6) | +| **packaging** | `Enum<'bundled' \| 'manifest-deps'>` | optional | Dependency packaging strategy (ADR-0025 §3.3) | +| **integrity** | `Record` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) | + --- diff --git a/content/docs/references/kernel/plugin-capability.mdx b/content/docs/references/kernel/plugin-capability.mdx index 455f7f9e77..0305f80b30 100644 --- a/content/docs/references/kernel/plugin-capability.mdx +++ b/content/docs/references/kernel/plugin-capability.mdx @@ -58,6 +58,14 @@ Level of protocol conformance | **contract** | `{ input?: string; output?: string; signature?: string }` | optional | | | **cardinality** | `Enum<'single' \| 'multiple'>` | optional (default: `"multiple"`) | Whether multiple extensions can register to this point | +### Nested Shape: `ExtensionPoint.contract` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **input** | `string` | optional | Input type/schema | +| **output** | `string` | optional | Output type/schema | +| **signature** | `string` | optional | Function signature if applicable | + --- @@ -75,6 +83,26 @@ Level of protocol conformance | **certified** | `boolean` | optional (default: `false`) | Has passed official conformance tests | | **certificationDate** | `string` | optional | | +### Nested Shape: `PluginCapability.protocol` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique protocol identifier (e.g., com.objectstack.protocol.storage.v1) | +| **label** | `string` | ✅ | | +| **version** | `{ major: integer; minor: integer; patch: integer }` | ✅ | Semantic version of the protocol | +| **specification** | `string` | optional | URL or path to protocol specification | +| **description** | `string` | optional | | + +### Nested Shape: `PluginCapability.features[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Feature identifier within the protocol | +| **enabled** | `boolean` | optional (default: `true`) | | +| **description** | `string` | optional | | +| **sinceVersion** | `string` | optional | Version when this feature was added | +| **deprecatedSince** | `string` | optional | Version when deprecated | + --- @@ -90,6 +118,60 @@ Level of protocol conformance | **extensionPoints** | `{ id: string; name: string; description?: string; type: Enum<'action' \| 'hook' \| 'widget' \| 'provider' \| 'transformer' \| 'validator' \| 'decorator'>; … }[]` | optional | Points where other plugins can extend this plugin | | **extensions** | `{ targetPluginId: string; extensionPointId: string; implementation: string; priority: integer }[]` | optional | Extensions contributed to other plugins | +### Nested Shape: `PluginCapabilityManifest.implements[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **protocol** | `{ id: string; label: string; version: object; specification?: string; … }` | ✅ | | +| **conformance** | `Enum<'full' \| 'partial' \| 'experimental' \| 'deprecated'>` | optional (default: `"full"`) | Level of protocol conformance | +| **implementedFeatures** | `string[]` | optional | List of implemented feature names | +| **features** | `{ name: string; enabled: boolean; description?: string; sinceVersion?: string; … }[]` | optional | | +| **metadata** | `Record` | optional | | +| **certified** | `boolean` | optional (default: `false`) | Has passed official conformance tests | +| **certificationDate** | `string` | optional | | + +### Nested Shape: `PluginCapabilityManifest.provides[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique interface identifier | +| **name** | `string` | ✅ | | +| **description** | `string` | optional | | +| **version** | `{ major: integer; minor: integer; patch: integer }` | ✅ | Semantic version of the protocol | +| **methods** | `{ name: string; description?: string; parameters?: object[]; returnType?: string; … }[]` | ✅ | | +| **events** | `{ name: string; description?: string; payload?: string }[]` | optional | | +| **stability** | `Enum<'stable' \| 'beta' \| 'alpha' \| 'experimental'>` | optional (default: `"stable"`) | | + +### Nested Shape: `PluginCapabilityManifest.requires[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **pluginId** | `string` | ✅ | Required plugin identifier | +| **version** | `string` | ✅ | Semantic version constraint | +| **optional** | `boolean` | optional (default: `false`) | | +| **reason** | `string` | optional | | +| **requiredCapabilities** | `string[]` | optional | Protocol IDs the dependency must support | + +### Nested Shape: `PluginCapabilityManifest.extensionPoints[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique extension point identifier | +| **name** | `string` | ✅ | | +| **description** | `string` | optional | | +| **type** | `Enum<'action' \| 'hook' \| 'widget' \| 'provider' \| 'transformer' \| 'validator' \| 'decorator'>` | ✅ | | +| **contract** | `{ input?: string; output?: string; signature?: string }` | optional | | +| **cardinality** | `Enum<'single' \| 'multiple'>` | optional (default: `"multiple"`) | Whether multiple extensions can register to this point | + +### Nested Shape: `PluginCapabilityManifest.extensions[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **targetPluginId** | `string` | ✅ | Plugin ID being extended | +| **extensionPointId** | `string` | ✅ | Extension point identifier | +| **implementation** | `string` | ✅ | Path to implementation module | +| **priority** | `integer` | optional (default: `100`) | Registration priority (lower = higher priority) | + --- @@ -122,6 +204,24 @@ Level of protocol conformance | **events** | `{ name: string; description?: string; payload?: string }[]` | optional | | | **stability** | `Enum<'stable' \| 'beta' \| 'alpha' \| 'experimental'>` | optional (default: `"stable"`) | | +### Nested Shape: `PluginInterface.methods[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Method name | +| **description** | `string` | optional | | +| **parameters** | `{ name: string; type: string; required: boolean; description?: string }[]` | optional | | +| **returnType** | `string` | optional | Return value type | +| **async** | `boolean` | optional (default: `false`) | Whether method returns a Promise | + +### Nested Shape: `PluginInterface.events[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Event name | +| **description** | `string` | optional | | +| **payload** | `string` | optional | Event payload type | + --- diff --git a/content/docs/references/kernel/plugin-lifecycle-advanced.mdx b/content/docs/references/kernel/plugin-lifecycle-advanced.mdx index ab01704421..4c0a5e9ec9 100644 --- a/content/docs/references/kernel/plugin-lifecycle-advanced.mdx +++ b/content/docs/references/kernel/plugin-lifecycle-advanced.mdx @@ -45,6 +45,62 @@ const result = AdvancedPluginLifecycleConfigSchema.parse(data); | **resources** | `{ maxMemory?: integer; maxCpu?: number; maxConnections?: integer; timeout?: integer }` | optional | | | **observability** | `{ enableMetrics: boolean; enableTracing: boolean; enableProfiling: boolean; metricsInterval: integer }` | optional | | +### Nested Shape: `AdvancedPluginLifecycleConfig.health` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **interval** | `integer` | optional (default: `30000`) | How often to perform health checks (default: 30s) | +| **timeout** | `integer` | optional (default: `5000`) | Maximum time to wait for health check response | +| **failureThreshold** | `integer` | optional (default: `3`) | Consecutive failures needed to mark unhealthy | +| **successThreshold** | `integer` | optional (default: `1`) | Consecutive successes needed to mark healthy | +| **checkMethod** | `string` | optional | Method name to call for health check | +| **autoRestart** | `boolean` | optional (default: `false`) | Automatically restart plugin on health check failure | +| **maxRestartAttempts** | `integer` | optional (default: `3`) | Maximum restart attempts before giving up | +| **restartBackoff** | `Enum<'fixed' \| 'linear' \| 'exponential'>` | optional (default: `"exponential"`) | Backoff strategy for restart delays | + +### Nested Shape: `AdvancedPluginLifecycleConfig.hotReload` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `false`) | | +| **watchPatterns** | `string[]` | optional | Glob patterns to watch for changes | +| **debounceDelay** | `integer` | optional (default: `1000`) | Wait time after change detection before reload | +| **preserveState** | `boolean` | optional (default: `true`) | Keep plugin state across reloads | +| **stateStrategy** | `Enum<'memory' \| 'disk' \| 'distributed' \| 'none'>` | optional (default: `"memory"`) | How to preserve state during reload | +| **distributedConfig** | `{ provider: Enum<'redis' \| 'etcd' \| 'custom'>; endpoints?: string[]; keyPrefix?: string; ttl?: integer; … }` | optional | Configuration for distributed state management | +| **shutdownTimeout** | `integer` | optional (default: `30000`) | Maximum time to wait for graceful shutdown | +| **beforeReload** | `string[]` | optional | Hook names to call before reload | +| **afterReload** | `string[]` | optional | Hook names to call after reload | + +### Nested Shape: `AdvancedPluginLifecycleConfig.degradation` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | | +| **fallbackMode** | `Enum<'minimal' \| 'cached' \| 'readonly' \| 'offline' \| 'disabled'>` | optional (default: `"minimal"`) | | +| **criticalDependencies** | `string[]` | optional | Plugin IDs that are required for operation | +| **optionalDependencies** | `string[]` | optional | Plugin IDs that are nice to have but not required | +| **degradedFeatures** | `{ feature: string; enabled: boolean; reason?: string }[]` | optional | | +| **autoRecovery** | `{ enabled: boolean; retryInterval: integer; maxAttempts: integer }` | optional | | + +### Nested Shape: `AdvancedPluginLifecycleConfig.resources` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **maxMemory** | `integer` | optional | Maximum memory in bytes | +| **maxCpu** | `number` | optional | Maximum CPU percentage | +| **maxConnections** | `integer` | optional | Maximum concurrent connections | +| **timeout** | `integer` | optional | Operation timeout in milliseconds | + +### Nested Shape: `AdvancedPluginLifecycleConfig.observability` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enableMetrics** | `boolean` | optional (default: `true`) | | +| **enableTracing** | `boolean` | optional (default: `true`) | | +| **enableProfiling** | `boolean` | optional (default: `false`) | | +| **metricsInterval** | `integer` | optional (default: `60000`) | Metrics collection interval in ms | + --- @@ -78,6 +134,22 @@ const result = AdvancedPluginLifecycleConfigSchema.parse(data); | **degradedFeatures** | `{ feature: string; enabled: boolean; reason?: string }[]` | optional | | | **autoRecovery** | `{ enabled: boolean; retryInterval: integer; maxAttempts: integer }` | optional | | +### Nested Shape: `GracefulDegradation.degradedFeatures[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **feature** | `string` | ✅ | Feature name | +| **enabled** | `boolean` | ✅ | Whether feature is available in degraded mode | +| **reason** | `string` | optional | | + +### Nested Shape: `GracefulDegradation.autoRecovery` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | | +| **retryInterval** | `integer` | optional (default: `60000`) | Interval between recovery attempts (ms) | +| **maxAttempts** | `integer` | optional (default: `5`) | Maximum recovery attempts before giving up | + --- @@ -97,6 +169,18 @@ const result = AdvancedPluginLifecycleConfigSchema.parse(data); | **beforeReload** | `string[]` | optional | Hook names to call before reload | | **afterReload** | `string[]` | optional | Hook names to call after reload | +### Nested Shape: `HotReloadConfig.distributedConfig` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **provider** | `Enum<'redis' \| 'etcd' \| 'custom'>` | ✅ | Distributed state backend provider | +| **endpoints** | `string[]` | optional | Backend connection endpoints | +| **keyPrefix** | `string` | optional | Prefix for all keys (e.g., "plugin:my-plugin:") | +| **ttl** | `integer` | optional | State expiration time in seconds | +| **auth** | `{ username?: string; password?: string; token?: string; certificate?: string }` | optional | | +| **replication** | `{ enabled: boolean; minReplicas: integer }` | optional | | +| **customConfig** | `Record` | optional | Provider-specific configuration | + --- @@ -131,6 +215,34 @@ const result = AdvancedPluginLifecycleConfigSchema.parse(data); | **checks** | `{ name: string; status: Enum<'passed' \| 'failed' \| 'warning'>; message?: string; data?: Record }[]` | optional | | | **dependencies** | `{ pluginId: string; status: Enum<'healthy' \| 'degraded' \| 'unhealthy' \| 'failed' \| 'recovering' \| 'unknown'>; message?: string }[]` | optional | | +### Nested Shape: `PluginHealthReport.metrics` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **uptime** | `number` | optional | Plugin uptime in milliseconds | +| **memoryUsage** | `number` | optional | Memory usage in bytes | +| **cpuUsage** | `number` | optional | CPU usage percentage | +| **activeConnections** | `number` | optional | Number of active connections | +| **errorRate** | `number` | optional | Error rate (errors per minute) | +| **responseTime** | `number` | optional | Average response time in ms | + +### Nested Shape: `PluginHealthReport.checks[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Check name | +| **status** | `Enum<'passed' \| 'failed' \| 'warning'>` | ✅ | | +| **message** | `string` | optional | | +| **data** | `Record` | optional | | + +### Nested Shape: `PluginHealthReport.dependencies[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **pluginId** | `string` | ✅ | | +| **status** | `Enum<'healthy' \| 'degraded' \| 'unhealthy' \| 'failed' \| 'recovering' \| 'unknown'>` | ✅ | Current health status of the plugin | +| **message** | `string` | optional | | + --- @@ -162,6 +274,14 @@ Current health status of the plugin | **state** | `Record` | ✅ | | | **metadata** | `{ checksum?: string; compressed: boolean; encryption?: string }` | optional | | +### Nested Shape: `PluginStateSnapshot.metadata` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **checksum** | `string` | optional | State checksum for verification | +| **compressed** | `boolean` | optional (default: `false`) | | +| **encryption** | `string` | optional | Encryption algorithm if encrypted | + --- @@ -177,6 +297,14 @@ Current health status of the plugin | **rollback** | `{ enabled: boolean; automatic: boolean; keepVersions: integer; timeout: integer }` | optional | | | **validation** | `{ checkCompatibility: boolean; runTests: boolean; testSuite?: string }` | optional | | +### Nested Shape: `PluginUpdateStrategy.autoUpdateConstraints` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **major** | `boolean` | optional (default: `false`) | Allow major version updates | +| **minor** | `boolean` | optional (default: `true`) | Allow minor version updates | +| **patch** | `boolean` | optional (default: `true`) | Allow patch version updates | + --- diff --git a/content/docs/references/kernel/plugin-registry.mdx b/content/docs/references/kernel/plugin-registry.mdx index 903f1fb0a6..5e48238505 100644 --- a/content/docs/references/kernel/plugin-registry.mdx +++ b/content/docs/references/kernel/plugin-registry.mdx @@ -54,6 +54,16 @@ const result = PluginInstallConfigSchema.parse(data); | **securityScan** | `{ lastScanDate?: string; vulnerabilities?: object; passed: boolean }` | optional | | | **conformanceTests** | `{ protocolId: string; passed: boolean; totalTests: integer; passedTests: integer; … }[]` | optional | | +### Nested Shape: `PluginQualityMetrics.conformanceTests[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **protocolId** | `string` | ✅ | Protocol being tested | +| **passed** | `boolean` | ✅ | | +| **totalTests** | `integer` | ✅ | | +| **passedTests** | `integer` | ✅ | | +| **lastRunDate** | `string` | optional | | + --- @@ -86,6 +96,27 @@ const result = PluginInstallConfigSchema.parse(data); | **replacedBy** | `string` | optional | Plugin ID that replaces this one | | **flags** | `{ experimental: boolean; beta: boolean; featured: boolean; verified: boolean }` | optional | | +### Nested Shape: `PluginRegistryEntry.vendor` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Vendor identifier (reverse domain) | +| **name** | `string` | ✅ | | +| **website** | `string` | optional | | +| **email** | `string` | optional | | +| **verified** | `boolean` | optional (default: `false`) | Whether vendor is verified by ObjectStack | +| **trustLevel** | `Enum<'official' \| 'verified' \| 'community' \| 'unverified'>` | optional (default: `"unverified"`) | | + +### Nested Shape: `PluginRegistryEntry.capabilities` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **implements** | `{ protocol: object; conformance: Enum<'full' \| 'partial' \| 'experimental' \| 'deprecated'>; implementedFeatures?: string[]; features?: object[]; … }[]` | optional | List of protocols this plugin conforms to | +| **provides** | `{ id: string; name: string; description?: string; version: object; … }[]` | optional | Services/APIs this plugin offers to others | +| **requires** | `{ pluginId: string; version: string; optional: boolean; reason?: string; … }[]` | optional | Required plugins and their capabilities | +| **extensionPoints** | `{ id: string; name: string; description?: string; type: Enum<'action' \| 'hook' \| 'widget' \| 'provider' \| 'transformer' \| 'validator' \| 'decorator'>; … }[]` | optional | Points where other plugins can extend this plugin | +| **extensions** | `{ targetPluginId: string; extensionPointId: string; implementation: string; priority: integer }[]` | optional | Extensions contributed to other plugins | + --- diff --git a/content/docs/references/kernel/plugin-security-advanced.mdx b/content/docs/references/kernel/plugin-security-advanced.mdx index b60a0193f1..6b66de7d41 100644 --- a/content/docs/references/kernel/plugin-security-advanced.mdx +++ b/content/docs/references/kernel/plugin-security-advanced.mdx @@ -46,6 +46,40 @@ const result = KernelSecurityPolicySchema.parse(data); | **encryption** | `{ dataAtRest: boolean; dataInTransit: boolean; algorithm?: string; minKeyLength?: integer }` | optional | | | **auditLog** | `{ enabled: boolean; events?: string[]; retention?: integer }` | optional | | +### Nested Shape: `KernelSecurityPolicy.rateLimit` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | | +| **maxRequests** | `integer` | ✅ | | +| **windowMs** | `integer` | ✅ | Time window in milliseconds | +| **strategy** | `Enum<'fixed' \| 'sliding' \| 'token-bucket'>` | optional (default: `"sliding"`) | | + +### Nested Shape: `KernelSecurityPolicy.authentication` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **required** | `boolean` | optional (default: `true`) | | +| **methods** | `Enum<'jwt' \| 'oauth2' \| 'api-key' \| 'session' \| 'certificate'>[]` | ✅ | | +| **tokenExpiration** | `integer` | optional | Token expiration in seconds | + +### Nested Shape: `KernelSecurityPolicy.encryption` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **dataAtRest** | `boolean` | optional (default: `false`) | Encrypt data at rest | +| **dataInTransit** | `boolean` | optional (default: `true`) | Enforce HTTPS/TLS | +| **algorithm** | `string` | optional | Encryption algorithm | +| **minKeyLength** | `integer` | optional | Minimum key length in bits | + +### Nested Shape: `KernelSecurityPolicy.auditLog` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | | +| **events** | `string[]` | optional | Events to log | +| **retention** | `integer` | optional | Log retention in days | + --- @@ -64,6 +98,17 @@ const result = KernelSecurityPolicySchema.parse(data); | **licenseCompliance** | `{ status: Enum<'compliant' \| 'non-compliant' \| 'unknown'>; issues?: object[] }` | optional | | | **summary** | `{ totalVulnerabilities: integer; criticalCount: integer; highCount: integer; mediumCount: integer; … }` | ✅ | | +### Nested Shape: `KernelSecurityScanResult.codeIssues[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | ✅ | | +| **type** | `string` | ✅ | Issue type (e.g., sql-injection, xss) | +| **file** | `string` | ✅ | | +| **line** | `integer` | optional | | +| **message** | `string` | ✅ | | +| **suggestion** | `string` | optional | | + --- @@ -164,6 +209,14 @@ Scope of permission application * `process.spawn` * `process.env` +### Nested Shape: `PluginPermission.filter` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **resourceIds** | `string[]` | optional | | +| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) filter, e.g. P`record.owner == os.user.id`. | +| **fields** | `string[]` | optional | Allowed fields for data resources | + --- @@ -177,6 +230,27 @@ Scope of permission application | **groups** | `{ name: string; description: string; permissions: string[] }[]` | optional | | | **defaultGrant** | `Enum<'prompt' \| 'allow' \| 'deny' \| 'inherit'>` | optional (default: `"prompt"`) | | +### Nested Shape: `PluginPermissionSet.permissions[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique permission identifier | +| **resource** | `Enum<'data.object' \| 'data.record' \| 'data.field' \| 'ui.view' \| 'ui.dashboard' \| …>` | ✅ | Type of resource being accessed | +| **actions** | `Enum<'create' \| 'read' \| 'update' \| 'delete' \| 'execute' \| 'manage' \| 'configure' \| …>[]` | ✅ | | +| **scope** | `Enum<'global' \| 'tenant' \| 'user' \| 'resource' \| 'plugin'>` | optional (default: `"plugin"`) | Scope of permission application | +| **filter** | `{ resourceIds?: string[]; condition?: string \| object; fields?: string[] }` | optional | | +| **description** | `string` | ✅ | | +| **required** | `boolean` | optional (default: `true`) | | +| **justification** | `string` | optional | Why this permission is needed | + +### Nested Shape: `PluginPermissionSet.groups[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Group name | +| **description** | `string` | ✅ | | +| **permissions** | `string[]` | ✅ | Permission IDs in this group | + --- @@ -198,6 +272,38 @@ Scope of permission application | **securityContact** | `{ email?: string; url?: string; pgpKey?: string }` | optional | | | **vulnerabilityDisclosure** | `{ policyUrl?: string; responseTime?: integer; bugBounty?: boolean }` | optional | | +### Nested Shape: `PluginSecurityManifest.sandbox` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | | +| **level** | `Enum<'none' \| 'minimal' \| 'standard' \| 'strict' \| 'paranoid'>` | optional (default: `"standard"`) | | +| **runtime** | `{ engine?: Enum<'v8-isolate' \| 'wasm' \| 'container' \| 'process'>; engineConfig?: object; resourceLimits?: object }` | optional | Execution environment and isolation settings | +| **filesystem** | `{ mode?: Enum<'none' \| 'readonly' \| 'restricted' \| 'full'>; allowedPaths?: string[]; deniedPaths?: string[]; maxFileSize?: integer }` | optional | | +| **network** | `{ mode?: Enum<'none' \| 'local' \| 'restricted' \| 'full'>; allowedHosts?: string[]; deniedHosts?: string[]; allowedPorts?: number[]; … }` | optional | | +| **process** | `{ allowSpawn?: boolean; allowedCommands?: string[]; timeout?: integer }` | optional | | +| **memory** | `{ maxHeap?: integer; maxStack?: integer }` | optional | | +| **cpu** | `{ maxCpuPercent?: number; maxThreads?: integer }` | optional | | +| **environment** | `{ mode?: Enum<'none' \| 'readonly' \| 'restricted' \| 'full'>; allowedVars?: string[]; deniedVars?: string[] }` | optional | | + +### Nested Shape: `PluginSecurityManifest.certifications[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Certification name (e.g., SOC 2, ISO 27001) | +| **issuer** | `string` | ✅ | | +| **issuedDate** | `string` | ✅ | | +| **expiryDate** | `string` | optional | | +| **certificateUrl** | `string` | optional | | + +### Nested Shape: `PluginSecurityManifest.vulnerabilityDisclosure` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **policyUrl** | `string` | optional | | +| **responseTime** | `integer` | optional | Expected response time in hours | +| **bugBounty** | `boolean` | optional (default: `false`) | | + --- @@ -252,6 +358,14 @@ Type of resource being accessed | **engineConfig** | `{ wasm?: object; container?: object; v8Isolate?: object }` | optional | | | **resourceLimits** | `{ maxMemory?: integer; maxCpu?: number; timeout?: integer }` | optional | | +### Nested Shape: `RuntimeConfig.resourceLimits` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **maxMemory** | `integer` | optional | Maximum memory allocation | +| **maxCpu** | `number` | optional | Maximum CPU usage percentage | +| **timeout** | `integer` | optional | Maximum execution time | + --- @@ -271,6 +385,48 @@ Type of resource being accessed | **cpu** | `{ maxCpuPercent?: number; maxThreads?: integer }` | optional | | | **environment** | `{ mode: Enum<'none' \| 'readonly' \| 'restricted' \| 'full'>; allowedVars?: string[]; deniedVars?: string[] }` | optional | | +### Nested Shape: `SandboxConfig.runtime` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **engine** | `Enum<'v8-isolate' \| 'wasm' \| 'container' \| 'process'>` | optional (default: `"v8-isolate"`) | Execution environment engine | +| **engineConfig** | `{ wasm?: object; container?: object; v8Isolate?: object }` | optional | | +| **resourceLimits** | `{ maxMemory?: integer; maxCpu?: number; timeout?: integer }` | optional | | + +### Nested Shape: `SandboxConfig.filesystem` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **mode** | `Enum<'none' \| 'readonly' \| 'restricted' \| 'full'>` | optional (default: `"restricted"`) | | +| **allowedPaths** | `string[]` | optional | Whitelisted paths | +| **deniedPaths** | `string[]` | optional | Blacklisted paths | +| **maxFileSize** | `integer` | optional | Maximum file size in bytes | + +### Nested Shape: `SandboxConfig.network` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **mode** | `Enum<'none' \| 'local' \| 'restricted' \| 'full'>` | optional (default: `"restricted"`) | | +| **allowedHosts** | `string[]` | optional | Whitelisted hosts | +| **deniedHosts** | `string[]` | optional | Blacklisted hosts | +| **allowedPorts** | `number[]` | optional | Allowed port numbers | +| **maxConnections** | `integer` | optional | | + +### Nested Shape: `SandboxConfig.process` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **allowSpawn** | `boolean` | optional (default: `false`) | Allow spawning child processes | +| **allowedCommands** | `string[]` | optional | Whitelisted commands | +| **timeout** | `integer` | optional | Process timeout in ms | + +### Nested Shape: `SandboxConfig.memory` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **maxHeap** | `integer` | optional | Maximum heap size in bytes | +| **maxStack** | `integer` | optional | Maximum stack size in bytes | + --- diff --git a/content/docs/references/kernel/plugin-security.mdx b/content/docs/references/kernel/plugin-security.mdx index adb983cd74..e8d6b81807 100644 --- a/content/docs/references/kernel/plugin-security.mdx +++ b/content/docs/references/kernel/plugin-security.mdx @@ -47,6 +47,42 @@ Complete dependency graph for a package and its transitive dependencies | **edges** | `{ from: string; to: string; constraint: string }[]` | ✅ | Directed edges representing dependency relationships | | **stats** | `{ totalDependencies: integer; directDependencies: integer; maxDepth: integer }` | ✅ | Summary statistics for the dependency graph | +### Nested Shape: `DependencyGraph.root` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Identifier of the root package | +| **version** | `string` | ✅ | Version of the root package | + +### Nested Shape: `DependencyGraph.nodes[number]` + +A node in the dependency graph representing a resolved package + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique identifier of the package | +| **version** | `string` | ✅ | Resolved version of the package | +| **dependencies** | `{ name: string; versionConstraint: string; type: Enum<'required' \| 'optional' \| 'peer' \| 'dev'>; resolvedVersion?: string }[]` | optional (default: `[]`) | Dependencies required by this package | +| **depth** | `integer` | ✅ | Depth level in the dependency tree (0 = root) | +| **isDirect** | `boolean` | ✅ | Whether this is a direct (top-level) dependency | +| **metadata** | `{ name: string; description?: string; license?: string; homepage?: string }` | optional | Additional metadata about the package | + +### Nested Shape: `DependencyGraph.edges[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **from** | `string` | ✅ | Package ID | +| **to** | `string` | ✅ | Package ID | +| **constraint** | `string` | ✅ | Version constraint | + +### Nested Shape: `DependencyGraph.stats` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **totalDependencies** | `integer` | ✅ | Total number of resolved dependencies | +| **directDependencies** | `integer` | ✅ | Number of direct (top-level) dependencies | +| **maxDepth** | `integer` | ✅ | Maximum depth of the dependency tree | + --- @@ -65,6 +101,26 @@ A node in the dependency graph representing a resolved package | **isDirect** | `boolean` | ✅ | Whether this is a direct (top-level) dependency | | **metadata** | `{ name: string; description?: string; license?: string; homepage?: string }` | optional | Additional metadata about the package | +### Nested Shape: `DependencyGraphNode.dependencies[number]` + +A resolver-side package dependency: version constraint plus its resolution outcome + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Package name or identifier | +| **versionConstraint** | `string` | ✅ | Semver range (e.g., `^1.0.0`, `>=2.0.0 <3.0.0`) | +| **type** | `Enum<'required' \| 'optional' \| 'peer' \| 'dev'>` | optional (default: `"required"`) | Category of the dependency relationship | +| **resolvedVersion** | `string` | optional | Concrete version resolved during dependency resolution | + +### Nested Shape: `DependencyGraphNode.metadata` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Display name of the package | +| **description** | `string` | optional | Short description of the package | +| **license** | `string` | optional | SPDX license identifier of the package | +| **homepage** | `string` | optional | Homepage URL of the package | + --- @@ -81,6 +137,22 @@ A detected conflict between dependency version requirements | **resolution** | `{ strategy: Enum<'pick-highest' \| 'pick-lowest' \| 'manual'>; version?: string; reason?: string }` | optional | Suggested resolution for the conflict | | **severity** | `Enum<'error' \| 'warning' \| 'info'>` | ✅ | Severity level of the dependency conflict | +### Nested Shape: `PackageDependencyConflict.conflicts[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **version** | `string` | ✅ | Conflicting version of the package | +| **requestedBy** | `string[]` | ✅ | Packages that require this version | +| **constraint** | `string` | ✅ | Semver constraint that produced this version requirement | + +### Nested Shape: `PackageDependencyConflict.resolution` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **strategy** | `Enum<'pick-highest' \| 'pick-lowest' \| 'manual'>` | ✅ | Strategy used to resolve the conflict | +| **version** | `string` | optional | Resolved version selected by the strategy | +| **reason** | `string` | optional | Explanation of why this resolution was chosen | + --- @@ -99,6 +171,33 @@ Result of a dependency resolution process | **installOrder** | `string[]` | optional (default: `[]`) | Topologically sorted list of package IDs for installation | | **resolvedIn** | `integer` | optional | Time taken to resolve dependencies in milliseconds | +### Nested Shape: `PackageDependencyResolutionResult.graph` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **root** | `{ id: string; version: string }` | ✅ | Root package of the dependency graph | +| **nodes** | `{ id: string; version: string; dependencies: object[]; depth: integer; … }[]` | ✅ | All resolved package nodes in the dependency graph | +| **edges** | `{ from: string; to: string; constraint: string }[]` | ✅ | Directed edges representing dependency relationships | +| **stats** | `{ totalDependencies: integer; directDependencies: integer; maxDepth: integer }` | ✅ | Summary statistics for the dependency graph | + +### Nested Shape: `PackageDependencyResolutionResult.conflicts[number]` + +A detected conflict between dependency version requirements + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **package** | `string` | ✅ | Name of the package with conflicting version requirements | +| **conflicts** | `{ version: string; requestedBy: string[]; constraint: string }[]` | ✅ | List of conflicting version requirements | +| **resolution** | `{ strategy: Enum<'pick-highest' \| 'pick-lowest' \| 'manual'>; version?: string; reason?: string }` | optional | Suggested resolution for the conflict | +| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | ✅ | Severity level of the dependency conflict | + +### Nested Shape: `PackageDependencyResolutionResult.errors[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **package** | `string` | ✅ | Name of the package that caused the error | +| **error** | `string` | ✅ | Error message describing what went wrong | + --- @@ -117,6 +216,42 @@ Verifiable provenance and chain of custody for a plugin artifact | **signatures** | `{ algorithm: Enum<'rsa' \| 'ecdsa' \| 'ed25519'>; publicKey: string; signature: string; signedBy: string; … }[]` | optional (default: `[]`) | Cryptographic signatures for the plugin artifact | | **attestations** | `{ type: Enum<'code-review' \| 'security-scan' \| 'test-results' \| 'ci-build'>; status: Enum<'passed' \| 'failed'>; url?: string; timestamp: string }[]` | optional (default: `[]`) | Verification attestations for the plugin | +### Nested Shape: `PluginProvenance.build` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **timestamp** | `string` | ✅ | ISO 8601 timestamp when the build was produced | +| **environment** | `{ os: string; arch: string; nodeVersion: string }` | optional | Environment details where the build was executed | +| **source** | `{ repository: string; commit: string; branch?: string; tag?: string }` | optional | Source repository information for the build | +| **builder** | `{ name: string; email?: string }` | optional | Identity of the builder who produced the artifact | + +### Nested Shape: `PluginProvenance.artifacts[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **filename** | `string` | ✅ | Name of the artifact file | +| **sha256** | `string` | ✅ | SHA-256 hash of the artifact | +| **size** | `integer` | ✅ | Size of the artifact in bytes | + +### Nested Shape: `PluginProvenance.signatures[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **algorithm** | `Enum<'rsa' \| 'ecdsa' \| 'ed25519'>` | ✅ | Cryptographic algorithm used for signing | +| **publicKey** | `string` | ✅ | Public key used to verify the signature | +| **signature** | `string` | ✅ | Digital signature value | +| **signedBy** | `string` | ✅ | Identity of the signer | +| **timestamp** | `string` | ✅ | ISO 8601 timestamp when the signature was created | + +### Nested Shape: `PluginProvenance.attestations[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'code-review' \| 'security-scan' \| 'test-results' \| 'ci-build'>` | ✅ | Type of attestation | +| **status** | `Enum<'passed' \| 'failed'>` | ✅ | Result status of the attestation | +| **url** | `string` | optional | URL with details about the attestation | +| **timestamp** | `string` | ✅ | ISO 8601 timestamp when the attestation was issued | + --- @@ -135,6 +270,16 @@ Trust score and verification status for a plugin | **badges** | `Enum<'official' \| 'verified-vendor' \| 'security-scanned' \| 'code-signed' \| 'open-source' \| 'popular'>[]` | optional (default: `[]`) | Verification badges earned by the plugin | | **updatedAt** | `string` | ✅ | ISO 8601 timestamp when the trust score was last updated | +### Nested Shape: `PluginTrustScore.components` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **vendorReputation** | `number` | ✅ | Vendor reputation score from 0 to 100 | +| **securityScore** | `number` | ✅ | Security scan results score from 0 to 100 | +| **codeQuality** | `number` | ✅ | Code quality score from 0 to 100 | +| **communityScore** | `number` | ✅ | Community engagement score from 0 to 100 | +| **maintenanceScore** | `number` | ✅ | Maintenance and update frequency score from 0 to 100 | + --- @@ -169,6 +314,35 @@ Software Bill of Materials for a plugin | **generatedAt** | `string` | ✅ | ISO 8601 timestamp when the SBOM was generated | | **generator** | `{ name: string; version: string }` | optional | Tool used to generate this SBOM | +### Nested Shape: `SBOM.plugin` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Plugin identifier | +| **version** | `string` | ✅ | Plugin version | +| **name** | `string` | ✅ | Human-readable plugin name | + +### Nested Shape: `SBOM.components[number]` + +A single entry in a Software Bill of Materials + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Name of the software component | +| **version** | `string` | ✅ | Version of the software component | +| **purl** | `string` | optional | Package URL identifier | +| **license** | `string` | optional | SPDX license identifier of the component | +| **hashes** | `{ sha256?: string; sha512?: string }` | optional | Cryptographic hashes for integrity verification | +| **supplier** | `{ name: string; url?: string }` | optional | Supplier information for the component | +| **externalRefs** | `{ type: Enum<'website' \| 'repository' \| 'documentation' \| 'issue-tracker'>; url: string }[]` | optional (default: `[]`) | External references related to the component | + +### Nested Shape: `SBOM.generator` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Name of the SBOM generator tool | +| **version** | `string` | ✅ | Version of the SBOM generator tool | + --- @@ -188,6 +362,27 @@ A single entry in a Software Bill of Materials | **supplier** | `{ name: string; url?: string }` | optional | Supplier information for the component | | **externalRefs** | `{ type: Enum<'website' \| 'repository' \| 'documentation' \| 'issue-tracker'>; url: string }[]` | optional (default: `[]`) | External references related to the component | +### Nested Shape: `SBOMEntry.hashes` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **sha256** | `string` | optional | SHA-256 hash of the component artifact | +| **sha512** | `string` | optional | SHA-512 hash of the component artifact | + +### Nested Shape: `SBOMEntry.supplier` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Name of the component supplier | +| **url** | `string` | optional | URL of the component supplier | + +### Nested Shape: `SBOMEntry.externalRefs[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'website' \| 'repository' \| 'documentation' \| 'issue-tracker'>` | ✅ | Type of external reference | +| **url** | `string` | ✅ | URL of the external reference | + --- @@ -208,6 +403,38 @@ Security policy governing plugin scanning and enforcement | **codeSigning** | `{ required: boolean; allowedSigners: string[] }` | optional | Code signing requirements for plugin artifacts | | **sandbox** | `{ networkAccess: Enum<'none' \| 'localhost' \| 'allowlist' \| 'all'>; allowedDestinations: string[]; filesystemAccess: Enum<'none' \| 'read-only' \| 'temp-only' \| 'full'>; maxMemoryMB?: integer; … }` | optional | Sandbox restrictions for plugin execution | +### Nested Shape: `SecurityPolicy.autoScan` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | Whether automatic scanning is enabled | +| **frequency** | `Enum<'on-publish' \| 'daily' \| 'weekly' \| 'monthly'>` | optional (default: `"daily"`) | How often automatic scans are performed | + +### Nested Shape: `SecurityPolicy.thresholds` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **maxCritical** | `integer` | optional (default: `0`) | Maximum allowed critical vulnerabilities before blocking | +| **maxHigh** | `integer` | optional (default: `0`) | Maximum allowed high vulnerabilities before blocking | +| **maxMedium** | `integer` | optional (default: `5`) | Maximum allowed medium vulnerabilities before warning | + +### Nested Shape: `SecurityPolicy.codeSigning` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **required** | `boolean` | optional (default: `false`) | Whether code signing is required for plugins | +| **allowedSigners** | `string[]` | optional (default: `[]`) | List of trusted signer identities | + +### Nested Shape: `SecurityPolicy.sandbox` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **networkAccess** | `Enum<'none' \| 'localhost' \| 'allowlist' \| 'all'>` | optional (default: `"all"`) | Level of network access granted to the plugin | +| **allowedDestinations** | `string[]` | optional (default: `[]`) | Permitted network destinations when using allowlist mode | +| **filesystemAccess** | `Enum<'none' \| 'read-only' \| 'temp-only' \| 'full'>` | optional (default: `"full"`) | Level of file system access granted to the plugin | +| **maxMemoryMB** | `integer` | optional | Maximum memory allocation in megabytes | +| **maxCPUSeconds** | `integer` | optional | Maximum CPU time allowed in seconds | + --- @@ -230,6 +457,67 @@ Result of a security scan performed on a plugin | **codeQuality** | `{ score?: number; issues: object[] }` | optional | Code quality analysis results | | **nextScanAt** | `string` | optional | ISO 8601 timestamp for the next scheduled scan | +### Nested Shape: `SecurityScanResult.plugin` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Plugin identifier | +| **version** | `string` | ✅ | Plugin version that was scanned | + +### Nested Shape: `SecurityScanResult.scanner` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Scanner name (e.g., snyk, osv, trivy) | +| **version** | `string` | ✅ | Version of the scanner tool | + +### Nested Shape: `SecurityScanResult.vulnerabilities[number]` + +A known security vulnerability in a package dependency + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **cve** | `string` | optional | CVE identifier | +| **id** | `string` | ✅ | Vulnerability ID | +| **title** | `string` | ✅ | Short title summarizing the vulnerability | +| **description** | `string` | ✅ | Detailed description of the vulnerability | +| **severity** | `Enum<'critical' \| 'high' \| 'medium' \| 'low' \| 'info'>` | ✅ | Severity level of this vulnerability | +| **cvss** | `number` | optional | CVSS score ranging from 0 to 10 | +| **package** | `{ name: string; version: string; ecosystem?: string }` | ✅ | Affected package information | +| **vulnerableVersions** | `string` | ✅ | Semver range of vulnerable versions | +| **patchedVersions** | `string` | optional | Semver range of patched versions | +| **references** | `{ type: Enum<'advisory' \| 'article' \| 'report' \| 'web'>; url: string }[]` | optional (default: `[]`) | External references related to the vulnerability | +| **cwe** | `string[]` | optional (default: `[]`) | CWE identifiers associated with this vulnerability | +| **publishedAt** | `string` | optional | ISO 8601 date when the vulnerability was published | +| **mitigation** | `string` | optional | Recommended steps to mitigate the vulnerability | + +### Nested Shape: `SecurityScanResult.summary` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **critical** | `integer` | optional (default: `0`) | Count of critical severity vulnerabilities | +| **high** | `integer` | optional (default: `0`) | Count of high severity vulnerabilities | +| **medium** | `integer` | optional (default: `0`) | Count of medium severity vulnerabilities | +| **low** | `integer` | optional (default: `0`) | Count of low severity vulnerabilities | +| **info** | `integer` | optional (default: `0`) | Count of informational severity vulnerabilities | +| **total** | `integer` | optional (default: `0`) | Total count of all vulnerabilities | + +### Nested Shape: `SecurityScanResult.licenseIssues[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **package** | `string` | ✅ | Name of the package with a license issue | +| **license** | `string` | ✅ | License identifier of the package | +| **reason** | `string` | ✅ | Reason the license is flagged | +| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | ✅ | Severity of the license compliance issue | + +### Nested Shape: `SecurityScanResult.codeQuality` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **score** | `number` | optional | Overall code quality score from 0 to 100 | +| **issues** | `{ type: Enum<'security' \| 'quality' \| 'style'>; severity: Enum<'error' \| 'warning' \| 'info'>; message: string; file?: string; … }[]` | optional (default: `[]`) | List of individual code quality issues | + --- @@ -255,6 +543,21 @@ A known security vulnerability in a package dependency | **publishedAt** | `string` | optional | ISO 8601 date when the vulnerability was published | | **mitigation** | `string` | optional | Recommended steps to mitigate the vulnerability | +### Nested Shape: `SecurityVulnerability.package` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Name of the affected package | +| **version** | `string` | ✅ | Version of the affected package | +| **ecosystem** | `string` | optional | Package ecosystem (e.g., npm, pip, maven) | + +### Nested Shape: `SecurityVulnerability.references[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'advisory' \| 'article' \| 'report' \| 'web'>` | ✅ | Type of reference source | +| **url** | `string` | ✅ | URL of the reference | + --- diff --git a/content/docs/references/kernel/plugin-validator.mdx b/content/docs/references/kernel/plugin-validator.mdx index d68b23aa2d..6e5d5a7bdf 100644 --- a/content/docs/references/kernel/plugin-validator.mdx +++ b/content/docs/references/kernel/plugin-validator.mdx @@ -71,6 +71,22 @@ Plugin metadata for validation | **errors** | `{ field: string; message: string; code?: string }[]` | optional | Validation errors | | **warnings** | `{ field: string; message: string; code?: string }[]` | optional | Validation warnings | +### Nested Shape: `ValidationResult.errors[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name that failed validation | +| **message** | `string` | ✅ | Human-readable error message | +| **code** | `string` | optional | Machine-readable error code | + +### Nested Shape: `ValidationResult.warnings[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name with warning | +| **message** | `string` | ✅ | Human-readable warning message | +| **code** | `string` | optional | Machine-readable warning code | + --- diff --git a/content/docs/references/kernel/plugin-versioning.mdx b/content/docs/references/kernel/plugin-versioning.mdx index 9dce0cf402..92a5c5e441 100644 --- a/content/docs/references/kernel/plugin-versioning.mdx +++ b/content/docs/references/kernel/plugin-versioning.mdx @@ -81,6 +81,19 @@ Compatibility level between versions | **migrationScript** | `string` | optional | Path to migration script | | **testCoverage** | `number` | optional | Percentage of migration covered by tests | +### Nested Shape: `CompatibilityMatrixEntry.breakingChanges[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **introducedIn** | `string` | ✅ | Version that introduced this breaking change | +| **type** | `Enum<'api-removed' \| 'api-renamed' \| 'api-signature-changed' \| 'behavior-changed' \| …>` | ✅ | | +| **description** | `string` | ✅ | | +| **migrationGuide** | `string` | optional | How to migrate from old to new | +| **deprecatedIn** | `string` | optional | Version where old API was deprecated | +| **removedIn** | `string` | optional | Version where old API will be removed | +| **automatedMigration** | `boolean` | optional (default: `false`) | Whether automated migration tool is available | +| **severity** | `Enum<'critical' \| 'major' \| 'minor'>` | ✅ | Impact severity | + --- @@ -96,6 +109,14 @@ Compatibility level between versions | **resolutions** | `{ strategy: Enum<'upgrade' \| 'downgrade' \| 'replace' \| 'disable' \| 'manual'>; description: string; automaticResolution: boolean; riskLevel: Enum<'low' \| 'medium' \| 'high'> }[]` | optional | | | **severity** | `Enum<'critical' \| 'error' \| 'warning' \| 'info'>` | ✅ | | +### Nested Shape: `DependencyConflict.plugins[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **pluginId** | `string` | ✅ | | +| **version** | `string` | ✅ | | +| **requirement** | `string` | optional | What this plugin requires | + --- @@ -127,6 +148,23 @@ Compatibility level between versions | **routing** | `{ condition: string \| object; version: string; priority?: integer }[]` | optional | | | **rollout** | `{ enabled?: boolean; strategy: Enum<'percentage' \| 'blue-green' \| 'canary'>; percentage?: number; duration?: integer }` | optional | | +### Nested Shape: `MultiVersionSupport.routing[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | ✅ | Routing predicate (CEL). | +| **version** | `string` | ✅ | Version to use when condition matches | +| **priority** | `integer` | optional (default: `100`) | Rule priority | + +### Nested Shape: `MultiVersionSupport.rollout` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `false`) | | +| **strategy** | `Enum<'percentage' \| 'blue-green' \| 'canary'>` | ✅ | | +| **percentage** | `number` | optional | Percentage of traffic to new version | +| **duration** | `integer` | optional | Rollout duration in milliseconds | + --- @@ -142,6 +180,29 @@ Compatibility level between versions | **supportedVersions** | `{ version: string; supported: boolean; endOfLife?: string; securitySupport: boolean }[]` | ✅ | | | **minimumCompatibleVersion** | `string` | optional | Oldest version that can be directly upgraded | +### Nested Shape: `PluginCompatibilityMatrix.compatibilityMatrix[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **from** | `string` | ✅ | Version being upgraded from | +| **to** | `string` | ✅ | Version being upgraded to | +| **compatibility** | `Enum<'fully-compatible' \| 'backward-compatible' \| 'deprecated-compatible' \| …>` | ✅ | Compatibility level between versions | +| **breakingChanges** | `{ introducedIn: string; type: Enum<'api-removed' \| 'api-renamed' \| 'api-signature-changed' \| 'behavior-changed' \| …>; description: string; migrationGuide?: string; … }[]` | optional | | +| **migrationRequired** | `boolean` | optional (default: `false`) | | +| **migrationComplexity** | `Enum<'trivial' \| 'simple' \| 'moderate' \| 'complex' \| 'major'>` | optional | | +| **estimatedMigrationTime** | `number` | optional | | +| **migrationScript** | `string` | optional | Path to migration script | +| **testCoverage** | `number` | optional | Percentage of migration covered by tests | + +### Nested Shape: `PluginCompatibilityMatrix.supportedVersions[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **version** | `string` | ✅ | | +| **supported** | `boolean` | ✅ | | +| **endOfLife** | `string` | optional | End of support date | +| **securitySupport** | `boolean` | optional (default: `false`) | Still receives security updates | + --- @@ -179,6 +240,63 @@ Compatibility level between versions | **statistics** | `{ downloads?: integer; installations?: integer; ratings?: number }` | optional | | | **support** | `{ status: Enum<'active' \| 'maintenance' \| 'deprecated' \| 'eol'>; endOfLife?: string; securitySupport: boolean }` | ✅ | | +### Nested Shape: `PluginVersionMetadata.version` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **major** | `integer` | ✅ | Major version (breaking changes) | +| **minor** | `integer` | ✅ | Minor version (backward compatible features) | +| **patch** | `integer` | ✅ | Patch version (backward compatible fixes) | +| **preRelease** | `string` | optional | Pre-release identifier (alpha, beta, rc.1) | +| **build** | `string` | optional | Build metadata | + +### Nested Shape: `PluginVersionMetadata.breakingChanges[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **introducedIn** | `string` | ✅ | Version that introduced this breaking change | +| **type** | `Enum<'api-removed' \| 'api-renamed' \| 'api-signature-changed' \| 'behavior-changed' \| …>` | ✅ | | +| **description** | `string` | ✅ | | +| **migrationGuide** | `string` | optional | How to migrate from old to new | +| **deprecatedIn** | `string` | optional | Version where old API was deprecated | +| **removedIn** | `string` | optional | Version where old API will be removed | +| **automatedMigration** | `boolean` | optional (default: `false`) | Whether automated migration tool is available | +| **severity** | `Enum<'critical' \| 'major' \| 'minor'>` | ✅ | Impact severity | + +### Nested Shape: `PluginVersionMetadata.deprecations[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **feature** | `string` | ✅ | Deprecated feature identifier | +| **deprecatedIn** | `string` | ✅ | | +| **removeIn** | `string` | optional | | +| **reason** | `string` | ✅ | | +| **alternative** | `string` | optional | What to use instead | +| **migrationPath** | `string` | optional | How to migrate to alternative | + +### Nested Shape: `PluginVersionMetadata.compatibilityMatrix[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **from** | `string` | ✅ | Version being upgraded from | +| **to** | `string` | ✅ | Version being upgraded to | +| **compatibility** | `Enum<'fully-compatible' \| 'backward-compatible' \| 'deprecated-compatible' \| …>` | ✅ | Compatibility level between versions | +| **breakingChanges** | `{ introducedIn: string; type: Enum<'api-removed' \| 'api-renamed' \| 'api-signature-changed' \| 'behavior-changed' \| …>; description: string; migrationGuide?: string; … }[]` | optional | | +| **migrationRequired** | `boolean` | optional (default: `false`) | | +| **migrationComplexity** | `Enum<'trivial' \| 'simple' \| 'moderate' \| 'complex' \| 'major'>` | optional | | +| **estimatedMigrationTime** | `number` | optional | | +| **migrationScript** | `string` | optional | Path to migration script | +| **testCoverage** | `number` | optional | Percentage of migration covered by tests | + +### Nested Shape: `PluginVersionMetadata.securityFixes[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **cve** | `string` | optional | CVE identifier | +| **severity** | `Enum<'critical' \| 'high' \| 'medium' \| 'low'>` | ✅ | | +| **description** | `string` | ✅ | | +| **fixedIn** | `string` | ✅ | Version where vulnerability was fixed | + --- diff --git a/content/docs/references/kernel/service-registry.mdx b/content/docs/references/kernel/service-registry.mdx index 0ba7a5dda3..368424aa59 100644 --- a/content/docs/references/kernel/service-registry.mdx +++ b/content/docs/references/kernel/service-registry.mdx @@ -73,6 +73,14 @@ const result = ScopeConfigSchema.parse(data); | **singleton** | `boolean` | optional (default: `true`) | Whether to cache the factory result (singleton pattern) | | **cluster** | `{ clusterScope: Enum<'node' \| 'cluster'>; leaderStrategy?: Enum<'leader-elected' \| 'partitioned' \| 'idempotent-broadcast'>; clusterId?: string }` | optional | Cluster scope & leader strategy for this service. | +### Nested Shape: `ServiceFactoryRegistration.cluster` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **clusterScope** | `Enum<'node' \| 'cluster'>` | optional (default: `"node"`) | Per-node vs cluster-singleton presence. | +| **leaderStrategy** | `Enum<'leader-elected' \| 'partitioned' \| 'idempotent-broadcast'>` | optional | How the cluster-singleton invariant is maintained. | +| **clusterId** | `string` | optional | Logical cluster identity used for leader election (defaults to service name). | + --- @@ -89,6 +97,14 @@ const result = ScopeConfigSchema.parse(data); | **metadata** | `Record` | optional | Additional service-specific metadata | | **cluster** | `{ clusterScope: Enum<'node' \| 'cluster'>; leaderStrategy?: Enum<'leader-elected' \| 'partitioned' \| 'idempotent-broadcast'>; clusterId?: string }` | optional | Cluster scope & leader strategy. See cluster-semantics.mdx §5. | +### Nested Shape: `ServiceMetadata.cluster` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **clusterScope** | `Enum<'node' \| 'cluster'>` | optional (default: `"node"`) | Per-node vs cluster-singleton presence. | +| **leaderStrategy** | `Enum<'leader-elected' \| 'partitioned' \| 'idempotent-broadcast'>` | optional | How the cluster-singleton invariant is maintained. | +| **clusterId** | `string` | optional | Logical cluster identity used for leader election (defaults to service name). | + --- diff --git a/content/docs/references/kernel/startup-orchestrator.mdx b/content/docs/references/kernel/startup-orchestrator.mdx index 6db6cb62f4..356a354f61 100644 --- a/content/docs/references/kernel/startup-orchestrator.mdx +++ b/content/docs/references/kernel/startup-orchestrator.mdx @@ -55,6 +55,24 @@ const result = HealthStatusSchema.parse(data); | **error** | `{ name: string; message: string; stack?: string; code?: string }` | optional | Serializable error representation if startup failed | | **health** | `{ healthy: boolean; timestamp: integer; details?: Record; message?: string }` | optional | Health status after startup if health check was enabled | +### Nested Shape: `PluginStartupResult.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Error class name | +| **message** | `string` | ✅ | Error message | +| **stack** | `string` | optional | Stack trace | +| **code** | `string` | optional | Error code | + +### Nested Shape: `PluginStartupResult.health` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **healthy** | `boolean` | ✅ | Whether the plugin is healthy | +| **timestamp** | `integer` | ✅ | Unix timestamp in milliseconds when health check was performed | +| **details** | `Record` | optional | Optional plugin-specific health details | +| **message** | `string` | optional | Error message if plugin is unhealthy | + --- @@ -84,6 +102,16 @@ const result = HealthStatusSchema.parse(data); | **allSuccessful** | `boolean` | ✅ | Whether all plugins started successfully | | **rolledBack** | `string[]` | optional | Names of plugins that were rolled back | +### Nested Shape: `StartupOrchestrationResult.results[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **plugin** | `{ name: string; version?: string } & Record` | ✅ | Plugin metadata | +| **success** | `boolean` | ✅ | Whether the plugin started successfully | +| **duration** | `number` | ✅ | Time taken to start the plugin in milliseconds | +| **error** | `{ name: string; message: string; stack?: string; code?: string }` | optional | Serializable error representation if startup failed | +| **health** | `{ healthy: boolean; timestamp: integer; details?: Record; message?: string }` | optional | Health status after startup if health check was enabled | + --- diff --git a/content/docs/references/qa/testing.mdx b/content/docs/references/qa/testing.mdx index e51f46cddb..36c0dbfcb6 100644 --- a/content/docs/references/qa/testing.mdx +++ b/content/docs/references/qa/testing.mdx @@ -117,6 +117,49 @@ A complete test scenario with setup, execution steps, and teardown | **teardown** | `{ name: string; description?: string; action: object; assertions?: object[]; … }[]` | optional | Steps to cleanup after test execution | | **requires** | `{ params?: string[]; plugins?: string[] }` | optional | Environment requirements for this scenario | +### Nested Shape: `TestScenario.setup[number]` + +A single step in a test scenario, consisting of an action and optional assertions + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Step name for identification in test reports | +| **description** | `string` | optional | Human-readable description of what this step tests | +| **action** | `{ type: Enum<'create_record' \| 'update_record' \| 'delete_record' \| 'read_record' \| …>; target: string; payload?: Record; user?: string }` | ✅ | The action to execute in this step | +| **assertions** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'is_null' \| 'not_null' \| …>; expectedValue: any }[]` | optional | Assertions to validate after the action completes | +| **capture** | `Record` | optional | Map result fields to context variables, paths resolved against the response body root (e.g. `{ "newId": "data.id" }`) | + +### Nested Shape: `TestScenario.steps[number]` + +A single step in a test scenario, consisting of an action and optional assertions + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Step name for identification in test reports | +| **description** | `string` | optional | Human-readable description of what this step tests | +| **action** | `{ type: Enum<'create_record' \| 'update_record' \| 'delete_record' \| 'read_record' \| …>; target: string; payload?: Record; user?: string }` | ✅ | The action to execute in this step | +| **assertions** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'is_null' \| 'not_null' \| …>; expectedValue: any }[]` | optional | Assertions to validate after the action completes | +| **capture** | `Record` | optional | Map result fields to context variables, paths resolved against the response body root (e.g. `{ "newId": "data.id" }`) | + +### Nested Shape: `TestScenario.teardown[number]` + +A single step in a test scenario, consisting of an action and optional assertions + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Step name for identification in test reports | +| **description** | `string` | optional | Human-readable description of what this step tests | +| **action** | `{ type: Enum<'create_record' \| 'update_record' \| 'delete_record' \| 'read_record' \| …>; target: string; payload?: Record; user?: string }` | ✅ | The action to execute in this step | +| **assertions** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'is_null' \| 'not_null' \| …>; expectedValue: any }[]` | optional | Assertions to validate after the action completes | +| **capture** | `Record` | optional | Map result fields to context variables, paths resolved against the response body root (e.g. `{ "newId": "data.id" }`) | + +### Nested Shape: `TestScenario.requires` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **params** | `string[]` | optional | Required environment variables or parameters | +| **plugins** | `string[]` | optional | Required plugins that must be loaded | + --- @@ -134,6 +177,25 @@ A single step in a test scenario, consisting of an action and optional assertion | **assertions** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'is_null' \| 'not_null' \| …>; expectedValue: any }[]` | optional | Assertions to validate after the action completes | | **capture** | `Record` | optional | Map result fields to context variables, paths resolved against the response body root (e.g. `{ "newId": "data.id" }`) | +### Nested Shape: `TestStep.action` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'create_record' \| 'update_record' \| 'delete_record' \| 'read_record' \| …>` | ✅ | The action type to execute | +| **target** | `string` | ✅ | Target Object, API Endpoint, or Function Name | +| **payload** | `Record` | optional | Data to send or use | +| **user** | `string` | optional | Run as specific user/role for impersonation testing | + +### Nested Shape: `TestStep.assertions[number]` + +A test assertion that validates the result of a test action + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field path in the result to check, resolved against the parsed response body root — no "body." prefix (e.g. "data.0.status") | +| **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'is_null' \| 'not_null' \| …>` | ✅ | Comparison operator to use | +| **expectedValue** | `any` | ✅ | Expected value to compare against | + --- @@ -148,6 +210,21 @@ A collection of test scenarios grouped into a test suite | **name** | `string` | ✅ | Test suite name | | **scenarios** | `{ id: string; name: string; description?: string; tags?: string[]; … }[]` | ✅ | List of test scenarios in this suite | +### Nested Shape: `TestSuite.scenarios[number]` + +A complete test scenario with setup, execution steps, and teardown + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique scenario identifier | +| **name** | `string` | ✅ | Scenario name for test reports | +| **description** | `string` | optional | Detailed description of the test scenario | +| **tags** | `string[]` | optional | Tags for filtering and categorization (e.g. "critical", "regression", "crm") | +| **setup** | `{ name: string; description?: string; action: object; assertions?: object[]; … }[]` | optional | Steps to run before main test (preconditions) | +| **steps** | `{ name: string; description?: string; action: object; assertions?: object[]; … }[]` | ✅ | Main test sequence to execute | +| **teardown** | `{ name: string; description?: string; action: object; assertions?: object[]; … }[]` | optional | Steps to cleanup after test execution | +| **requires** | `{ params?: string[]; plugins?: string[] }` | optional | Environment requirements for this scenario | + --- diff --git a/content/docs/references/security/explain.mdx b/content/docs/references/security/explain.mdx index c63b4d2ad7..85d6d7bfb7 100644 --- a/content/docs/references/security/explain.mdx +++ b/content/docs/references/security/explain.mdx @@ -110,6 +110,44 @@ ADR-0095 D2 posture rung — PLATFORM_ADMIN crosses the tenant wall where object | **record** | `{ recordId: string; visible: boolean; decidedBy?: Enum<'tenant_isolation' \| 'principal' \| 'required_permissions' \| 'object_crud' \| …> }` | optional | Row-level verdict for the specific record; set only for singular record-grained requests. | | **records** | `{ recordId: string; visible: boolean; decidedBy?: Enum<'tenant_isolation' \| 'principal' \| 'required_permissions' \| 'object_crud' \| …> }[]` | optional | Per-record verdicts for a batch request — records[i] answers recordIds[i]; set only when the request carried recordIds. | +### Nested Shape: `ExplainDecision.principal` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **userId** | `string \| null` | ✅ | | +| **positions** | `string[]` | optional (default: `[]`) | | +| **permissionSets** | `string[]` | optional (default: `[]`) | | +| **principalKind** | `Enum<'human' \| 'agent' \| 'service' \| 'guest' \| 'system'>` | optional | | +| **onBehalfOf** | `{ userId: string }` | optional | | +| **posture** | `Enum<'PLATFORM_ADMIN' \| 'TENANT_ADMIN' \| 'MEMBER' \| 'EXTERNAL'>` | optional | ADR-0095 D2 posture rung — PLATFORM_ADMIN crosses the tenant wall where object posture permits; TENANT_ADMIN sees all rows in the org; MEMBER gets business RLS; EXTERNAL sees only explicitly shared rows. | + +### Nested Shape: `ExplainDecision.layers[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **layer** | `Enum<'tenant_isolation' \| 'principal' \| 'required_permissions' \| 'object_crud' \| …>` | ✅ | | +| **kernelTier** | `Enum<'layer_0_tenant' \| 'layer_1_business'>` | optional | ADR-0095 kernel layer: layer_0_tenant = the always-first org wall; layer_1_business = business RLS/sharing/ownership. | +| **verdict** | `Enum<'grants' \| 'denies' \| 'narrows' \| 'widens' \| 'neutral' \| 'not_applicable'>` | ✅ | | +| **detail** | `string` | ✅ | | +| **contributors** | `{ kind: Enum<'permission_set' \| 'position' \| 'system'>; name: string; via?: string; state?: Enum<'active' \| 'expired' \| 'deactivated'> }[]` | optional (default: `[]`) | | +| **record** | `{ outcome: Enum<'admitted' \| 'excluded' \| 'not_evaluated'>; rowFilter?: any; matchesRecord?: boolean; rules: object[]; … }` | optional | Row-level determination for the specific record under explanation; set only for record-grained requests. | + +### Nested Shape: `ExplainDecision.record` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **recordId** | `string` | ✅ | The concrete record id this verdict is about (echoes the request recordId or recordIds[i]). | +| **visible** | `boolean` | ✅ | Whether the operation is permitted on this specific record after all layers. | +| **decidedBy** | `Enum<'tenant_isolation' \| 'principal' \| 'required_permissions' \| 'object_crud' \| …>` | optional | The pipeline layer that decided the record-level outcome (excluded it, or last admitted it); omitted for a missing record. | + +### Nested Shape: `ExplainDecision.records[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **recordId** | `string` | ✅ | The concrete record id this verdict is about (echoes the request recordId or recordIds[i]). | +| **visible** | `boolean` | ✅ | Whether the operation is permitted on this specific record after all layers. | +| **decidedBy** | `Enum<'tenant_isolation' \| 'principal' \| 'required_permissions' \| 'object_crud' \| …>` | optional | The pipeline layer that decided the record-level outcome (excluded it, or last admitted it); omitted for a missing record. | + --- @@ -126,6 +164,16 @@ ADR-0095 D2 posture rung — PLATFORM_ADMIN crosses the tenant wall where object | **contributors** | `{ kind: Enum<'permission_set' \| 'position' \| 'system'>; name: string; via?: string; state?: Enum<'active' \| 'expired' \| 'deactivated'> }[]` | optional (default: `[]`) | | | **record** | `{ outcome: Enum<'admitted' \| 'excluded' \| 'not_evaluated'>; rowFilter?: any; matchesRecord?: boolean; rules: object[]; … }` | optional | Row-level determination for the specific record under explanation; set only for record-grained requests. | +### Nested Shape: `ExplainLayer.record` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **outcome** | `Enum<'admitted' \| 'excluded' \| 'not_evaluated'>` | ✅ | This layer's row-level outcome for the record: admitted, excluded, or not_evaluated (skipped/not row-scoped). | +| **rowFilter** | `any` | optional | The effective row predicate this layer contributed for the record set (null = unrestricted, __deny_all__ = zero rows). | +| **matchesRecord** | `boolean` | optional | Whether the specific record satisfies rowFilter — the judgement behind outcome. | +| **rules** | `{ kind: Enum<'tenant_filter' \| 'owd_baseline' \| 'ownership' \| 'record_share' \| 'sharing_rule' \| …>; name: string; grants?: Enum<'read' \| 'edit' \| 'full'>; via?: string; … }[]` | optional (default: `[]`) | Concrete rules, shares, or policies this layer evaluated against the record, in evaluation order. | +| **detail** | `string` | optional | Human-readable, record-specific explanation of this layer's outcome. | + --- @@ -173,6 +221,17 @@ ADR-0095 D2 posture rung — PLATFORM_ADMIN crosses the tenant wall where object | **rules** | `{ kind: Enum<'tenant_filter' \| 'owd_baseline' \| 'ownership' \| 'record_share' \| 'sharing_rule' \| …>; name: string; grants?: Enum<'read' \| 'edit' \| 'full'>; via?: string; … }[]` | optional (default: `[]`) | Concrete rules, shares, or policies this layer evaluated against the record, in evaluation order. | | **detail** | `string` | optional | Human-readable, record-specific explanation of this layer's outcome. | +### Nested Shape: `ExplainRecordAttribution.rules[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **kind** | `Enum<'tenant_filter' \| 'owd_baseline' \| 'ownership' \| 'record_share' \| 'sharing_rule' \| …>` | ✅ | The row-visibility source kind evaluated for this record at this layer. | +| **name** | `string` | ✅ | Stable identifier of the concrete rule, share, or policy that was evaluated. | +| **grants** | `Enum<'read' \| 'edit' \| 'full'>` | optional | Access level a sharing source grants on the record (authorable: read/edit; `full` appears only for legacy rows pending normalisation). | +| **via** | `string` | optional | How the rule reached the principal — recipient group/position, ownership, or the matching criteria. | +| **predicate** | `any` | optional | The row predicate this rule contributed, when it is filter-shaped (null = unrestricted). | +| **effect** | `Enum<'admits' \| 'excludes' \| 'neutral'>` | ✅ | The rule's effect on THIS record: admits, excludes, or neutral. | + --- diff --git a/content/docs/references/security/permission.mdx b/content/docs/references/security/permission.mdx index eae6f0e2c9..bba81b716d 100644 --- a/content/docs/references/security/permission.mdx +++ b/content/docs/references/security/permission.mdx @@ -142,6 +142,65 @@ const result = AdminScopeSchema.parse(data); | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `PermissionSet.objects[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **allowCreate** | `boolean` | optional (default: `false`) | Create permission | +| **allowRead** | `boolean` | optional (default: `false`) | Read permission | +| **allowEdit** | `boolean` | optional (default: `false`) | Edit permission | +| **allowDelete** | `boolean` | optional (default: `false`) | Delete permission | +| **allowExport** | `boolean` | optional | [#3544] User-level export axis over read (opt-in grant). true = export granted (still bounded by read); unset/false = no export. Merged most-permissively like the CRUD bits; NOT implied by viewAllRecords/modifyAllRecords. | +| **allowTransfer** | `boolean` | optional (default: `false`) | [RBAC-gated; ENFORCED now via insert/update owner_id guard, #3004] Change record ownership (assign/reassign/disown owner_id) | +| **allowRestore** | `boolean` | optional (default: `false`) | [RBAC-gated; operation pending M2] Restore from trash (Undelete) | +| **allowPurge** | `boolean` | optional (default: `false`) | [RBAC-gated; operation pending M2] Permanently delete (Hard Delete/GDPR) | +| **viewAllRecords** | `boolean` | optional (default: `false`) | View All Data (Bypass Sharing) | +| **modifyAllRecords** | `boolean` | optional (default: `false`) | Modify All Data (Bypass Sharing) — bypasses sharing rules and ownership on the objects record sharing enforces on; on an object with NO owner field sharing abstains, so the platform created_by write floor still applies (#6698). | +| **readScope** | `Enum<'own' \| 'own_and_reports' \| 'unit' \| 'unit_and_below' \| 'org'>` | optional | [ADR-0057 D1] Read depth: own\|unit\|unit_and_below\|org | +| **writeScope** | `Enum<'own' \| 'own_and_reports' \| 'unit' \| 'unit_and_below' \| 'org'>` | optional | [ADR-0057 D1] Write depth: own\|unit\|unit_and_below\|org | + +### Nested Shape: `PermissionSet.fields[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **readable** | `boolean` | optional (default: `true`) | Field read access | +| **editable** | `boolean` | optional (default: `false`) | Field edit access | + +### Nested Shape: `PermissionSet.rowLevelSecurity[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Policy unique identifier (snake_case) | +| **label** | `string` | optional | Human-readable policy label | +| **description** | `string` | optional | Policy description and business justification | +| **object** | `string` | ✅ | Target object name | +| **operation** | `Enum<'select' \| 'insert' \| 'update' \| 'delete' \| 'all'>` | ✅ | Database operation this policy applies to | +| **using** | `string` | optional | Filter condition for SELECT/UPDATE/DELETE, authored in canonical CEL (ADR-0058 D1). It enforces when the predicate lowers to an ObjectQL filter: a field compared against a literal or a `current_user.*` context value using `==`, `!=`, `<`, `<=`, `>` or `>=`; `in` against a `current_user.*` array or an inline literal list (e.g. status in ['draft', 'pending']); these combined with `&&` / `\|\|`; or the bare allow-all `true`. Anything that does not lower fails closed — the policy matches zero rows. The legacy SQL-ish spellings are still accepted through a transitional bridge that rewrites `=` to `==` and `IN` to `in` (deprecated under ADR-0058 D1); SQL `AND` / `OR` / `NOT IN` / `IS NULL` / `LIKE` are NOT bridged and fail closed. Optional for INSERT-only policies. | +| **check** | `string` | optional | Validation condition for INSERT/UPDATE (defaults to USING clause if not specified - enforced at application level) | +| **positions** | `string[]` | optional | Positions this policy applies to (omit for all) | +| **enabled** | `boolean` | optional (default: `true`) | Whether this policy is active | +| **priority** | `never` | optional | [REMOVED] `rowLevelSecurity[].priority` was removed in @objectstack/spec 17.0.0 (#3896 security audit). It never had an effect and could not: applicable policies OR-combine (most permissive wins), so there is no conflict to order. Delete the key — policy outcomes are unchanged. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **tags** | `string[]` | optional | Policy categorization tags | + +### Nested Shape: `PermissionSet.adminScope` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **businessUnit** | `string` | ✅ | [ADR-0090 D12] Delegation boundary: sys_business_unit.name of the subtree root | +| **includeSubtree** | `boolean` | optional (default: `true`) | Cover descendant business units too (default true) | +| **manageAssignments** | `boolean` | optional (default: `false`) | Manage user↔position assignments within the subtree | +| **manageBindings** | `boolean` | optional (default: `false`) | Manage position↔permission-set bindings within the subtree | +| **authorEnvironmentSets** | `boolean` | optional (default: `false`) | Author environment-owned permission sets | +| **assignablePermissionSets** | `string[]` | optional (default: `[]`) | Allowlist of permission-set names the delegate may hand out | + +### Nested Shape: `PermissionSet.protection` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | ✅ | Lock policy — none \| no-overlay \| no-delete \| full. | +| **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | +| **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | + --- diff --git a/content/docs/references/security/sharing.mdx b/content/docs/references/security/sharing.mdx index 08f1b53c88..d441a9c2b0 100644 --- a/content/docs/references/security/sharing.mdx +++ b/content/docs/references/security/sharing.mdx @@ -47,6 +47,13 @@ const result = CriteriaSharingRuleSchema.parse(data); | **type** | `'criteria'` | ✅ | | | **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL). e.g. P`record.department == "Sales"` | +### Nested Shape: `CriteriaSharingRule.sharedWith` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'user' \| 'team' \| 'position' \| 'unit_and_subordinates' \| 'business_unit'>` | ✅ | | +| **value** | `string` | ✅ | ID or code of the recipient (user / team / position / business unit) | + --- @@ -108,6 +115,13 @@ const result = CriteriaSharingRuleSchema.parse(data); | **type** | `'criteria'` | ✅ | | | **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL). e.g. P`record.department == "Sales"` | +### Nested Shape: `SharingRule.sharedWith` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'user' \| 'team' \| 'position' \| 'unit_and_subordinates' \| 'business_unit'>` | ✅ | | +| **value** | `string` | ✅ | ID or code of the recipient (user / team / position / business unit) | + --- diff --git a/content/docs/references/studio/flow-builder.mdx b/content/docs/references/studio/flow-builder.mdx index 3da51176e4..128133543d 100644 --- a/content/docs/references/studio/flow-builder.mdx +++ b/content/docs/references/studio/flow-builder.mdx @@ -70,6 +70,40 @@ Studio Flow Builder configuration | **animateExecution** | `boolean` | optional (default: `true`) | Animate edges during execution preview | | **connectionValidation** | `boolean` | optional (default: `true`) | Validate connections before creating edges | +### Nested Shape: `FlowBuilderConfig.snap` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | Enable snap-to-grid | +| **gridSize** | `integer` | optional (default: `16`) | Snap grid size in pixels | +| **showGrid** | `boolean` | optional (default: `true`) | Show grid overlay | + +### Nested Shape: `FlowBuilderConfig.zoom` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **min** | `number` | optional (default: `0.25`) | Minimum zoom level | +| **max** | `number` | optional (default: `3`) | Maximum zoom level | +| **default** | `number` | optional (default: `1`) | Default zoom level | +| **step** | `number` | optional (default: `0.1`) | Zoom step | + +### Nested Shape: `FlowBuilderConfig.nodeDescriptors[number]` + +Visual render descriptor for a flow node type + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **action** | `string` | ✅ | FlowNodeAction value (e.g., "parallel_gateway") | +| **shape** | `Enum<'rounded_rect' \| 'circle' \| 'diamond' \| 'parallelogram' \| 'hexagon' \| …>` | ✅ | Shape to render | +| **icon** | `string` | ✅ | Lucide icon name | +| **defaultLabel** | `string` | ✅ | Default display label | +| **defaultWidth** | `integer` | optional (default: `120`) | Default width in pixels | +| **defaultHeight** | `integer` | optional (default: `60`) | Default height in pixels | +| **fillColor** | `string` | optional (default: `"#ffffff"`) | Node fill color (CSS value) | +| **borderColor** | `string` | optional (default: `"#94a3b8"`) | Node border color (CSS value) | +| **allowBoundaryEvents** | `boolean` | optional (default: `false`) | Whether boundary events can be attached to this node type | +| **paletteCategory** | `Enum<'event' \| 'gateway' \| 'activity' \| 'data' \| 'subflow'>` | ✅ | Palette category for grouping | + --- @@ -88,6 +122,13 @@ Canvas layout and visual data for a flow edge | **waypoints** | `{ x: number; y: number }[]` | optional | Manual waypoints for edge routing | | **animated** | `boolean` | optional (default: `false`) | Show animated flow indicator | +### Nested Shape: `FlowCanvasEdge.waypoints[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **x** | `number` | ✅ | Waypoint X | +| **y** | `number` | ✅ | Waypoint Y | + --- diff --git a/content/docs/references/studio/object-designer.mdx b/content/docs/references/studio/object-designer.mdx index b773f44118..896f7804d1 100644 --- a/content/docs/references/studio/object-designer.mdx +++ b/content/docs/references/studio/object-designer.mdx @@ -99,6 +99,18 @@ const result = ERDiagramConfigSchema.parse(data); | **autoFit** | `boolean` | optional (default: `true`) | Auto-fit diagram to viewport on load | | **exportFormats** | `Enum<'png' \| 'svg' \| 'json'>[]` | optional (default: `["png","svg"]`) | Available export formats for diagram | +### Nested Shape: `ERDiagramConfig.nodeDisplay` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **showFields** | `boolean` | optional (default: `true`) | Show field list inside entity nodes | +| **maxFieldsVisible** | `number` | optional (default: `8`) | Max fields visible before "N more..." collapse | +| **showFieldTypes** | `boolean` | optional (default: `true`) | Show field type badges | +| **showRequiredIndicator** | `boolean` | optional (default: `true`) | Show required field indicators | +| **showRecordCount** | `boolean` | optional (default: `false`) | Show live record count on nodes | +| **showIcon** | `boolean` | optional (default: `true`) | Show object icon on node header | +| **showDescription** | `boolean` | optional (default: `true`) | Show description tooltip on hover | + --- @@ -149,6 +161,26 @@ ER diagram layout algorithm | **batchOperations** | `boolean` | optional (default: `true`) | Enable batch add/remove field operations | | **showUsageStats** | `boolean` | optional (default: `false`) | Show field usage statistics | +### Nested Shape: `FieldEditorConfig.propertySections[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **key** | `string` | ✅ | Section key (e.g., "basics", "constraints", "security") | +| **label** | `string` | ✅ | Section display label | +| **icon** | `string` | optional | Lucide icon name | +| **defaultExpanded** | `boolean` | optional (default: `true`) | Whether section is expanded by default | +| **order** | `number` | optional (default: `0`) | Sort order (lower = higher) | + +### Nested Shape: `FieldEditorConfig.fieldGroups[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **key** | `string` | ✅ | Group key matching field.group values | +| **label** | `string` | ✅ | Group display label | +| **icon** | `string` | optional | Lucide icon name | +| **defaultExpanded** | `boolean` | optional (default: `true`) | Whether group is expanded by default | +| **order** | `number` | optional (default: `0`) | Sort order (lower = higher) | + --- @@ -195,6 +227,73 @@ ER diagram layout algorithm | **objectManager** | `{ defaultDisplayMode: Enum<'table' \| 'cards' \| 'tree'>; defaultSortField: Enum<'name' \| 'label' \| 'fieldCount' \| 'updatedAt'>; defaultSortDirection: Enum<'asc' \| 'desc'>; defaultFilter: object; … }` | optional (has default) | Object manager configuration | | **objectPreview** | `{ tabs: object[]; defaultTab: string; showHeader: boolean; showBreadcrumbs: boolean }` | optional (has default) | Object preview configuration | +### Nested Shape: `ObjectDesignerConfig.fieldEditor` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **inlineEditing** | `boolean` | optional (default: `true`) | Enable inline editing of field properties | +| **dragReorder** | `boolean` | optional (default: `true`) | Enable drag-and-drop field reordering | +| **showFieldGroups** | `boolean` | optional (default: `true`) | Show field group headers | +| **showPropertyPanel** | `boolean` | optional (default: `true`) | Show the right-side property panel | +| **propertySections** | `{ key: string; label: string; icon?: string; defaultExpanded: boolean; … }[]` | optional (has default) | Property panel section definitions | +| **fieldGroups** | `{ key: string; label: string; icon?: string; defaultExpanded: boolean; … }[]` | optional (default: `[]`) | Field group definitions | +| **paginationThreshold** | `number` | optional (default: `50`) | Number of fields before pagination is enabled | +| **batchOperations** | `boolean` | optional (default: `true`) | Enable batch add/remove field operations | +| **showUsageStats** | `boolean` | optional (default: `false`) | Show field usage statistics | + +### Nested Shape: `ObjectDesignerConfig.relationshipMapper` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **visualCreation** | `boolean` | optional (default: `true`) | Enable drag-to-create relationships | +| **showReverseRelationships** | `boolean` | optional (default: `true`) | Show reverse/child-to-parent relationships | +| **showCascadeWarnings** | `boolean` | optional (default: `true`) | Show cascade delete behavior warnings | +| **displayConfig** | `{ type: Enum<'lookup' \| 'master_detail' \| 'tree'>; lineStyle: Enum<'solid' \| 'dashed' \| 'dotted'>; color: string; highlightColor: string; … }[]` | optional (has default) | Visual config per relationship type | + +### Nested Shape: `ObjectDesignerConfig.erDiagram` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | Enable ER diagram panel | +| **layout** | `Enum<'force' \| 'hierarchy' \| 'grid' \| 'circular'>` | optional (default: `"force"`) | Default layout algorithm | +| **nodeDisplay** | `{ showFields: boolean; maxFieldsVisible: number; showFieldTypes: boolean; showRequiredIndicator: boolean; … }` | optional (has default) | Node display configuration | +| **showMinimap** | `boolean` | optional (default: `true`) | Show minimap for large diagrams | +| **zoomControls** | `boolean` | optional (default: `true`) | Show zoom in/out/fit controls | +| **minZoom** | `number` | optional (default: `0.1`) | Minimum zoom level | +| **maxZoom** | `number` | optional (default: `3`) | Maximum zoom level | +| **showEdgeLabels** | `boolean` | optional (default: `true`) | Show cardinality labels on relationship edges | +| **highlightOnHover** | `boolean` | optional (default: `true`) | Highlight connected entities on node hover | +| **clickToNavigate** | `boolean` | optional (default: `true`) | Click node to navigate to object detail | +| **dragToConnect** | `boolean` | optional (default: `true`) | Drag between nodes to create relationships | +| **hideOrphans** | `boolean` | optional (default: `false`) | Hide objects with no relationships | +| **autoFit** | `boolean` | optional (default: `true`) | Auto-fit diagram to viewport on load | +| **exportFormats** | `Enum<'png' \| 'svg' \| 'json'>[]` | optional (default: `["png","svg"]`) | Available export formats for diagram | + +### Nested Shape: `ObjectDesignerConfig.objectManager` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **defaultDisplayMode** | `Enum<'table' \| 'cards' \| 'tree'>` | optional (default: `"table"`) | Default list display mode | +| **defaultSortField** | `Enum<'name' \| 'label' \| 'fieldCount' \| 'updatedAt'>` | optional (default: `"label"`) | Default sort field | +| **defaultSortDirection** | `Enum<'asc' \| 'desc'>` | optional (default: `"asc"`) | Default sort direction | +| **defaultFilter** | `{ package?: string; tags?: string[]; includeSystem: boolean; includeAbstract: boolean; … }` | optional (default: `{"includeSystem":true,"includeAbstract":false}`) | Default filter configuration | +| **showFieldCount** | `boolean` | optional (default: `true`) | Show field count badge | +| **showRelationshipCount** | `boolean` | optional (default: `true`) | Show relationship count badge | +| **showQuickPreview** | `boolean` | optional (default: `true`) | Show quick field preview tooltip on hover | +| **enableComparison** | `boolean` | optional (default: `false`) | Enable side-by-side object comparison | +| **showERDiagramToggle** | `boolean` | optional (default: `true`) | Show ER diagram toggle in toolbar | +| **showCreateAction** | `boolean` | optional (default: `true`) | Show create object action | +| **showStatsSummary** | `boolean` | optional (default: `true`) | Show statistics summary bar | + +### Nested Shape: `ObjectDesignerConfig.objectPreview` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **tabs** | `{ key: string; label: string; icon?: string; enabled: boolean; … }[]` | optional (has default) | Object detail preview tabs | +| **defaultTab** | `string` | optional (default: `"fields"`) | Default active tab key | +| **showHeader** | `boolean` | optional (default: `true`) | Show object summary header | +| **showBreadcrumbs** | `boolean` | optional (default: `true`) | Show navigation breadcrumbs | + --- @@ -260,6 +359,18 @@ Object list display mode | **showCreateAction** | `boolean` | optional (default: `true`) | Show create object action | | **showStatsSummary** | `boolean` | optional (default: `true`) | Show statistics summary bar | +### Nested Shape: `ObjectManagerConfig.defaultFilter` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **package** | `string` | optional | Filter by owning package | +| **tags** | `string[]` | optional | Filter by object tags | +| **includeSystem** | `boolean` | optional (default: `true`) | Include system-level objects | +| **includeAbstract** | `boolean` | optional (default: `false`) | Include abstract base objects | +| **hasFieldType** | `string` | optional | Filter to objects containing a specific field type | +| **hasRelationships** | `boolean` | optional | Filter to objects with lookup/master_detail fields | +| **searchQuery** | `string` | optional | Free-text search across name, label, and description | + --- @@ -274,6 +385,16 @@ Object list display mode | **showHeader** | `boolean` | optional (default: `true`) | Show object summary header | | **showBreadcrumbs** | `boolean` | optional (default: `true`) | Show navigation breadcrumbs | +### Nested Shape: `ObjectPreviewConfig.tabs[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **key** | `string` | ✅ | Tab key | +| **label** | `string` | ✅ | Tab display label | +| **icon** | `string` | optional | Lucide icon name | +| **enabled** | `boolean` | optional (default: `true`) | Whether this tab is available | +| **order** | `number` | optional (default: `0`) | Sort order (lower = higher) | + --- @@ -332,6 +453,16 @@ Object list sort field | **showCascadeWarnings** | `boolean` | optional (default: `true`) | Show cascade delete behavior warnings | | **displayConfig** | `{ type: Enum<'lookup' \| 'master_detail' \| 'tree'>; lineStyle: Enum<'solid' \| 'dashed' \| 'dotted'>; color: string; highlightColor: string; … }[]` | optional (has default) | Visual config per relationship type | +### Nested Shape: `RelationshipMapperConfig.displayConfig[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'lookup' \| 'master_detail' \| 'tree'>` | ✅ | Relationship type | +| **lineStyle** | `Enum<'solid' \| 'dashed' \| 'dotted'>` | optional (default: `"solid"`) | Line style in diagrams | +| **color** | `string` | optional (default: `"#94a3b8"`) | Line color (CSS value) | +| **highlightColor** | `string` | optional (default: `"#0891b2"`) | Highlighted color on hover/select | +| **cardinalityLabel** | `string` | optional (default: `"1:N"`) | Cardinality label (e.g., "1:N", "1:1", "N:M") | + --- diff --git a/content/docs/references/studio/plugin.mdx b/content/docs/references/studio/plugin.mdx index 369687415a..91cc26f09b 100644 --- a/content/docs/references/studio/plugin.mdx +++ b/content/docs/references/studio/plugin.mdx @@ -194,6 +194,62 @@ const result = ActionContributionSchema.parse(data); | **panels** | `{ id: string; label: string; icon?: string; location: Enum<'bottom' \| 'right' \| 'modal'> }[]` | optional (default: `[]`) | | | **commands** | `{ id: string; label: string; shortcut?: string; icon?: string }[]` | optional (default: `[]`) | | +### Nested Shape: `StudioPluginContributions.metadataViewers[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique viewer identifier | +| **metadataTypes** | `string[]` | ✅ | Metadata types this viewer can handle | +| **label** | `string` | ✅ | Viewer display label | +| **priority** | `number` | optional (default: `0`) | Viewer priority (higher wins) | +| **modes** | `Enum<'preview' \| 'design' \| 'code' \| 'data' \| 'history'>[]` | optional (default: `["preview"]`) | Supported view modes | + +### Nested Shape: `StudioPluginContributions.sidebarGroups[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **key** | `string` | ✅ | Unique group key | +| **label** | `string` | ✅ | Group display label | +| **icon** | `string` | optional | Lucide icon name | +| **metadataTypes** | `string[]` | ✅ | Metadata types in this group | +| **order** | `number` | optional (default: `100`) | Sort order (lower = higher) | + +### Nested Shape: `StudioPluginContributions.actions[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique action identifier | +| **label** | `string` | ✅ | Action display label | +| **icon** | `string` | optional | Lucide icon name | +| **location** | `Enum<'toolbar' \| 'contextMenu' \| 'commandPalette'>` | ✅ | UI location | +| **metadataTypes** | `string[]` | optional (default: `[]`) | Applicable metadata types | + +### Nested Shape: `StudioPluginContributions.metadataIcons[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **metadataType** | `string` | ✅ | Metadata type | +| **label** | `string` | ✅ | Display label | +| **icon** | `string` | ✅ | Lucide icon name | + +### Nested Shape: `StudioPluginContributions.panels[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique panel identifier | +| **label** | `string` | ✅ | Panel display label | +| **icon** | `string` | optional | Lucide icon name | +| **location** | `Enum<'bottom' \| 'right' \| 'modal'>` | optional (default: `"bottom"`) | Panel location | + +### Nested Shape: `StudioPluginContributions.commands[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique command identifier | +| **label** | `string` | ✅ | Command display label | +| **shortcut** | `string` | optional | Keyboard shortcut | +| **icon** | `string` | optional | Lucide icon name | + --- diff --git a/content/docs/references/system/app-install.mdx b/content/docs/references/system/app-install.mdx index 9ef611d19d..223b78d61a 100644 --- a/content/docs/references/system/app-install.mdx +++ b/content/docs/references/system/app-install.mdx @@ -45,6 +45,14 @@ App compatibility check result | **compatible** | `boolean` | ✅ | Whether the app is compatible | | **issues** | `{ severity: Enum<'error' \| 'warning'>; message: string; category: Enum<'kernel_version' \| 'object_conflict' \| 'dependency_missing' \| 'quota_exceeded'> }[]` | optional (default: `[]`) | Compatibility issues | +### Nested Shape: `AppCompatibilityCheck.issues[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **severity** | `Enum<'error' \| 'warning'>` | ✅ | Issue severity | +| **message** | `string` | ✅ | Issue description | +| **category** | `Enum<'kernel_version' \| 'object_conflict' \| 'dependency_missing' \| 'quota_exceeded'>` | ✅ | Issue category | + --- diff --git a/content/docs/references/system/auth-config.mdx b/content/docs/references/system/auth-config.mdx index fce2eb6c96..c53769072c 100644 --- a/content/docs/references/system/auth-config.mdx +++ b/content/docs/references/system/auth-config.mdx @@ -39,6 +39,14 @@ Advanced / low-level Better-Auth options | **disableCSRFCheck** | `boolean` | optional | ⚠ Disable CSRF check — security risk, use with caution | | **cookiePrefix** | `string` | optional | Prefix for auth cookie names | +### Nested Shape: `AdvancedAuthConfig.crossSubDomainCookies` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | ✅ | Enable cross-subdomain cookies | +| **additionalCookies** | `string[]` | optional | Extra cookies shared across subdomains | +| **domain** | `string` | optional | Cookie domain override — defaults to root domain derived from baseUrl | + --- @@ -78,6 +86,117 @@ Advanced / low-level Better-Auth options | **ssoOnlyMode** | `boolean` | optional | SSO-only login: hide the local password form + self-registration (the break-glass password endpoint stays enabled) | | **mutualTls** | `{ enabled: boolean; clientCertRequired: boolean; trustedCAs: string[]; crlUrl?: string; … }` | optional | Mutual TLS (mTLS) configuration | +### Nested Shape: `AuthConfig.providers[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Provider ID (github, google) | +| **clientId** | `string` | ✅ | OAuth Client ID | +| **clientSecret** | `string` | ✅ | OAuth Client Secret | +| **scope** | `string[]` | optional | Requested permissions | + +### Nested Shape: `AuthConfig.plugins` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **organization** | `boolean` | optional (default: `true`) | Enable Organization/Teams support (frontend AuthProvider expects this enabled) | +| **twoFactor** | `boolean` | optional (default: `false`) | Enable 2FA | +| **passkeys** | `boolean` | optional (default: `false`) | Enable Passkey support | +| **passwordRejectBreached** | `boolean` | optional (default: `false`) | Reject passwords found in the Have I Been Pwned breach corpus (enables better-auth's haveibeenpwned plugin) | +| **magicLink** | `boolean` | optional (default: `false`) | Enable Magic Link login | +| **oidcProvider** | `boolean` | optional (default: `false`) | Enable the OpenID Connect provider plugin (acts as an OIDC IdP) | +| **dynamicClientRegistration** | `boolean` | optional | Allow unauthenticated RFC 7591 Dynamic Client Registration (default: follows OS_MCP_SERVER_ENABLED) | +| **deviceAuthorization** | `boolean` | optional (default: `false`) | Enable RFC 8628 Device Authorization Grant (CLI / TV-style login) | +| **admin** | `boolean` | optional (default: `false`) | Enable platform admin operations (ban/unban, set-password, impersonate, set-role) | +| **phoneNumber** | `boolean` | optional (default: `false`) | Enable phone-number sign-in (phone + password; OTP sign-in/reset when an SMS service is configured) | + +### Nested Shape: `AuthConfig.session` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **expiresIn** | `number` | optional (default: `604800`) | Session duration in seconds | +| **updateAge** | `number` | optional (default: `86400`) | Session update frequency | + +### Nested Shape: `AuthConfig.socialProviders[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **clientId** | `string` | ✅ | OAuth Client ID | +| **clientSecret** | `string` | ✅ | OAuth Client Secret | +| **enabled** | `boolean` | optional (default: `true`) | Enable this provider (default: true) | +| **scope** | `string[]` | optional | Additional OAuth scopes | + +### Nested Shape: `AuthConfig.oidcProviders[number]` + +OIDC / Generic OAuth2 provider configuration for enterprise SSO + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **providerId** | `string` | ✅ | Unique identifier for this provider (e.g., okta, azure-ad) | +| **name** | `string` | optional | Display name shown in the UI (defaults to providerId) | +| **discoveryUrl** | `string` | optional | OIDC discovery URL (.well-known/openid-configuration). When provided, authorizationUrl/tokenUrl/userInfoUrl are fetched automatically. | +| **issuer** | `string` | optional | Expected issuer identifier for token validation | +| **authorizationUrl** | `string` | optional | OAuth2 authorization endpoint (optional if discoveryUrl is set) | +| **tokenUrl** | `string` | optional | OAuth2 token endpoint (optional if discoveryUrl is set) | +| **userInfoUrl** | `string` | optional | OAuth2 userinfo endpoint (optional if discoveryUrl is set) | +| **clientId** | `string` | ✅ | OAuth2 client ID | +| **clientSecret** | `string` | ✅ | OAuth2 client secret | +| **scopes** | `string[]` | optional | Requested scopes (default: openid email profile) | +| **pkce** | `boolean` | optional | Enable PKCE (recommended for public clients) | + +### Nested Shape: `AuthConfig.emailAndPassword` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | Enable email/password auth | +| **disableSignUp** | `boolean` | optional | Disable new user registration via email/password | +| **requireEmailVerification** | `boolean` | optional | Require email verification before creating a session | +| **minPasswordLength** | `number` | optional | Minimum password length (default 8) | +| **maxPasswordLength** | `number` | optional | Maximum password length (default 128) | +| **resetPasswordTokenExpiresIn** | `number` | optional | Reset-password token TTL in seconds (default 3600) | +| **autoSignIn** | `boolean` | optional | Auto sign-in after sign-up (default true) | +| **revokeSessionsOnPasswordReset** | `boolean` | optional | Revoke all other sessions on password reset | + +### Nested Shape: `AuthConfig.emailVerification` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **sendOnSignUp** | `boolean` | optional | Automatically send verification email after sign-up | +| **sendOnSignIn** | `boolean` | optional | Send verification email on sign-in when not yet verified | +| **autoSignInAfterVerification** | `boolean` | optional | Auto sign-in the user after email verification | +| **expiresIn** | `number` | optional | Verification token TTL in seconds (default 3600) | + +### Nested Shape: `AuthConfig.audience` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **posture** | `Enum<'invite_only' \| 'email_domain' \| 'open'>` | optional (default: `"invite_only"`) | Who may self-register into this environment: invite_only (default — operator acts only), email_domain (allowlisted email domains), or open (anyone). Any posture other than invite_only forces email verification on. | +| **allowedEmailDomains** | `string[]` | optional | Email domains admitted to self-register under posture email_domain (exact, case-insensitive match; subdomains need their own entries). Required non-empty for email_domain; refused under other postures. | +| **selfRegistrationPermissionSet** | `string` | optional | sys_permission_set name granted to each self-registrant. Required when posture is email_domain or open; refused for invite_only. admin_full_access is refused. | + +### Nested Shape: `AuthConfig.advanced` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **crossSubDomainCookies** | `{ enabled: boolean; additionalCookies?: string[]; domain?: string }` | optional | Share auth cookies across subdomains (critical for *.example.com multi-tenant) | +| **useSecureCookies** | `boolean` | optional | Force Secure flag on cookies | +| **disableCSRFCheck** | `boolean` | optional | ⚠ Disable CSRF check — security risk, use with caution | +| **cookiePrefix** | `string` | optional | Prefix for auth cookie names | + +### Nested Shape: `AuthConfig.mutualTls` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `false`) | Enable mutual TLS authentication | +| **clientCertRequired** | `boolean` | optional (default: `false`) | Require client certificates for all connections | +| **trustedCAs** | `string[]` | ✅ | PEM-encoded CA certificates or file paths | +| **crlUrl** | `string` | optional | Certificate Revocation List (CRL) URL | +| **ocspUrl** | `string` | optional | Online Certificate Status Protocol (OCSP) URL | +| **certificateValidation** | `Enum<'strict' \| 'relaxed' \| 'none'>` | ✅ | Certificate validation strictness level | +| **allowedCNs** | `string[]` | optional | Allowed Common Names (CN) on client certificates | +| **allowedOUs** | `string[]` | optional | Allowed Organizational Units (OU) on client certificates | +| **pinning** | `{ enabled: boolean; pins: string[] }` | optional | Certificate pinning configuration | + --- @@ -167,6 +286,13 @@ Email verification options forwarded to better-auth | **allowedOUs** | `string[]` | optional | Allowed Organizational Units (OU) on client certificates | | **pinning** | `{ enabled: boolean; pins: string[] }` | optional | Certificate pinning configuration | +### Nested Shape: `MutualTLSConfig.pinning` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | ✅ | Enable certificate pinning | +| **pins** | `string[]` | ✅ | Pinned certificate hashes | + --- diff --git a/content/docs/references/system/book.mdx b/content/docs/references/system/book.mdx index effc87452c..eed264a5c3 100644 --- a/content/docs/references/system/book.mdx +++ b/content/docs/references/system/book.mdx @@ -61,6 +61,18 @@ const result = BookSchema.parse(data); | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `Book.groups[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **key** | `string` | ✅ | Stable group key (used by overrides, deep links, explicit `doc.group`) | +| **label** | `string` | ✅ | Section title — first-class, i18n-homed | +| **translations** | `never` | optional | [REMOVED] Inline `translations` on a book (and on a book group) was removed in @objectstack/spec 17.0.0 (#4667, ADR-0049) — no resolver ever read it. The book tree endpoint and the docs portal render `label` / `description` verbatim in every locale, so a localized book shipped its authoring-locale strings to every reader. Delete the key. NOTE the near neighbour that DOES work: `doc.translations` is live and read on every doc render path — localize the docs themselves, and the portal picks the reader's locale up from there. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **order** | `number` | optional | Order of THIS group within the book | +| **include** | `string \| { tag: string }` | optional | Rule that derives membership (glob or tag) | +| **package** | `string` | optional | Scope the rule to a package id (default: the book package; cross-package via ADR-0048) | +| **pages** | `(string \| { doc?: string; href?: string; label?: string; badge?: string; … })[]` | optional | OPTIONAL explicit override — hand-pin a curated order; wins over `include` | + --- @@ -109,6 +121,16 @@ Type: `'public'` | **package** | `string` | optional | Scope the rule to a package id (default: the book package; cross-package via ADR-0048) | | **pages** | `(string \| { doc?: string; href?: string; label?: string; badge?: string; … })[]` | optional | OPTIONAL explicit override — hand-pin a curated order; wins over `include` | +### Nested Shape: `BookGroup.pages[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **doc** | `string` | optional | Doc name to reference | +| **href** | `string` | optional | External link (use instead of `doc`) | +| **label** | `string` | optional | Optional label override; title authority stays in the doc | +| **badge** | `string` | optional | e.g. "beta" \| "new" | +| **icon** | `string` | optional | | + --- diff --git a/content/docs/references/system/cache.mdx b/content/docs/references/system/cache.mdx index a37ee5b08a..f2ae75b68b 100644 --- a/content/docs/references/system/cache.mdx +++ b/content/docs/references/system/cache.mdx @@ -56,6 +56,28 @@ Cache avalanche/stampede prevention configuration | **circuitBreaker** | `{ enabled: boolean; failureThreshold: number; resetTimeout: number }` | optional | Circuit breaker for backend protection | | **lockout** | `{ enabled: boolean; lockTimeoutMs: number }` | optional | Lock-based stampede prevention | +### Nested Shape: `CacheAvalanchePrevention.jitterTtl` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `false`) | Add random jitter to TTL values | +| **maxJitterSeconds** | `number` | optional (default: `60`) | Maximum jitter added to TTL in seconds | + +### Nested Shape: `CacheAvalanchePrevention.circuitBreaker` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `false`) | Enable circuit breaker for backend protection | +| **failureThreshold** | `number` | optional (default: `5`) | Failures before circuit opens | +| **resetTimeout** | `number` | optional (default: `30`) | Seconds before half-open state | + +### Nested Shape: `CacheAvalanchePrevention.lockout` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `false`) | Enable cache locking for key regeneration | +| **lockTimeoutMs** | `number` | optional (default: `5000`) | Maximum lock wait time in milliseconds | + --- @@ -74,6 +96,30 @@ Top-level application cache configuration | **compression** | `boolean` | optional (default: `false`) | Enable data compression in cache | | **encryption** | `boolean` | optional (default: `false`) | Enable encryption for cached data | +### Nested Shape: `CacheConfig.tiers[number]` + +Configuration for a single cache tier in the hierarchy + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Unique cache tier name | +| **type** | `Enum<'memory' \| 'redis' \| 'memcached' \| 'cdn'>` | ✅ | Cache backend type | +| **maxSize** | `number` | optional | Max size in MB | +| **ttl** | `number` | optional (default: `300`) | Default TTL in seconds | +| **strategy** | `Enum<'lru' \| 'lfu' \| 'fifo' \| 'ttl'>` | optional (default: `"lru"`) | Eviction strategy | +| **warmup** | `boolean` | optional (default: `false`) | Pre-populate cache on startup | + +### Nested Shape: `CacheConfig.invalidation[number]` + +Rule defining when and how cached entries are invalidated + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **trigger** | `Enum<'create' \| 'update' \| 'delete' \| 'manual'>` | ✅ | Event that triggers invalidation | +| **scope** | `Enum<'key' \| 'pattern' \| 'tag' \| 'all'>` | ✅ | Invalidation scope | +| **pattern** | `string` | optional | Key pattern for pattern-based invalidation | +| **tags** | `string[]` | optional | Cache tags to invalidate | + --- @@ -174,6 +220,48 @@ Distributed cache configuration with consistency and avalanche prevention | **avalanchePrevention** | `{ jitterTtl?: object; circuitBreaker?: object; lockout?: object }` | optional | Cache avalanche and stampede prevention | | **warmup** | `{ enabled?: boolean; strategy?: Enum<'eager' \| 'lazy' \| 'scheduled'>; schedule?: string \| object; patterns?: string[]; … }` | optional | Cache warmup strategy | +### Nested Shape: `DistributedCacheConfig.tiers[number]` + +Configuration for a single cache tier in the hierarchy + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Unique cache tier name | +| **type** | `Enum<'memory' \| 'redis' \| 'memcached' \| 'cdn'>` | ✅ | Cache backend type | +| **maxSize** | `number` | optional | Max size in MB | +| **ttl** | `number` | optional (default: `300`) | Default TTL in seconds | +| **strategy** | `Enum<'lru' \| 'lfu' \| 'fifo' \| 'ttl'>` | optional (default: `"lru"`) | Eviction strategy | +| **warmup** | `boolean` | optional (default: `false`) | Pre-populate cache on startup | + +### Nested Shape: `DistributedCacheConfig.invalidation[number]` + +Rule defining when and how cached entries are invalidated + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **trigger** | `Enum<'create' \| 'update' \| 'delete' \| 'manual'>` | ✅ | Event that triggers invalidation | +| **scope** | `Enum<'key' \| 'pattern' \| 'tag' \| 'all'>` | ✅ | Invalidation scope | +| **pattern** | `string` | optional | Key pattern for pattern-based invalidation | +| **tags** | `string[]` | optional | Cache tags to invalidate | + +### Nested Shape: `DistributedCacheConfig.avalanchePrevention` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **jitterTtl** | `{ enabled?: boolean; maxJitterSeconds?: number }` | optional | TTL jitter to prevent simultaneous expiration | +| **circuitBreaker** | `{ enabled?: boolean; failureThreshold?: number; resetTimeout?: number }` | optional | Circuit breaker for backend protection | +| **lockout** | `{ enabled?: boolean; lockTimeoutMs?: number }` | optional | Lock-based stampede prevention | + +### Nested Shape: `DistributedCacheConfig.warmup` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `false`) | Enable cache warmup | +| **strategy** | `Enum<'eager' \| 'lazy' \| 'scheduled'>` | optional (default: `"lazy"`) | Warmup strategy: eager (at startup), lazy (on first access), scheduled (cron) | +| **schedule** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Cron expression for scheduled warmup | +| **patterns** | `string[]` | optional | Key patterns to warm up (e.g., "user:*", "config:*") | +| **concurrency** | `number` | optional (default: `10`) | Maximum concurrent warmup operations | + --- diff --git a/content/docs/references/system/change-management.mdx b/content/docs/references/system/change-management.mdx index 077932c2b6..2dbcc5ca72 100644 --- a/content/docs/references/system/change-management.mdx +++ b/content/docs/references/system/change-management.mdx @@ -37,6 +37,13 @@ const result = ChangeImpactSchema.parse(data); | **affectedUsers** | `number` | optional | Affected user count | | **downtime** | `{ required: boolean; durationMinutes?: number }` | optional | Downtime information | +### Nested Shape: `ChangeImpact.downtime` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **required** | `boolean` | ✅ | Downtime required | +| **durationMinutes** | `number` | optional | Downtime duration | + --- @@ -75,6 +82,66 @@ const result = ChangeImpactSchema.parse(data); | **attachments** | `{ name: string; url: string }[]` | optional | Attachments | | **metadata** | `Record` | optional | Custom metadata key-value pairs for extensibility | +### Nested Shape: `ChangeRequest.impact` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **level** | `Enum<'low' \| 'medium' \| 'high' \| 'critical'>` | ✅ | Impact level | +| **affectedSystems** | `string[]` | ✅ | Affected systems | +| **affectedUsers** | `number` | optional | Affected user count | +| **downtime** | `{ required: boolean; durationMinutes?: number }` | optional | Downtime information | + +### Nested Shape: `ChangeRequest.implementation` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **description** | `string` | ✅ | Implementation description | +| **steps** | `{ order: number; description: string; estimatedMinutes: number }[]` | ✅ | Implementation steps | +| **testing** | `string` | optional | Testing procedure | + +### Nested Shape: `ChangeRequest.rollbackPlan` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **description** | `string` | ✅ | Rollback description | +| **steps** | `{ order: number; description: string; estimatedMinutes: number }[]` | ✅ | Rollback steps | +| **testProcedure** | `string` | optional | Test procedure | + +### Nested Shape: `ChangeRequest.schedule` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **plannedStart** | `number` | ✅ | Planned start time | +| **plannedEnd** | `number` | ✅ | Planned end time | +| **actualStart** | `number` | optional | Actual start time | +| **actualEnd** | `number` | optional | Actual end time | + +### Nested Shape: `ChangeRequest.securityImpact` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **assessed** | `boolean` | ✅ | Whether security impact has been assessed | +| **riskLevel** | `Enum<'none' \| 'low' \| 'medium' \| 'high' \| 'critical'>` | optional | Security risk level | +| **affectedDataClassifications** | `Enum<'pii' \| 'phi' \| 'pci' \| 'financial' \| 'confidential' \| 'internal' \| 'public'>[]` | optional | Affected data classifications | +| **requiresSecurityApproval** | `boolean` | optional (default: `false`) | Whether security team approval is required | +| **reviewedBy** | `string` | optional | Security reviewer user ID | +| **reviewedAt** | `number` | optional | Security review timestamp | +| **reviewNotes** | `string` | optional | Security review notes or conditions | + +### Nested Shape: `ChangeRequest.approval` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **required** | `boolean` | ✅ | Approval required | +| **approvers** | `{ userId: string; approvedAt?: number; comments?: string }[]` | ✅ | Approvers | + +### Nested Shape: `ChangeRequest.attachments[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Attachment name | +| **url** | `string` | ✅ | Attachment URL | + --- @@ -118,6 +185,14 @@ const result = ChangeImpactSchema.parse(data); | **steps** | `{ order: number; description: string; estimatedMinutes: number }[]` | ✅ | Rollback steps | | **testProcedure** | `string` | optional | Test procedure | +### Nested Shape: `RollbackPlan.steps[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **order** | `number` | ✅ | Step order | +| **description** | `string` | ✅ | Step description | +| **estimatedMinutes** | `number` | ✅ | Estimated duration | + --- diff --git a/content/docs/references/system/collaboration.mdx b/content/docs/references/system/collaboration.mdx index cae1f6a390..93256ecaa9 100644 --- a/content/docs/references/system/collaboration.mdx +++ b/content/docs/references/system/collaboration.mdx @@ -58,6 +58,22 @@ const result = AwarenessEventSchema.parse(data); | **lastUpdate** | `string` | ✅ | ISO 8601 datetime of last update | | **metadata** | `Record` | optional | Session metadata | +### Nested Shape: `AwarenessSession.users[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **userId** | `string` | ✅ | User identifier | +| **sessionId** | `string` | ✅ | Session identifier | +| **userName** | `string` | ✅ | Display name | +| **userAvatar** | `string` | optional | User avatar URL | +| **status** | `Enum<'active' \| 'idle' \| 'viewing' \| 'disconnected'>` | ✅ | Current activity status | +| **currentDocument** | `string` | optional | Document ID user is currently editing | +| **currentView** | `string` | optional | Current view/page user is on | +| **lastActivity** | `string` | ✅ | ISO 8601 datetime of last activity | +| **joinedAt** | `string` | ✅ | ISO 8601 datetime when user joined session | +| **permissions** | `string[]` | optional | User permissions in this session | +| **metadata** | `Record` | optional | Additional user state metadata | + --- @@ -105,6 +121,14 @@ 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.conflicts[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `string` | ✅ | Conflict type | +| **description** | `string` | ✅ | Conflict description | +| **resolved** | `boolean` | ✅ | Whether conflict was automatically resolved | + --- @@ -128,6 +152,12 @@ This schema accepts one of the following structures: | **replicaId** | `string` | ✅ | ID of replica that performed last write | | **vectorClock** | `{ clock: Record }` | optional | Optional vector clock for causality tracking | +### Nested Shape: `CRDTState.vectorClock` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **clock** | `Record` | ✅ | Map of replica ID to logical timestamp | + --- #### Option 2 @@ -168,6 +198,16 @@ This schema accepts one of the following structures: | **type** | `'or-set'` | ✅ | | | **elements** | `{ value: any; timestamp: string; replicaId: string; uid: string; … }[]` | ✅ | Set elements with metadata | +### Nested Shape: `CRDTState.elements[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **value** | `any` | ✅ | Element value | +| **timestamp** | `string` | ✅ | Addition timestamp | +| **replicaId** | `string` | ✅ | Replica that added the element | +| **uid** | `string` | ✅ | Unique identifier for this addition | +| **removed** | `boolean` | optional (default: `false`) | Whether element has been removed | + --- #### Option 5 @@ -185,6 +225,24 @@ This schema accepts one of the following structures: | **lamportClock** | `integer` | ✅ | Current Lamport clock value | | **vectorClock** | `{ clock: Record }` | ✅ | Vector clock for causality | +### Nested Shape: `CRDTState.operations[number]` + +| 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 | + +### Nested Shape: `CRDTState.vectorClock` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **clock** | `Record` | ✅ | Map of replica ID to logical timestamp | + --- @@ -236,6 +294,51 @@ This schema accepts one of the following structures: | **lastActivity** | `string` | ✅ | ISO 8601 datetime of last activity | | **status** | `Enum<'active' \| 'idle' \| 'ended'>` | ✅ | Session status | +### Nested Shape: `CollaborationSession.config` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **mode** | `Enum<'ot' \| 'crdt' \| 'lock' \| 'hybrid'>` | ✅ | Collaboration mode to use | +| **enableCursorSharing** | `boolean` | optional (default: `true`) | Enable cursor sharing | +| **enablePresence** | `boolean` | optional (default: `true`) | Enable presence tracking | +| **enableAwareness** | `boolean` | optional (default: `true`) | Enable awareness state | +| **maxUsers** | `integer` | optional | Maximum concurrent users | +| **idleTimeout** | `integer` | optional (default: `300000`) | Idle timeout in milliseconds | +| **conflictResolution** | `Enum<'ot' \| 'crdt' \| 'manual'>` | optional (default: `"ot"`) | Conflict resolution strategy | +| **persistence** | `boolean` | optional (default: `true`) | Enable operation persistence | +| **snapshot** | `{ enabled: boolean; interval: integer }` | optional | Snapshot configuration | + +### Nested Shape: `CollaborationSession.users[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **userId** | `string` | ✅ | User identifier | +| **sessionId** | `string` | ✅ | Session identifier | +| **userName** | `string` | ✅ | Display name | +| **userAvatar** | `string` | optional | User avatar URL | +| **status** | `Enum<'active' \| 'idle' \| 'viewing' \| 'disconnected'>` | ✅ | Current activity status | +| **currentDocument** | `string` | optional | Document ID user is currently editing | +| **currentView** | `string` | optional | Current view/page user is on | +| **lastActivity** | `string` | ✅ | ISO 8601 datetime of last activity | +| **joinedAt** | `string` | ✅ | ISO 8601 datetime when user joined session | +| **permissions** | `string[]` | optional | User permissions in this session | +| **metadata** | `Record` | optional | Additional user state metadata | + +### Nested Shape: `CollaborationSession.cursors[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **userId** | `string` | ✅ | User identifier | +| **sessionId** | `string` | ✅ | Session identifier | +| **documentId** | `string` | ✅ | Document identifier | +| **userName** | `string` | ✅ | Display name of user | +| **position** | `{ line: integer; column: integer }` | ✅ | Current cursor position | +| **selection** | `{ anchor: object; focus: object; direction?: Enum<'forward' \| 'backward'> }` | optional | Current text selection | +| **style** | `{ color: Enum<'blue' \| 'green' \| 'red' \| 'yellow' \| 'purple' \| 'orange' \| 'pink' \| 'teal' \| 'indigo' \| 'cyan'> \| string; opacity: number; label?: string; showLabel: boolean; … }` | ✅ | Visual style for this cursor | +| **isTyping** | `boolean` | optional (default: `false`) | Whether user is currently typing | +| **lastUpdate** | `string` | ✅ | ISO 8601 datetime of last cursor update | +| **metadata** | `Record` | optional | Additional cursor metadata | + --- @@ -255,6 +358,13 @@ This schema accepts one of the following structures: | **persistence** | `boolean` | optional (default: `true`) | Enable operation persistence | | **snapshot** | `{ enabled: boolean; interval: integer }` | optional | Snapshot configuration | +### Nested Shape: `CollaborationSessionConfig.snapshot` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | ✅ | Enable periodic snapshots | +| **interval** | `integer` | ✅ | Snapshot interval in milliseconds | + --- @@ -275,6 +385,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: `CollaborativeCursor.position` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **line** | `integer` | ✅ | Cursor line number (0-indexed) | +| **column** | `integer` | ✅ | Cursor column number (0-indexed) | + +### Nested Shape: `CollaborativeCursor.selection` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **anchor** | `{ line: integer; column: integer }` | ✅ | Selection anchor (start point) | +| **focus** | `{ line: integer; column: integer }` | ✅ | Selection focus (end point) | +| **direction** | `Enum<'forward' \| 'backward'>` | optional | Selection direction | + +### Nested Shape: `CollaborativeCursor.style` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **color** | `Enum<'blue' \| 'green' \| 'red' \| 'yellow' \| 'purple' \| 'orange' \| 'pink' \| 'teal' \| 'indigo' \| 'cyan'> \| string` | ✅ | Cursor color (preset or custom hex) | +| **opacity** | `number` | optional (default: `1`) | Cursor opacity (0-1) | +| **label** | `string` | optional | Label to display with cursor (usually username) | +| **showLabel** | `boolean` | optional (default: `true`) | Whether to show label | +| **pulseOnUpdate** | `boolean` | optional (default: `true`) | Whether to pulse when cursor moves | + --- @@ -319,6 +454,20 @@ This schema accepts one of the following structures: | **focus** | `{ line: integer; column: integer }` | ✅ | Selection focus (end point) | | **direction** | `Enum<'forward' \| 'backward'>` | optional | Selection direction | +### Nested Shape: `CursorSelection.anchor` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **line** | `integer` | ✅ | Anchor line number | +| **column** | `integer` | ✅ | Anchor column number | + +### Nested Shape: `CursorSelection.focus` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **line** | `integer` | ✅ | Focus line number | +| **column** | `integer` | ✅ | Focus column number | + --- @@ -348,6 +497,14 @@ This schema accepts one of the following structures: | **isTyping** | `boolean` | optional | Updated typing state | | **metadata** | `Record` | optional | Updated metadata | +### Nested Shape: `CursorUpdate.selection` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **anchor** | `{ line: integer; column: integer }` | ✅ | Selection anchor (start point) | +| **focus** | `{ line: integer; column: integer }` | ✅ | Selection focus (end point) | +| **direction** | `Enum<'forward' \| 'backward'>` | optional | Selection direction | + --- @@ -375,6 +532,12 @@ This schema accepts one of the following structures: | **replicaId** | `string` | ✅ | ID of replica that performed last write | | **vectorClock** | `{ clock: Record }` | optional | Optional vector clock for causality tracking | +### Nested Shape: `LWWRegister.vectorClock` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **clock** | `Record` | ✅ | Map of replica ID to logical timestamp | + --- @@ -387,6 +550,16 @@ This schema accepts one of the following structures: | **type** | `'or-set'` | ✅ | | | **elements** | `{ value: any; timestamp: string; replicaId: string; uid: string; … }[]` | ✅ | Set elements with metadata | +### Nested Shape: `ORSet.elements[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **value** | `any` | ✅ | Element value | +| **timestamp** | `string` | ✅ | Addition timestamp | +| **replicaId** | `string` | ✅ | Replica that added the element | +| **uid** | `string` | ✅ | Unique identifier for this addition | +| **removed** | `boolean` | optional (default: `false`) | Whether element has been removed | + --- @@ -494,6 +667,19 @@ This schema accepts one of the following structures: | **transformed** | `boolean` | ✅ | Whether transformation was applied | | **conflicts** | `string[]` | optional | Conflict descriptions if any | +### Nested Shape: `OTTransformResult.operation` + +| 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 | + --- @@ -540,6 +726,24 @@ This schema accepts one of the following structures: | **lamportClock** | `integer` | ✅ | Current Lamport clock value | | **vectorClock** | `{ clock: Record }` | ✅ | Vector clock for causality | +### Nested Shape: `TextCRDTState.operations[number]` + +| 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 | + +### Nested Shape: `TextCRDTState.vectorClock` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **clock** | `Record` | ✅ | Map of replica ID to logical timestamp | + --- diff --git a/content/docs/references/system/deploy-bundle.mdx b/content/docs/references/system/deploy-bundle.mdx index 1ddec108b9..493a796f51 100644 --- a/content/docs/references/system/deploy-bundle.mdx +++ b/content/docs/references/system/deploy-bundle.mdx @@ -48,6 +48,18 @@ Deploy bundle containing all metadata for deployment | **permissions** | `Record[]` | optional (default: `[]`) | Permission definitions | | **seedData** | `Record[]` | optional (default: `[]`) | Seed data records | +### Nested Shape: `DeployBundle.manifest` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **version** | `string` | ✅ | Deployment version | +| **checksum** | `string` | optional | SHA256 checksum | +| **objects** | `string[]` | optional (default: `[]`) | Object names included | +| **views** | `string[]` | optional (default: `[]`) | View names included | +| **flows** | `string[]` | optional (default: `[]`) | Flow names included | +| **permissions** | `string[]` | optional (default: `[]`) | Permission names included | +| **createdAt** | `string` | optional | Bundle creation time | + --- @@ -63,6 +75,27 @@ Schema diff between current and desired state | **summary** | `{ added: integer; modified: integer; removed: integer }` | ✅ | Change summary counts | | **hasBreakingChanges** | `boolean` | optional (default: `false`) | Whether diff contains breaking changes | +### Nested Shape: `DeployDiff.changes[number]` + +Individual schema change + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **entityType** | `Enum<'object' \| 'field' \| 'index' \| 'view' \| 'flow' \| 'permission'>` | ✅ | Entity type | +| **entityName** | `string` | ✅ | Entity name | +| **parentEntity** | `string` | optional | Parent entity name | +| **changeType** | `Enum<'added' \| 'modified' \| 'removed'>` | ✅ | Change type | +| **oldValue** | `any` | optional | Previous value | +| **newValue** | `any` | optional | New value | + +### Nested Shape: `DeployDiff.summary` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **added** | `integer` | optional (default: `0`) | Number of added entities | +| **modified** | `integer` | optional (default: `0`) | Number of modified entities | +| **removed** | `integer` | optional (default: `0`) | Number of removed entities | + --- @@ -131,6 +164,17 @@ Bundle validation result | **errorCount** | `integer` | optional (default: `0`) | Number of errors | | **warningCount** | `integer` | optional (default: `0`) | Number of warnings | +### Nested Shape: `DeployValidationResult.issues[number]` + +Validation issue + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | ✅ | Issue severity | +| **path** | `string` | ✅ | Entity path (e.g., objects.project_task.fields.name) | +| **message** | `string` | ✅ | Issue description | +| **code** | `string` | optional | Validation error code | + --- @@ -147,6 +191,17 @@ Ordered migration plan | **reversible** | `boolean` | optional (default: `true`) | Whether the plan can be fully rolled back | | **estimatedDurationMs** | `integer` | optional | Estimated execution time | +### Nested Shape: `MigrationPlan.statements[number]` + +Single DDL migration statement + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **sql** | `string` | ✅ | SQL DDL statement | +| **reversible** | `boolean` | optional (default: `true`) | Whether the statement can be reversed | +| **rollbackSql** | `string` | optional | Reverse SQL for rollback | +| **order** | `integer` | ✅ | Execution order | + --- diff --git a/content/docs/references/system/disaster-recovery.mdx b/content/docs/references/system/disaster-recovery.mdx index 8bb255e16a..8be8c1d2ad 100644 --- a/content/docs/references/system/disaster-recovery.mdx +++ b/content/docs/references/system/disaster-recovery.mdx @@ -55,6 +55,38 @@ Backup configuration | **compression** | `{ enabled?: boolean; algorithm?: Enum<'gzip' \| 'zstd' \| 'lz4' \| 'snappy'> }` | optional | Backup compression settings | | **verifyAfterBackup** | `boolean` | optional (default: `true`) | Verify backup integrity after creation | +### Nested Shape: `BackupConfig.retention` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **days** | `number` | ✅ | Retention period in days | +| **minCopies** | `number` | optional (default: `3`) | Minimum backup copies to retain | +| **maxCopies** | `number` | optional | Maximum backup copies to store | + +### Nested Shape: `BackupConfig.destination` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'s3' \| 'gcs' \| 'azure_blob' \| 'local'>` | ✅ | Storage backend type | +| **bucket** | `string` | optional | Cloud storage bucket/container name | +| **path** | `string` | optional | Storage path prefix | +| **region** | `string` | optional | Cloud storage region | + +### Nested Shape: `BackupConfig.encryption` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | Enable backup encryption | +| **algorithm** | `Enum<'AES-256-GCM' \| 'AES-256-CBC' \| 'ChaCha20-Poly1305'>` | optional (default: `"AES-256-GCM"`) | Encryption algorithm | +| **keyId** | `string` | optional | KMS key ID for encryption | + +### Nested Shape: `BackupConfig.compression` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | Enable backup compression | +| **algorithm** | `Enum<'gzip' \| 'zstd' \| 'lz4' \| 'snappy'>` | optional (default: `"zstd"`) | Compression algorithm | + --- @@ -104,6 +136,69 @@ Complete disaster recovery plan configuration | **runbookUrl** | `string` | optional | URL to disaster recovery runbook/playbook | | **contacts** | `{ name: string; role: string; email?: string; phone?: string }[]` | optional | Emergency contact list for DR incidents | +### Nested Shape: `DisasterRecoveryPlan.rpo` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **value** | `number` | ✅ | RPO value | +| **unit** | `Enum<'seconds' \| 'minutes' \| 'hours'>` | optional (default: `"minutes"`) | RPO time unit | + +### Nested Shape: `DisasterRecoveryPlan.rto` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **value** | `number` | ✅ | RTO value | +| **unit** | `Enum<'seconds' \| 'minutes' \| 'hours'>` | optional (default: `"minutes"`) | RTO time unit | + +### Nested Shape: `DisasterRecoveryPlan.backup` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **strategy** | `Enum<'full' \| 'incremental' \| 'differential'>` | optional (default: `"incremental"`) | Backup strategy | +| **schedule** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Cron expression for backup schedule — cron`0 2 * * *` | +| **retention** | `{ days: number; minCopies?: number; maxCopies?: number }` | ✅ | Backup retention policy | +| **destination** | `{ type: Enum<'s3' \| 'gcs' \| 'azure_blob' \| 'local'>; bucket?: string; path?: string; region?: string }` | ✅ | Backup storage destination | +| **encryption** | `{ enabled?: boolean; algorithm?: Enum<'AES-256-GCM' \| 'AES-256-CBC' \| 'ChaCha20-Poly1305'>; keyId?: string }` | optional | Backup encryption settings | +| **compression** | `{ enabled?: boolean; algorithm?: Enum<'gzip' \| 'zstd' \| 'lz4' \| 'snappy'> }` | optional | Backup compression settings | +| **verifyAfterBackup** | `boolean` | optional (default: `true`) | Verify backup integrity after creation | + +### Nested Shape: `DisasterRecoveryPlan.failover` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **mode** | `Enum<'active_passive' \| 'active_active' \| 'pilot_light' \| 'warm_standby'>` | optional (default: `"active_passive"`) | Failover mode | +| **autoFailover** | `boolean` | optional (default: `true`) | Enable automatic failover | +| **healthCheckInterval** | `number` | optional (default: `30`) | Health check interval in seconds | +| **failureThreshold** | `number` | optional (default: `3`) | Consecutive failures before failover | +| **regions** | `{ name: string; role: Enum<'primary' \| 'secondary' \| 'witness'>; endpoint?: string; priority?: number }[]` | ✅ | Multi-region configuration (minimum 2 regions) | +| **dns** | `{ ttl?: number; provider?: Enum<'route53' \| 'cloudflare' \| 'azure_dns' \| 'custom'> }` | optional | DNS failover settings | + +### Nested Shape: `DisasterRecoveryPlan.replication` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **mode** | `Enum<'synchronous' \| 'asynchronous' \| 'semi_synchronous'>` | optional (default: `"asynchronous"`) | Data replication mode | +| **maxLagSeconds** | `number` | optional | Maximum acceptable replication lag in seconds | +| **includeObjects** | `string[]` | optional | Objects to replicate (empty = all) | +| **excludeObjects** | `string[]` | optional | Objects to exclude from replication | + +### Nested Shape: `DisasterRecoveryPlan.testing` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `false`) | Enable automated DR testing | +| **schedule** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Cron expression for DR test schedule | +| **notificationChannel** | `string` | optional | Notification channel for DR test results | + +### Nested Shape: `DisasterRecoveryPlan.contacts[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Contact name | +| **role** | `string` | ✅ | Contact role (e.g., "DBA", "SRE Lead") | +| **email** | `string` | optional | Contact email | +| **phone** | `string` | optional | Contact phone | + --- @@ -122,6 +217,22 @@ Failover configuration | **regions** | `{ name: string; role: Enum<'primary' \| 'secondary' \| 'witness'>; endpoint?: string; priority?: number }[]` | ✅ | Multi-region configuration (minimum 2 regions) | | **dns** | `{ ttl: number; provider?: Enum<'route53' \| 'cloudflare' \| 'azure_dns' \| 'custom'> }` | optional | DNS failover settings | +### Nested Shape: `FailoverConfig.regions[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Region identifier (e.g., "us-east-1", "eu-west-1") | +| **role** | `Enum<'primary' \| 'secondary' \| 'witness'>` | ✅ | Region role | +| **endpoint** | `string` | optional | Region endpoint URL | +| **priority** | `number` | optional | Failover priority (lower = higher priority) | + +### Nested Shape: `FailoverConfig.dns` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ttl** | `number` | optional (default: `60`) | DNS TTL in seconds for failover | +| **provider** | `Enum<'route53' \| 'cloudflare' \| 'azure_dns' \| 'custom'>` | optional | DNS provider for automatic failover | + --- diff --git a/content/docs/references/system/email-config.mdx b/content/docs/references/system/email-config.mdx index 00ace05f4c..cbd434aa17 100644 --- a/content/docs/references/system/email-config.mdx +++ b/content/docs/references/system/email-config.mdx @@ -89,6 +89,13 @@ const result = EmailAddressConfigSchema.parse(data); | **appName** | `string` | optional | Product name templates interpolate as the appName variable — OS_APP_NAME env wins, then this, then defaultTemplateContext.appName, then the top-level config appName, then "ObjectStack". Also seeds the placeholder no-reply sender when no defaultFrom is configured | | **defaultTemplateContext** | `Record` | optional | Free-form render context merged into every sendTemplate() call, under the per-call data. Passed through unchanged except appName, which is resolved by its own chain — OS_APP_NAME and the appName key both override the value written here | +### Nested Shape: `EmailServiceConfig.defaultFrom` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional | Display name (e.g. "Acme CRM") | +| **address** | `string` | ✅ | RFC-5322 address | + --- diff --git a/content/docs/references/system/email-template.mdx b/content/docs/references/system/email-template.mdx index fdf0750cfa..d00fb13ebd 100644 --- a/content/docs/references/system/email-template.mdx +++ b/content/docs/references/system/email-template.mdx @@ -63,6 +63,23 @@ const result = EmailTemplateDefinitionSchema.parse(data); | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `EmailTemplateDefinition.variables[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Variable name as referenced in placeholders (snake_case or dotted path) | +| **type** | `Enum<'string' \| 'number' \| 'boolean' \| 'date' \| 'url' \| 'user' \| 'record'>` | optional (default: `"string"`) | | +| **required** | `boolean` | optional (default: `false`) | | +| **description** | `string` | optional | Author hint shown in Studio | + +### Nested Shape: `EmailTemplateDefinition.protection` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | ✅ | Lock policy — none \| no-overlay \| no-delete \| full. | +| **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | +| **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | + --- diff --git a/content/docs/references/system/encryption.mdx b/content/docs/references/system/encryption.mdx index c82df89208..c5a8b85402 100644 --- a/content/docs/references/system/encryption.mdx +++ b/content/docs/references/system/encryption.mdx @@ -52,6 +52,14 @@ Field-level encryption configuration | **deterministicEncryption** | `boolean` | optional (default: `false`) | Allows equality queries on encrypted data | | **searchableEncryption** | `boolean` | optional (default: `false`) | Allows search on encrypted data | +### Nested Shape: `EncryptionConfig.keyManagement` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **provider** | `Enum<'local' \| 'aws-kms' \| 'azure-key-vault' \| 'gcp-kms' \| 'hashicorp-vault'>` | ✅ | Key management service provider | +| **keyId** | `string` | optional | Key identifier in the provider | +| **rotationPolicy** | `{ enabled: boolean; frequencyDays: number; retainOldVersions: number; autoRotate: boolean }` | optional | Key rotation policy | + --- @@ -67,6 +75,17 @@ Per-field encryption assignment | **encryptionConfig** | `{ enabled: boolean; algorithm: Enum<'aes-256-gcm' \| 'aes-256-cbc' \| 'chacha20-poly1305'>; keyManagement: object; scope: Enum<'field' \| 'record' \| 'table' \| 'database'>; … }` | ✅ | Encryption settings for this field | | **indexable** | `boolean` | optional (default: `false`) | Allow indexing on encrypted field | +### Nested Shape: `FieldEncryption.encryptionConfig` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `false`) | Enable field-level encryption | +| **algorithm** | `Enum<'aes-256-gcm' \| 'aes-256-cbc' \| 'chacha20-poly1305'>` | optional (default: `"aes-256-gcm"`) | Encryption algorithm | +| **keyManagement** | `{ provider: Enum<'local' \| 'aws-kms' \| 'azure-key-vault' \| 'gcp-kms' \| 'hashicorp-vault'>; keyId?: string; rotationPolicy?: object }` | ✅ | Key management configuration | +| **scope** | `Enum<'field' \| 'record' \| 'table' \| 'database'>` | ✅ | Encryption scope level | +| **deterministicEncryption** | `boolean` | optional (default: `false`) | Allows equality queries on encrypted data | +| **searchableEncryption** | `boolean` | optional (default: `false`) | Allows search on encrypted data | + --- diff --git a/content/docs/references/system/http-server.mdx b/content/docs/references/system/http-server.mdx index eef0556bb9..6aff59e4c6 100644 --- a/content/docs/references/system/http-server.mdx +++ b/content/docs/references/system/http-server.mdx @@ -43,6 +43,13 @@ const result = MiddlewareConfigSchema.parse(data); | **config** | `Record` | optional | Middleware configuration object | | **paths** | `{ include?: string[]; exclude?: string[] }` | optional | Path filtering | +### Nested Shape: `MiddlewareConfig.paths` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **include** | `string[]` | optional | Include path patterns (glob) | +| **exclude** | `string[]` | optional | Exclude path patterns (glob) | + --- @@ -73,6 +80,23 @@ const result = MiddlewareConfigSchema.parse(data); | **metadata** | `{ summary?: string; description?: string; tags?: string[]; operationId?: string }` | optional | | | **security** | `{ authRequired: boolean; permissions?: string[]; rateLimit?: string }` | optional | | +### Nested Shape: `RouteHandlerMetadata.metadata` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **summary** | `string` | optional | Route summary for documentation | +| **description** | `string` | optional | Route description | +| **tags** | `string[]` | optional | Tags for grouping | +| **operationId** | `string` | optional | Unique operation identifier | + +### Nested Shape: `RouteHandlerMetadata.security` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **authRequired** | `boolean` | optional (default: `true`) | Require authentication | +| **permissions** | `string[]` | optional | Required permissions | +| **rateLimit** | `string` | optional | Rate limit policy override | + --- diff --git a/content/docs/references/system/incident-response.mdx b/content/docs/references/system/incident-response.mdx index 02f58d7b4f..5054fbed4d 100644 --- a/content/docs/references/system/incident-response.mdx +++ b/content/docs/references/system/incident-response.mdx @@ -72,6 +72,19 @@ Security incident record per ISO 27001:2022 A.5.24–A.5.28 * `policy_violation` * `other` +### Nested Shape: `Incident.responsePhases[number]` + +Incident response phase with timing and assignment + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **phase** | `Enum<'identification' \| 'containment' \| 'eradication' \| 'recovery' \| 'lessons_learned'>` | ✅ | Response phase name | +| **description** | `string` | ✅ | Phase description and objectives | +| **assignedTo** | `string` | ✅ | Responsible team or role | +| **targetHours** | `number` | ✅ | Target completion time in hours | +| **completedAt** | `number` | optional | Actual completion timestamp | +| **notes** | `string` | optional | Phase notes and findings | + --- @@ -106,6 +119,19 @@ Incident notification matrix with escalation policies | **escalationTimeoutMinutes** | `number` | optional (default: `30`) | Auto-escalation timeout in minutes | | **escalationChain** | `string[]` | optional (default: `[]`) | Ordered escalation chain of roles | +### Nested Shape: `IncidentNotificationMatrix.rules[number]` + +Incident notification rule per severity level + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **severity** | `Enum<'critical' \| 'high' \| 'medium' \| 'low'>` | ✅ | Minimum severity to trigger notification | +| **channels** | `Enum<'email' \| 'sms' \| 'slack' \| 'pagerduty' \| 'webhook'>[]` | ✅ | Notification channels | +| **recipients** | `string[]` | ✅ | Roles or teams to notify | +| **withinMinutes** | `number` | ✅ | Notification deadline in minutes from detection | +| **notifyRegulators** | `boolean` | optional (default: `false`) | Whether to notify regulatory authorities | +| **regulatorDeadlineHours** | `number` | optional | Regulatory notification deadline in hours | + --- @@ -161,6 +187,14 @@ Organization-level incident response policy per ISO 27001:2022 | **regulatoryNotificationThreshold** | `Enum<'critical' \| 'high' \| 'medium' \| 'low'>` | optional (default: `"high"`) | Minimum severity requiring regulatory notification | | **retentionDays** | `number` | optional (default: `2555`) | Incident record retention period in days (default ~7 years) | +### Nested Shape: `IncidentResponsePolicy.notificationMatrix` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **rules** | `{ severity: Enum<'critical' \| 'high' \| 'medium' \| 'low'>; channels: Enum<'email' \| 'sms' \| 'slack' \| 'pagerduty' \| 'webhook'>[]; recipients: string[]; withinMinutes: number; … }[]` | ✅ | Notification rules by severity level | +| **escalationTimeoutMinutes** | `number` | optional (default: `30`) | Auto-escalation timeout in minutes | +| **escalationChain** | `string[]` | optional (default: `[]`) | Ordered escalation chain of roles | + --- diff --git a/content/docs/references/system/job.mdx b/content/docs/references/system/job.mdx index c2be0d4d48..a38d332ab0 100644 --- a/content/docs/references/system/job.mdx +++ b/content/docs/references/system/job.mdx @@ -71,6 +71,17 @@ const result = CronScheduleSchema.parse(data); | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `Job.retryPolicy` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **maxRetries** | `integer` | optional (default: `0`) | Retry attempts after the initial one. 0 (the default) means no retry — state a count to opt in. | +| **backoffMs** | `integer` | optional (default: `1000`) | Base delay before the first retry (ms); subsequent delays multiply by backoffMultiplier | +| **backoffMultiplier** | `number` | optional (default: `1`) | Exponential backoff multiplier; 1 (the default) keeps the delay flat | +| **maxRetryDelayMs** | `integer` | optional (default: `30000`) | Ceiling for a single backoff delay (ms) | +| **jitter** | `boolean` | optional (default: `false`) | Randomize each delay within [50%, 100%] of its computed value — spreads a thundering herd of simultaneous retries | +| **retryDelayMs** | `never` | optional | [REMOVED] `retryDelayMs` was removed in @objectstack/spec 17.0.0 (#4661, #4964) — the retry policy now has ONE spelling for its base delay across every surface that carries it: `job.retryPolicy`, a `try_catch` node's `retry` and `flow.errorHandling`. Rename the key to `backoffMs`; the value (milliseconds before the first retry) is unchanged. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | + --- diff --git a/content/docs/references/system/logging.mdx b/content/docs/references/system/logging.mdx index a7375e6513..a07a4b6c9b 100644 --- a/content/docs/references/system/logging.mdx +++ b/content/docs/references/system/logging.mdx @@ -113,6 +113,17 @@ HTTP destination configuration | **retry** | `{ maxAttempts: integer; initialDelay: integer; backoffMultiplier: number }` | optional | | | **timeout** | `integer` | optional (default: `30000`) | | +### Nested Shape: `HttpDestinationConfig.auth` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'basic' \| 'bearer' \| 'api_key'>` | ✅ | Auth type | +| **username** | `string` | optional | | +| **password** | `string` | optional | | +| **token** | `string` | optional | | +| **apiKey** | `string` | optional | | +| **apiKeyHeader** | `string` | optional (default: `"X-API-Key"`) | | + --- @@ -135,6 +146,27 @@ Log destination configuration | **format** | `Enum<'json' \| 'text' \| 'pretty'>` | optional (default: `"json"`) | | | **filterId** | `string` | optional | Filter function identifier | +### Nested Shape: `LogDestination.file` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **path** | `string` | ✅ | Log file path | +| **rotation** | `{ maxSize: string; maxFiles: integer; compress: boolean; interval?: Enum<'hourly' \| 'daily' \| 'weekly' \| 'monthly'> }` | optional | | +| **encoding** | `string` | optional (default: `"utf8"`) | | +| **append** | `boolean` | optional (default: `true`) | | + +### Nested Shape: `LogDestination.http` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **url** | `string` | ✅ | HTTP endpoint URL | +| **method** | `Enum<'POST' \| 'PUT'>` | optional (default: `"POST"`) | | +| **headers** | `Record` | optional | | +| **auth** | `{ type: Enum<'basic' \| 'bearer' \| 'api_key'>; username?: string; password?: string; token?: string; … }` | optional | | +| **batch** | `{ maxSize: integer; flushInterval: integer }` | optional | | +| **retry** | `{ maxAttempts: integer; initialDelay: integer; backoffMultiplier: number }` | optional | | +| **timeout** | `integer` | optional (default: `30000`) | | + --- @@ -268,6 +300,60 @@ Logging configuration | **buffer** | `{ enabled: boolean; size: integer; flushInterval: integer; flushOnShutdown: boolean }` | optional | | | **performance** | `{ async: boolean; workers: integer }` | optional | | +### Nested Shape: `LoggingConfig.default` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional | Logger name identifier | +| **level** | `Enum<'debug' \| 'info' \| 'warn' \| 'error' \| 'fatal' \| 'silent'>` | optional (default: `"info"`) | Log severity level | +| **format** | `Enum<'json' \| 'text' \| 'pretty'>` | optional (default: `"json"`) | Log output format | +| **redact** | `string[]` | optional (default: `["password","token","secret","key"]`) | Keys to redact from log context | +| **sourceLocation** | `boolean` | optional (default: `false`) | Include file and line number | +| **file** | `string` | optional | Path to log file | +| **rotation** | `{ maxSize: string; maxFiles: number }` | optional | | + +### Nested Shape: `LoggingConfig.loggers[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional | Logger name identifier | +| **level** | `Enum<'debug' \| 'info' \| 'warn' \| 'error' \| 'fatal' \| 'silent'>` | optional (default: `"info"`) | Log severity level | +| **format** | `Enum<'json' \| 'text' \| 'pretty'>` | optional (default: `"json"`) | Log output format | +| **redact** | `string[]` | optional (default: `["password","token","secret","key"]`) | Keys to redact from log context | +| **sourceLocation** | `boolean` | optional (default: `false`) | Include file and line number | +| **file** | `string` | optional | Path to log file | +| **rotation** | `{ maxSize: string; maxFiles: number }` | optional | | + +### Nested Shape: `LoggingConfig.destinations[number]` + +Log destination configuration + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Destination name (snake_case) | +| **type** | `Enum<'console' \| 'file' \| 'syslog' \| 'elasticsearch' \| 'cloudwatch' \| 'stackdriver' \| …>` | ✅ | Destination type | +| **level** | `Enum<'trace' \| 'debug' \| 'info' \| 'warn' \| 'error' \| 'fatal'>` | optional (default: `"info"`) | Extended log severity level | +| **enabled** | `boolean` | optional (default: `true`) | | +| **console** | `{ stream: Enum<'stdout' \| 'stderr'>; colors: boolean; prettyPrint: boolean }` | optional | Console destination configuration | +| **file** | `{ path: string; rotation?: object; encoding: string; append: boolean }` | optional | File destination configuration | +| **http** | `{ url: string; method: Enum<'POST' \| 'PUT'>; headers?: Record; auth?: object; … }` | optional | HTTP destination configuration | +| **externalService** | `{ endpoint?: string; region?: string; credentials?: object; logGroup?: string; … }` | optional | External service destination configuration | +| **format** | `Enum<'json' \| 'text' \| 'pretty'>` | optional (default: `"json"`) | | +| **filterId** | `string` | optional | Filter function identifier | + +### Nested Shape: `LoggingConfig.enrichment` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **staticFields** | `Record` | optional | Static fields added to every log | +| **dynamicEnrichers** | `string[]` | optional | Dynamic enricher function IDs | +| **addHostname** | `boolean` | optional (default: `true`) | | +| **addProcessId** | `boolean` | optional (default: `true`) | | +| **addEnvironment** | `boolean` | optional (default: `true`) | | +| **addTimestampFormats** | `{ unix: boolean; iso: boolean }` | optional | | +| **addCaller** | `boolean` | optional (default: `false`) | | +| **addCorrelationIds** | `boolean` | optional (default: `true`) | | + --- @@ -293,6 +379,25 @@ Structured log entry | **labels** | `Record` | optional | Custom labels | | **metadata** | `Record` | optional | Additional metadata | +### Nested Shape: `StructuredLogEntry.trace` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **traceId** | `string` | ✅ | Trace ID | +| **spanId** | `string` | ✅ | Span ID | +| **parentSpanId** | `string` | optional | Parent span ID | +| **traceFlags** | `integer` | optional | Trace flags | + +### Nested Shape: `StructuredLogEntry.source` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **service** | `string` | optional | Service name | +| **component** | `string` | optional | Component name | +| **file** | `string` | optional | Source file | +| **line** | `integer` | optional | Line number | +| **function** | `string` | optional | Function name | + --- diff --git a/content/docs/references/system/metadata-persistence.mdx b/content/docs/references/system/metadata-persistence.mdx index 30d51fb3c1..a5fa30a425 100644 --- a/content/docs/references/system/metadata-persistence.mdx +++ b/content/docs/references/system/metadata-persistence.mdx @@ -107,6 +107,24 @@ Metadata file format | **total** | `integer` | ✅ | | | **hasMore** | `boolean` | ✅ | | +### Nested Shape: `MetadataHistoryQueryResult.records[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | | +| **name** | `string` | ✅ | | +| **type** | `string` | ✅ | | +| **version** | `number` | ✅ | Version number at this snapshot | +| **operationType** | `Enum<'create' \| 'update' \| 'publish' \| 'revert' \| 'delete'>` | ✅ | Type of operation that created this history entry | +| **metadata** | `string \| Record \| null` | optional | Snapshot of metadata definition at this version (raw JSON string or parsed object) | +| **checksum** | `string` | ✅ | SHA-256 checksum of metadata content | +| **previousChecksum** | `string` | optional | Checksum of the previous version | +| **changeNote** | `string` | optional | Description of changes made in this version | +| **organizationId** | `string` | optional | Organization identifier for multi-tenant isolation | +| **environmentId** | `string` | optional | Deprecated (ADR-0006 v4): legacy environment_id column. New writes leave unset. | +| **recordedBy** | `string` | optional | User who made this change | +| **recordedAt** | `string` | ✅ | Timestamp when this version was recorded | + --- @@ -183,6 +201,18 @@ Metadata file format | **notModified** | `boolean` | optional | | | **loadTime** | `number` | optional | | +### Nested Shape: `MetadataLoadResult.stats` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **path** | `string` | optional | | +| **size** | `number` | optional | | +| **mtime** | `string` | optional | | +| **hash** | `string` | optional | | +| **etag** | `string` | optional | | +| **modifiedAt** | `string` | optional | | +| **format** | `Enum<'yaml' \| 'json' \| 'typescript' \| 'javascript'>` | optional | Metadata file format | + --- @@ -222,6 +252,37 @@ Metadata file format | **loaderOptions** | `Record` | optional | Loader-specific configuration | | **persistence** | `{ writable: boolean; overlayWritable: boolean }` | optional | Persistence write gates | +### Nested Shape: `MetadataManagerConfig.cache` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | Enable caching | +| **ttl** | `integer` | optional (default: `3600`) | Cache TTL in seconds | +| **maxSize** | `integer` | optional | Max cache size in bytes | +| **databaseLoader** | `{ enabled: boolean; maxSize: integer; ttl: integer }` | optional | DatabaseLoader read-through cache | + +### Nested Shape: `MetadataManagerConfig.watchOptions` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ignored** | `string[]` | optional | Patterns to ignore | +| **persistent** | `boolean` | optional (default: `true`) | Keep process running | +| **ignoreInitial** | `boolean` | optional (default: `true`) | Ignore initial add events | + +### Nested Shape: `MetadataManagerConfig.validation` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **strict** | `boolean` | optional (default: `true`) | Strict validation | +| **throwOnError** | `boolean` | optional (default: `true`) | Throw on validation error | + +### Nested Shape: `MetadataManagerConfig.persistence` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **writable** | `boolean` | optional (default: `true`) | Allow base metadata writes via register() | +| **overlayWritable** | `boolean` | optional (default: `true`) | Allow overlay writes via saveOverlay() | + --- @@ -295,6 +356,18 @@ Metadata file format | **saveTime** | `number` | optional | | | **backupPath** | `string` | optional | | +### Nested Shape: `MetadataSaveResult.stats` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **path** | `string` | optional | | +| **size** | `number` | optional | | +| **mtime** | `string` | optional | | +| **hash** | `string` | optional | | +| **etag** | `string` | optional | | +| **modifiedAt** | `string` | optional | | +| **format** | `Enum<'yaml' \| 'json' \| 'typescript' \| 'javascript'>` | optional | Metadata file format | + --- @@ -364,6 +437,18 @@ Metadata file format | **data** | `any` | optional | | | **timestamp** | `string` | optional | | +### Nested Shape: `MetadataWatchEvent.stats` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **path** | `string` | optional | | +| **size** | `number` | optional | | +| **mtime** | `string` | optional | | +| **hash** | `string` | optional | | +| **etag** | `string` | optional | | +| **modifiedAt** | `string` | optional | | +| **format** | `Enum<'yaml' \| 'json' \| 'typescript' \| 'javascript'>` | optional | Metadata file format | + --- @@ -380,6 +465,14 @@ Metadata file format | **itemsPublished** | `integer` | ✅ | Total metadata items published | | **validationErrors** | `{ type: string; name: string; message: string }[]` | optional | Validation errors if publish failed | +### Nested Shape: `PackagePublishResult.validationErrors[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `string` | ✅ | Metadata type that failed validation | +| **name** | `string` | ✅ | Item name that failed validation | +| **message** | `string` | ✅ | Validation error message | + --- diff --git a/content/docs/references/system/metrics.mdx b/content/docs/references/system/metrics.mdx index 8bb9cdcb16..9db2e67afa 100644 --- a/content/docs/references/system/metrics.mdx +++ b/content/docs/references/system/metrics.mdx @@ -43,6 +43,28 @@ Histogram bucket configuration | **exponential** | `{ start: number; factor: number; count: integer }` | optional | | | **explicit** | `{ boundaries: number[] }` | optional | | +### Nested Shape: `HistogramBucketConfig.linear` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **start** | `number` | ✅ | Start value | +| **width** | `number` | ✅ | Bucket width | +| **count** | `integer` | ✅ | Number of buckets | + +### Nested Shape: `HistogramBucketConfig.exponential` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **start** | `number` | ✅ | Start value | +| **factor** | `number` | ✅ | Growth factor | +| **count** | `integer` | ✅ | Number of buckets | + +### Nested Shape: `HistogramBucketConfig.explicit` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **boundaries** | `number[]` | ✅ | Bucket boundaries | + --- @@ -59,6 +81,14 @@ Metric aggregation configuration | **groupBy** | `string[]` | optional | Group by label names | | **filters** | `Record` | optional | Filter criteria | +### Nested Shape: `MetricAggregationConfig.window` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **size** | `integer` | ✅ | Window size in seconds | +| **sliding** | `boolean` | optional (default: `false`) | | +| **slideInterval** | `integer` | optional | | + --- @@ -101,6 +131,22 @@ Metric data point | **histogram** | `{ count: integer; sum: number; buckets: object[] }` | optional | | | **summary** | `{ count: integer; sum: number; quantiles: object[] }` | optional | | +### Nested Shape: `MetricDataPoint.histogram` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **count** | `integer` | ✅ | Total count | +| **sum** | `number` | ✅ | Sum of all values | +| **buckets** | `{ upperBound: number; count: integer }[]` | ✅ | Histogram buckets | + +### Nested Shape: `MetricDataPoint.summary` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **count** | `integer` | ✅ | Total count | +| **sum** | `number` | ✅ | Sum of all values | +| **quantiles** | `{ quantile: number; value: number }[]` | ✅ | Summary quantiles | + --- @@ -145,6 +191,15 @@ Metric definition * `operations` * `custom` +### Nested Shape: `MetricDefinition.histogram` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'linear' \| 'exponential' \| 'explicit'>` | ✅ | Bucket type | +| **linear** | `{ start: number; width: number; count: integer }` | optional | | +| **exponential** | `{ start: number; factor: number; count: integer }` | optional | | +| **explicit** | `{ boundaries: number[] }` | optional | | + --- @@ -163,6 +218,16 @@ Metric export configuration | **auth** | `{ type: Enum<'none' \| 'basic' \| 'bearer' \| 'api_key'>; username?: string; password?: string; token?: string; … }` | optional | | | **config** | `Record` | optional | Additional configuration | +### Nested Shape: `MetricExportConfig.auth` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'none' \| 'basic' \| 'bearer' \| 'api_key'>` | ✅ | Auth type | +| **username** | `string` | optional | | +| **password** | `string` | optional | | +| **token** | `string` | optional | | +| **apiKey** | `string` | optional | | + --- @@ -240,6 +305,77 @@ Metrics configuration | **retention** | `{ period?: integer; downsampling?: object[] }` | optional | | | **cardinalityLimits** | `{ maxLabelCombinations?: integer; onLimitExceeded?: Enum<'drop' \| 'sample' \| 'alert'> }` | optional | | +### Nested Shape: `MetricsConfig.metrics[number]` + +Metric definition + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Metric name (snake_case) | +| **label** | `string` | optional | Display label | +| **type** | `Enum<'counter' \| 'gauge' \| 'histogram' \| 'summary'>` | ✅ | Metric type | +| **unit** | `Enum<'nanoseconds' \| 'microseconds' \| 'milliseconds' \| 'seconds' \| 'minutes' \| …>` | optional | Metric unit | +| **description** | `string` | optional | Metric description | +| **labelNames** | `string[]` | optional (default: `[]`) | Label names | +| **histogram** | `{ type: Enum<'linear' \| 'exponential' \| 'explicit'>; linear?: object; exponential?: object; explicit?: object }` | optional | Histogram bucket configuration | +| **summary** | `{ quantiles?: number[]; maxAge?: integer; ageBuckets?: integer }` | optional | | +| **enabled** | `boolean` | optional (default: `true`) | | + +### Nested Shape: `MetricsConfig.aggregations[number]` + +Metric aggregation configuration + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'sum' \| 'avg' \| 'min' \| 'max' \| 'count' \| 'p50' \| 'p75' \| 'p90' \| 'p95' \| 'p99' \| …>` | ✅ | Aggregation type | +| **window** | `{ size: integer; sliding?: boolean; slideInterval?: integer }` | optional | | +| **groupBy** | `string[]` | optional | Group by label names | +| **filters** | `Record` | optional | Filter criteria | + +### Nested Shape: `MetricsConfig.slis[number]` + +Service Level Indicator + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | SLI name (snake_case) | +| **label** | `string` | ✅ | Display label | +| **description** | `string` | optional | SLI description | +| **metric** | `string` | ✅ | Base metric name | +| **type** | `Enum<'availability' \| 'latency' \| 'throughput' \| 'error_rate' \| 'saturation' \| 'custom'>` | ✅ | SLI type | +| **successCriteria** | `{ threshold: number; operator: Enum<'lt' \| 'lte' \| 'gt' \| 'gte' \| 'eq'>; percentile?: number } \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | ✅ | Success criteria — structured or CEL predicate | +| **window** | `{ size: integer; rolling?: boolean }` | ✅ | Measurement window | +| **enabled** | `boolean` | optional (default: `true`) | | + +### Nested Shape: `MetricsConfig.slos[number]` + +Service Level Objective + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | SLO name (snake_case) | +| **label** | `string` | ✅ | Display label | +| **description** | `string` | optional | SLO description | +| **sli** | `string` | ✅ | SLI name | +| **target** | `number` | ✅ | Target percentage | +| **period** | `{ type: Enum<'rolling' \| 'calendar'>; duration?: integer; calendar?: Enum<'daily' \| 'weekly' \| 'monthly' \| 'quarterly' \| 'yearly'> }` | ✅ | Time period | +| **errorBudget** | `{ enabled?: boolean; alertThreshold?: number; burnRateWindows?: object[] }` | optional | | +| **alerts** | `{ name: string; severity: Enum<'info' \| 'warning' \| 'critical'>; condition: object }[]` | optional (default: `[]`) | | +| **enabled** | `boolean` | optional (default: `true`) | | + +### Nested Shape: `MetricsConfig.exports[number]` + +Metric export configuration + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'prometheus' \| 'openmetrics' \| 'graphite' \| 'statsd' \| 'influxdb' \| 'datadog' \| …>` | ✅ | Export type | +| **endpoint** | `string` | optional | Export endpoint | +| **interval** | `integer` | optional (default: `60`) | | +| **batch** | `{ enabled?: boolean; size?: integer }` | optional | | +| **auth** | `{ type: Enum<'none' \| 'basic' \| 'bearer' \| 'api_key'>; username?: string; password?: string; token?: string; … }` | optional | | +| **config** | `Record` | optional | Additional configuration | + --- @@ -260,6 +396,13 @@ Service Level Indicator | **window** | `{ size: integer; rolling?: boolean }` | ✅ | Measurement window | | **enabled** | `boolean` | optional (default: `true`) | | +### Nested Shape: `ServiceLevelIndicator.window` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **size** | `integer` | ✅ | Window size in seconds | +| **rolling** | `boolean` | optional (default: `true`) | | + --- @@ -281,6 +424,22 @@ Service Level Objective | **alerts** | `{ name: string; severity: Enum<'info' \| 'warning' \| 'critical'>; condition: object }[]` | optional (default: `[]`) | | | **enabled** | `boolean` | optional (default: `true`) | | +### Nested Shape: `ServiceLevelObjective.period` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'rolling' \| 'calendar'>` | ✅ | Period type | +| **duration** | `integer` | optional | Duration in seconds | +| **calendar** | `Enum<'daily' \| 'weekly' \| 'monthly' \| 'quarterly' \| 'yearly'>` | optional | | + +### Nested Shape: `ServiceLevelObjective.alerts[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Alert name | +| **severity** | `Enum<'info' \| 'warning' \| 'critical'>` | ✅ | Alert severity | +| **condition** | `{ type: Enum<'slo_breach' \| 'error_budget' \| 'burn_rate'>; threshold?: number }` | ✅ | Alert condition | + --- @@ -298,6 +457,16 @@ Time series | **startTime** | `string` | optional | Start time | | **endTime** | `string` | optional | End time | +### Nested Shape: `TimeSeries.dataPoints[number]` + +Time series data point + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **timestamp** | `string` | ✅ | Timestamp | +| **value** | `number` | ✅ | Value | +| **labels** | `Record` | optional | Labels | + --- diff --git a/content/docs/references/system/migration.mdx b/content/docs/references/system/migration.mdx index a131a98dae..73da0b552b 100644 --- a/content/docs/references/system/migration.mdx +++ b/content/docs/references/system/migration.mdx @@ -48,6 +48,83 @@ Add a new field to an existing object | **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: `AddFieldOperation.field` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional | Machine name (snake_case) | +| **label** | `string` | optional | Human readable label | +| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>` | ✅ | Field Data Type | +| **description** | `string` | optional | Tooltip/Help text | +| **format** | `string` | optional | Format string (e.g. email, phone) | +| **required** | `boolean` | optional (default: `false`) | Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. On a multi-value lookup (`multiple: true`) required means NON-EMPTY array — an emptied required set fails validation loudly; `[]` does not satisfy it (#9447, maintainer ruling 2026-08-18). NOT a column constraint — the physical NOT NULL is a separate explicit opt-in (`storage.notNull`), so tightening this on a deployed object is safe: existing null rows stay readable, and editable as long as the write does not touch this field. | +| **storage** | `{ notNull?: boolean }` | optional | Physical storage constraints (ADR-0113). Owns the DDL the write contract deliberately does not imply. Absent = no storage-level constraint requested. | +| **searchable** | `boolean` | optional (default: `false`) | Is searchable | +| **multiple** | `boolean` | optional (default: `false`) | Allow multiple values (Stores as Array/JSON). Applicable for select, lookup, file, image. An emptied multi-value lookup reads back as `[]`, never `null` — the rule binds every writer (cascade repair, form clears, API writes), not just cascade repair (#9447, maintainer ruling 2026-08-18). | +| **unique** | `boolean \| 'global' \| 'organization'` | optional (default: `false`) | Unique constraint and its scope (ADR-0120). 'organization' = one holder per organization (NULL-safe composite with the organization key part on organization-scoped objects) — prefer this explicit spelling in new code; true = same per-organization scope (positional synonym, stays valid); 'global' = one holder across the whole installation. 'tenant'/'org' are rejected — the word is 'organization' | +| **defaultValue** | `any` | optional | Default applied on INSERT when the field is omitted or null (`''` is a real value, not absence). Three legal shapes (#7127), discriminated in the engine's own order: a CEL Expression envelope `{ dialect: 'cel', source: 'today()' }` (accepted structurally; result type is a runtime concern); a runtime TOKEN — `NOW()` on `datetime`/`date`/`time` only, `current_user` on `user` or `lookup` with `reference: 'sys_user'` only, neither on a multi-value field; or a LITERAL, which must satisfy this field's own stored value contract (ADR-0104 D1 `valueSchemaFor`). Anything else is refused at parse time with a prescriptive message. | +| **maxLength** | `integer` | optional | Max character length (positive integer). Only authorable on types that store a bounded string: text, textarea, email, url, phone, password, markdown, html, richtext, code, signature, qrcode. | +| **minLength** | `integer` | optional | Min character length (positive integer; `minLength: 0` is refused — express "no minimum" by omitting the key). Only authorable on types that store a bounded string: text, textarea, email, url, phone, password, markdown, html, richtext, code, signature, qrcode. | +| **precision** | `integer` | optional | Total digits (non-negative integer) | +| **scale** | `integer` | optional | Decimal places (non-negative integer) | +| **min** | `number` | optional | Minimum value | +| **max** | `number` | optional | Maximum value | +| **useGrouping** | `boolean` | optional | Digit-grouping presentation hint for `number` fields (#7768) — maps to `Intl.NumberFormat`'s `useGrouping`. Absent = renderer decides (interim heuristic today, locale default eventually); `false` = author opts out of grouping (e.g. a year or other ordinal/identifier integer); `true` = author pins grouping on. | +| **accept** | `string[]` | optional | Permitted upload types for media fields, as MIME types or extensions (e.g. ["image/*", ".pdf"]). Offered to the file picker AND enforced on write. | +| **maxSize** | `integer` | optional | Maximum permitted file size in BYTES for media fields. Enforced on write against the stored file size, not just checked in the browser. | +| **options** | `{ label: string; value: string; color?: string; default?: boolean; … }[]` | optional | Static options for select/multiselect | +| **reference** | `string` | optional | Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects. | +| **referenceVia** | `string` | optional | Declares this text field as the id half of a polymorphic pointer pair (ADR-0052 §5 ActivityPointer): the value is a record id of the object named by the SIBLING FIELD this key names — e.g. `record_id` with `referenceVia: 'object_name'`. The sibling must be a declared field on the same object holding an object machine name. Text fields only; mutually exclusive with `reference` (a static and a per-record target contradict). Enforced today at seed load: the value resolves as a natural key against the object the sibling column names, and an unresolvable pointer is refused loudly instead of stored verbatim. Adds no referential integrity or $expand behavior. | +| **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted | +| **inlineEdit** | `boolean \| Enum<'grid' \| 'form'>` | optional | Edit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form. | +| **inlineTitle** | `string` | optional | Title for the inline master-detail grid | +| **inlineColumns** | `{ name: string; label?: string; type?: Enum<'text' \| 'number' \| 'currency' \| 'date' \| 'datetime' \| 'time' \| 'select' \| 'lookup' \| 'file'>; width?: number; … }[]` | optional | Explicit columns for the inline grid (derived from the child object when omitted). Each entry is a strict, name-keyed column (`{ name, label?, type?, … }` — objectui GridColumn, #3951); identity-only entries (`{ name }`) hydrate everything else from the child object's fields. Unknown keys and the retired `field` spelling are refused at parse. | +| **inlineAmountField** | `string` | optional | Numeric child field summed for the inline grid total | +| **relatedList** | `boolean \| 'primary'` | optional | Show this child collection as a related list on the parent's detail page (read-side mirror of inlineEdit). false = suppress; true/absent = shown (stacked under the shared "Related" tab); 'primary' = core relationship, promoted to its own tab. Prominence intent, not a layout switch (ADR-0085). | +| **relatedListTitle** | `string` | optional | Title for the detail-page related list | +| **relatedListColumns** | `string[]` | optional | Explicit columns for the detail-page related list, as child field names (e.g. ['name', 'status']); derived from the child object (highlightFields → field walk) when omitted. Strings only — labels, cell types and formatting always derive from the child object's field definitions; column objects are refused at parse. | +| **relatedListFilter** | `any` | optional | Declarative default filter for the detail-page related list: AND-composed with the parent-relationship condition `{ [referenceField]: parentId }` — an authored constraint, never a user-editable suggestion. The related-list tab badge count honors the same composed filter, so counts match the visible rows. Canonical Query-DSL FilterCondition (the same dialect as a query `where`), e.g. `{ status: { $ne: 'deleted' } }` to hide soft-deleted children. | +| **displayField** | `string` | optional | Field shown as each candidate's label in the picker/popover (defaults to the referenced object's name/title). | +| **descriptionField** | `string` | optional | Secondary field shown under the label in the quick-select popover. | +| **lookupColumns** | `(string \| { field: string; label?: string; width?: string; type?: string })[]` | optional | Explicit columns for the record-picker table; auto-derived from the referenced object when omitted. | +| **lookupPageSize** | `integer` | optional | Rows per page in the record-picker dialog (default 10). | +| **lookupFilters** | `{ field: string; operator: Enum<'eq' \| 'ne' \| 'gt' \| 'lt' \| 'gte' \| 'lte' \| 'contains' \| 'in' \| 'notIn'>; value: any }[]` | optional | Base filters restricting which records are selectable (e.g. only active). The structured, picker-honoured lookup filter. | +| **dependsOn** | `(string \| { field: string; param?: string })[]` | optional | Declares that this field's available values depend on the value of other field(s) on the same record — the form gates the field until they are set and re-evaluates as they change. For `lookup`/`master_detail` it scopes the candidate query (string = same local/remote key; `{field,param}` when the remote filter key differs — the `{field,param}` form is lookup-only). For `select`/`multiselect`/`radio` the actual per-option rule lives in each option's `visibleWhen`; list the referenced fields here (string form) so the option list gates and refreshes with the parent. | +| **allowCreate** | `boolean` | optional | Allow inline quick-create from the record picker: when no match exists the user can create a record from the typed text (optimistic dataSource.create with the display field). Best for simple objects whose only required field is the display field. | +| **expression** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Formula expression (CEL). e.g. F`record.amount * 0.1` | +| **returnType** | `Enum<'number' \| 'text' \| 'boolean' \| 'date'>` | optional | Inferred value type of a formula field (number/text/boolean/date) | +| **summaryOperations** | `{ object: string; field: string; function: Enum<'count' \| 'sum' \| 'min' \| 'max' \| 'avg'>; relationshipField?: string; … }` | optional | Roll-up summary definition. The engine recomputes the value when child records are inserted/updated/deleted. | +| **language** | `string` | optional | Programming language for syntax highlighting (e.g., javascript, python, sql) | +| **step** | `number` | optional | Step increment for slider (default: 1) | +| **currencyConfig** | `{ precision?: integer; currencyMode?: Enum<'dynamic' \| 'fixed'>; defaultCurrency?: string }` | optional | Configuration for currency field type | +| **dimensions** | `integer` | optional | Vector dimensionality (e.g., 1536 for OpenAI embeddings) | +| **trackHistory** | `boolean` | optional | Render this field's value changes as human-readable entries on the record activity timeline (ADR-0052 §5b). Opt-in per field. | +| **group** | `string` | optional | Field group name for organizing fields in forms and layouts (e.g., "contact_info", "billing", "system") | +| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is shown only when TRUE (else hidden). e.g. P`record.type == 'invoice'` | +| **readonlyWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is read-only when TRUE. e.g. P`record.status == 'paid'` | +| **requiredWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is required when TRUE. The only slot; the `conditionalRequired` alias was removed in protocol 17 (#3855). | +| **conditionalRequired** | `never` | optional | [REMOVED] `conditionalRequired` was removed in @objectstack/spec 17 (#3855) — use `requiredWhen`. Rename the key; the value (a CEL predicate) is unchanged. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **widget** | `string` | optional | Form widget override — names a registered field component (resolved as `field:`) to render this field instead of the `type` default. Degrades to the `type` renderer when unregistered. e.g. "object-ref", "filter-condition", "recipient-picker". | +| **hidden** | `boolean` | optional (default: `false`) | Hidden from default UI | +| **internal** | `boolean` | optional | [#7728] Never return this field's value on the generic data path — the engine OMITS the key from `find`/`findOne` results, the 201 create body and the by-id update body, on the default projection AND when a client names the field in `?select=`. Storage, filtering and indexing are untouched, so a server-side verifier can still match on the column and a purpose-built mint route can still return the value once at creation. The read protection for ADR-0100's third credential channel (auth-subsystem one-way hashes on `text` columns). Omission, not masking: a mask signals 'a value is set', which carries no information on a `required` column. | +| **readonly** | `boolean` | optional (default: `false`) | Read-only — never editable in forms, AND server-enforced on BOTH write paths: a non-system write to this field is silently dropped from the payload on UPDATE (#2948/#3003) and on INSERT (#3043; a create can no longer directly seed e.g. `approval_status: "approved"`), symmetric with `readonlyWhen`. A stripped INSERT field still falls back to its `defaultValue`. Exempt from the strip on BOTH paths: `isSystem` writes (seed replay, migration). Exempt on the UPDATE path ONLY: an opt-in "historical" import (`preserveAudit`, #3493) — which admits a whitelist (the audit/timestamp family plus author-declared business `readonly` fields). On INSERT the exemption does NOT apply (#6640): a non-system create that requests `preserveAudit` still has its readonly fields stripped, and is warned loudly that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. A normal (non-system) import is NOT system-context and still strips. | +| **requiredPermissions** | `string[]` | optional | [ADR-0066 D3] Capabilities required to read/edit this field (mask on read, deny on write; AND-gate). | +| **maskingRule** | `Enum<'phone' \| 'id_card' \| 'bank_account' \| 'email' \| 'name'> \| { keepHead: integer; keepTail: integer }` | optional | [#8993] Partial masking rule enforced by the runtime FieldMasker (single channel — API, UI, export and AI context all see the same masked value). A named preset ('phone' 138****5678, 'id_card' keep 6+4, 'bank_account' keep last 4, 'email' j***@example.com, 'name' keep first char) or `{ keepHead, keepTail }`. Masked for every non-system caller unless the field's `requiredPermissions` are ALL held (that evaluation is the unmask gate); a permission set marking the field non-readable still deletes it entirely. Deterministic, length-preserving output; masked callers cannot filter/sort/group/aggregate on the field. | +| **ackPlaintextMasking** | `boolean` | optional | [ADR-0100] Affirm a generic `password` field's plaintext-at-rest / masked-on-read contract is intended, silencing the author-time warning (#3420). No effect on non-password fields. | +| **system** | `boolean` | optional | Auto-injected system/audit field (e.g. created_at, updated_by, organization_id). Tools that surface system fields separately from author-declared business fields should branch on this flag. | +| **sortable** | `boolean` | optional (default: `true`) | Whether field is sortable in list views | +| **inlineHelpText** | `string` | optional | Help text displayed below the field in forms | +| **placeholder** | `string` | optional | Placeholder text rendered inside the empty input (the HTML placeholder attribute); disappears once a value is entered. Distinct from `inlineHelpText` (always-visible help rendered beside/under the input) and `description` (tooltip/developer documentation). | +| **autonumberFormat** | `string` | optional (default: `"{0000}"`) | Auto-number format: literal text + `{0000}` counter, `{YYYY}`/`{MM}`/`{DD}`/`{YYYYMMDD}` date tokens (business tz), and `{field_name}` interpolation. Counter resets per rendered prefix (e.g. AD`{YYYYMMDD}``{0000}` resets daily). Omitted on an `autonumber` field ⇒ the contract default `{0000}` (#6555). | +| **externalId** | `boolean` | optional (default: `false`) | Is external ID for upsert operations | +| **_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. | + --- @@ -68,6 +145,15 @@ A versioned set of atomic schema migration operations | **operations** | `({ type: 'add_field'; objectName: string; fieldName: string; field: object } \| { type: 'modify_field'; objectName: string; fieldName: string; changes: Record } \| { type: 'remove_field'; objectName: string; fieldName: string } \| { type: 'create_object'; object: object } \| … +3 more)[]` | ✅ | Ordered list of atomic migration operations | | **rollback** | `({ type: 'add_field'; objectName: string; fieldName: string; field: object } \| { type: 'modify_field'; objectName: string; fieldName: string; changes: Record } \| { type: 'remove_field'; objectName: string; fieldName: string } \| { type: 'create_object'; object: object } \| … +3 more)[]` | optional | Operations to reverse this migration | +### Nested Shape: `ChangeSet.dependencies[number]` + +Dependency reference to another migration that must run first + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **migrationId** | `string` | ✅ | ID of the migration this depends on | +| **package** | `string` | optional | Package that owns the dependency migration | + --- @@ -82,6 +168,54 @@ Create a new object | **type** | `'create_object'` | ✅ | | | **object** | `{ name: string; label?: string; pluralLabel?: string; description?: string; … }` | ✅ | Full object definition to create | +### Nested Shape: `CreateObjectOperation.object` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Machine unique key (snake_case). Immutable. | +| **label** | `string` | optional | Human readable singular label (e.g. "Account") | +| **pluralLabel** | `string` | optional | Human readable plural label (e.g. "Accounts") | +| **description** | `string` | optional | Developer documentation / description | +| **icon** | `string` | optional | Icon name (Lucide/Material) for UI representation | +| **isSystem** | `boolean` | optional (default: `false`) | Is system object (protected from deletion; defaults its org-wide sharing to public when no sharingModel is set — plugin-sharing) | +| **managedBy** | `Enum<'platform' \| 'config' \| 'system-data' \| 'engine-owned' \| 'append-only' \| 'better-auth'>` | optional | Lifecycle bucket — platform (user CRUD) \| config (admin authored) \| system-data (platform-defined schema, admin/user-writable data) \| engine-owned (engine owns the lifecycle, no user writes) \| append-only (audit) \| better-auth (identity). UI clients honour the resolved affordance matrix. | +| **ownership** | `Enum<'user' \| 'business_unit' \| 'org' \| 'none'>` | optional | Record-ownership model: user (default — injects reassignable owner_id plus owning_business_unit_id) \| business_unit (unit-owned: owning_business_unit_id only, no owner_id) \| org \| none (no per-record owner, neither anchor). Distinct from the package own/extend contribution kind. | +| **userActions** | `{ create?: boolean \| object; import?: boolean \| object; edit?: boolean \| object; delete?: boolean \| object; … }` | optional | Per-object override of the resolved CRUD affordance matrix. | +| **systemFields** | `false \| { tenant?: boolean; audit?: boolean }` | optional | Opt out of, or selectively disable, registry-level system-field auto-injection. | +| **datasource** | `string` | optional (default: `"default"`) | Target Datasource ID. "default" is the primary DB. | +| **external** | `{ remoteName?: string; remoteSchema?: string; writable?: boolean; columnMap?: Record; … }` | optional | Remote table binding for federated (external) objects. | +| **fields** | `Record; description?: string; … }>` | ✅ | Field definitions map. Keys must be snake_case identifiers. | +| **indexes** | `{ name?: string; fields: string[]; unique?: boolean \| 'global' \| 'organization' }[]` | optional | Database performance indexes | +| **fieldGroups** | `{ key: string; label: string; icon?: string; description?: string; … }[]` | optional | Ordered list of field groups (array order = display order). See ObjectFieldGroupSchema. | +| **tenancy** | `{ enabled: boolean; tenantField?: string; organizationField?: string }` | optional | Multi-tenancy configuration for SaaS applications | +| **access** | `{ default?: Enum<'public' \| 'private'> }` | optional | [ADR-0066 D2] Object exposure posture (public-by-default vs private secure-by-default). | +| **requiredPermissions** | `string[] \| { read?: string[]; create?: string[]; update?: string[]; delete?: string[] }` | optional | [ADR-0066 D3/⑤] Capabilities required to access this object (AND-gate) — `string[]` gates all CRUD, or a `{read,create,update,delete}` map gates per operation. | +| **lifecycle** | `{ class: Enum<'record' \| 'audit' \| 'telemetry' \| 'transient' \| 'event'>; retention?: object; ttl?: object; storage?: object; … }` | optional | Data lifecycle contract (ADR-0057): class + retention/ttl/rotation/archive policies enforced by the platform LifecycleService. | +| **fileAccessDelegate** | `string` | optional | Kernel service that authorizes downloads of files owned by this object's media fields, instead of testing whether the caller can read the owning row. For objects whose access is mediated by a service (e.g. sys_approval_action → approvals). Fails closed. | +| **validations** | `any[]` | optional | Object-level validation rules | +| **activityMilestones** | `{ field: string; value: string; summary: string; type?: string }[]` | optional | Declarative semantic activity milestones — emit a templated timeline row when a field transitions into a value, no hook code (ADR-0052 §5b.2). | +| **nameField** | `string` | optional | [ADR-0079] Canonical primary title field — the stored field used as the record display name (e.g. "name", "title"). | +| **displayNameField** | `string` | optional | [DEPRECATED → nameField] Field to use as the record display name (e.g., "name", "title"). Accepted as an alias for nameField. | +| **titleFormat** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | [DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField. | +| **highlightFields** | `string[]` | optional | [ADR-0085] Ordered most-important fields; first entry wins where only one fits. Drives default columns, cards, previews, detail highlight strip. Renamed from compactLayout. | +| **stageField** | `string \| false` | optional | [ADR-0085] Lifecycle stage field (linear/ordered), or false to declare the status field non-linear and suppress stage heuristics. Absent = heuristic detection allowed. | +| **editMode** | `Enum<'modal' \| 'page'>` | optional | Edit-interaction intent for records of this object: 'modal' opens the edit form as a dialog over the current view; 'page' navigates to a dedicated full-page edit route. Absent = the renderer picks its own default (objectui defaults to modal). Cross-renderer intent, not pixel styling (#11408, #10144 family). | +| **listViews** | `Record; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: object \| … +3 more; … }>` | optional | Built-in named list views (segmented tabs) shipped with the object schema — "views" mode, dropdown userFilters allowed, no page-only tabs (ADR-0047) | +| **searchableFields** | `string[]` | optional | Fields the `$search` query matches against (ADR-0061). Canonical default for the record picker, list quick-search and global search; views may narrow it. When unset, search auto-defaults to the name/title field plus short-text fields. Entries must name a STORED column: a virtual `formula` field is computed on read and materializes no column, so searching it can never match and it is refused (#6674) — mirror the value onto a stored text field and declare that. | +| **enable** | `{ trackHistory?: boolean; searchable?: boolean; apiEnabled?: boolean; apiMethods?: Enum<'get' \| 'list' \| 'create' \| 'update' \| 'delete' \| 'bulk'>[]; … }` | optional | Enabled system features modules | +| **sharingModel** | `Enum<'private' \| 'public_read' \| 'public_read_write' \| 'controlled_by_parent'>` | optional | Org-Wide Default record visibility (OWD) for INTERNAL users. Canonical four only (legacy aliases removed, ADR-0090 D4): private (owner-only) \| public_read (everyone reads, owner writes) \| public_read_write (everyone reads+writes) \| controlled_by_parent (derived from the master record). A CUSTOM object that omits this resolves to private at runtime (ADR-0090 D1). | +| **externalSharingModel** | `Enum<'private' \| 'public_read' \| 'public_read_write' \| 'controlled_by_parent'>` | optional | [ADR-0090 D11] OWD for external (portal/partner) principals. Defaults to private; must be <= sharingModel in openness. | +| **publicSharing** | `{ enabled?: boolean; allowedAudiences?: Enum<'public' \| 'link_only' \| 'signed_in' \| 'email'>[]; allowedPermissions?: Enum<'view' \| 'comment' \| 'edit'>[]; maxExpiryDays?: integer; … }` | optional | Public share-link policy (Notion/Figma-style link sharing) | +| **actions** | `{ name: string; label: string \| Record; description?: string \| Record; objectName?: string; … }[]` | optional | Actions associated with this object (auto-populated from top-level actions via objectName) | +| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this object. | +| **_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. | + --- @@ -191,6 +325,83 @@ Add a new field to an existing object | **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: `MigrationOperation.field` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional | Machine name (snake_case) | +| **label** | `string` | optional | Human readable label | +| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>` | ✅ | Field Data Type | +| **description** | `string` | optional | Tooltip/Help text | +| **format** | `string` | optional | Format string (e.g. email, phone) | +| **required** | `boolean` | optional (default: `false`) | Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. On a multi-value lookup (`multiple: true`) required means NON-EMPTY array — an emptied required set fails validation loudly; `[]` does not satisfy it (#9447, maintainer ruling 2026-08-18). NOT a column constraint — the physical NOT NULL is a separate explicit opt-in (`storage.notNull`), so tightening this on a deployed object is safe: existing null rows stay readable, and editable as long as the write does not touch this field. | +| **storage** | `{ notNull?: boolean }` | optional | Physical storage constraints (ADR-0113). Owns the DDL the write contract deliberately does not imply. Absent = no storage-level constraint requested. | +| **searchable** | `boolean` | optional (default: `false`) | Is searchable | +| **multiple** | `boolean` | optional (default: `false`) | Allow multiple values (Stores as Array/JSON). Applicable for select, lookup, file, image. An emptied multi-value lookup reads back as `[]`, never `null` — the rule binds every writer (cascade repair, form clears, API writes), not just cascade repair (#9447, maintainer ruling 2026-08-18). | +| **unique** | `boolean \| 'global' \| 'organization'` | optional (default: `false`) | Unique constraint and its scope (ADR-0120). 'organization' = one holder per organization (NULL-safe composite with the organization key part on organization-scoped objects) — prefer this explicit spelling in new code; true = same per-organization scope (positional synonym, stays valid); 'global' = one holder across the whole installation. 'tenant'/'org' are rejected — the word is 'organization' | +| **defaultValue** | `any` | optional | Default applied on INSERT when the field is omitted or null (`''` is a real value, not absence). Three legal shapes (#7127), discriminated in the engine's own order: a CEL Expression envelope `{ dialect: 'cel', source: 'today()' }` (accepted structurally; result type is a runtime concern); a runtime TOKEN — `NOW()` on `datetime`/`date`/`time` only, `current_user` on `user` or `lookup` with `reference: 'sys_user'` only, neither on a multi-value field; or a LITERAL, which must satisfy this field's own stored value contract (ADR-0104 D1 `valueSchemaFor`). Anything else is refused at parse time with a prescriptive message. | +| **maxLength** | `integer` | optional | Max character length (positive integer). Only authorable on types that store a bounded string: text, textarea, email, url, phone, password, markdown, html, richtext, code, signature, qrcode. | +| **minLength** | `integer` | optional | Min character length (positive integer; `minLength: 0` is refused — express "no minimum" by omitting the key). Only authorable on types that store a bounded string: text, textarea, email, url, phone, password, markdown, html, richtext, code, signature, qrcode. | +| **precision** | `integer` | optional | Total digits (non-negative integer) | +| **scale** | `integer` | optional | Decimal places (non-negative integer) | +| **min** | `number` | optional | Minimum value | +| **max** | `number` | optional | Maximum value | +| **useGrouping** | `boolean` | optional | Digit-grouping presentation hint for `number` fields (#7768) — maps to `Intl.NumberFormat`'s `useGrouping`. Absent = renderer decides (interim heuristic today, locale default eventually); `false` = author opts out of grouping (e.g. a year or other ordinal/identifier integer); `true` = author pins grouping on. | +| **accept** | `string[]` | optional | Permitted upload types for media fields, as MIME types or extensions (e.g. ["image/*", ".pdf"]). Offered to the file picker AND enforced on write. | +| **maxSize** | `integer` | optional | Maximum permitted file size in BYTES for media fields. Enforced on write against the stored file size, not just checked in the browser. | +| **options** | `{ label: string; value: string; color?: string; default?: boolean; … }[]` | optional | Static options for select/multiselect | +| **reference** | `string` | optional | Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects. | +| **referenceVia** | `string` | optional | Declares this text field as the id half of a polymorphic pointer pair (ADR-0052 §5 ActivityPointer): the value is a record id of the object named by the SIBLING FIELD this key names — e.g. `record_id` with `referenceVia: 'object_name'`. The sibling must be a declared field on the same object holding an object machine name. Text fields only; mutually exclusive with `reference` (a static and a per-record target contradict). Enforced today at seed load: the value resolves as a natural key against the object the sibling column names, and an unresolvable pointer is refused loudly instead of stored verbatim. Adds no referential integrity or $expand behavior. | +| **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted | +| **inlineEdit** | `boolean \| Enum<'grid' \| 'form'>` | optional | Edit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form. | +| **inlineTitle** | `string` | optional | Title for the inline master-detail grid | +| **inlineColumns** | `{ name: string; label?: string; type?: Enum<'text' \| 'number' \| 'currency' \| 'date' \| 'datetime' \| 'time' \| 'select' \| 'lookup' \| 'file'>; width?: number; … }[]` | optional | Explicit columns for the inline grid (derived from the child object when omitted). Each entry is a strict, name-keyed column (`{ name, label?, type?, … }` — objectui GridColumn, #3951); identity-only entries (`{ name }`) hydrate everything else from the child object's fields. Unknown keys and the retired `field` spelling are refused at parse. | +| **inlineAmountField** | `string` | optional | Numeric child field summed for the inline grid total | +| **relatedList** | `boolean \| 'primary'` | optional | Show this child collection as a related list on the parent's detail page (read-side mirror of inlineEdit). false = suppress; true/absent = shown (stacked under the shared "Related" tab); 'primary' = core relationship, promoted to its own tab. Prominence intent, not a layout switch (ADR-0085). | +| **relatedListTitle** | `string` | optional | Title for the detail-page related list | +| **relatedListColumns** | `string[]` | optional | Explicit columns for the detail-page related list, as child field names (e.g. ['name', 'status']); derived from the child object (highlightFields → field walk) when omitted. Strings only — labels, cell types and formatting always derive from the child object's field definitions; column objects are refused at parse. | +| **relatedListFilter** | `any` | optional | Declarative default filter for the detail-page related list: AND-composed with the parent-relationship condition `{ [referenceField]: parentId }` — an authored constraint, never a user-editable suggestion. The related-list tab badge count honors the same composed filter, so counts match the visible rows. Canonical Query-DSL FilterCondition (the same dialect as a query `where`), e.g. `{ status: { $ne: 'deleted' } }` to hide soft-deleted children. | +| **displayField** | `string` | optional | Field shown as each candidate's label in the picker/popover (defaults to the referenced object's name/title). | +| **descriptionField** | `string` | optional | Secondary field shown under the label in the quick-select popover. | +| **lookupColumns** | `(string \| { field: string; label?: string; width?: string; type?: string })[]` | optional | Explicit columns for the record-picker table; auto-derived from the referenced object when omitted. | +| **lookupPageSize** | `integer` | optional | Rows per page in the record-picker dialog (default 10). | +| **lookupFilters** | `{ field: string; operator: Enum<'eq' \| 'ne' \| 'gt' \| 'lt' \| 'gte' \| 'lte' \| 'contains' \| 'in' \| 'notIn'>; value: any }[]` | optional | Base filters restricting which records are selectable (e.g. only active). The structured, picker-honoured lookup filter. | +| **dependsOn** | `(string \| { field: string; param?: string })[]` | optional | Declares that this field's available values depend on the value of other field(s) on the same record — the form gates the field until they are set and re-evaluates as they change. For `lookup`/`master_detail` it scopes the candidate query (string = same local/remote key; `{field,param}` when the remote filter key differs — the `{field,param}` form is lookup-only). For `select`/`multiselect`/`radio` the actual per-option rule lives in each option's `visibleWhen`; list the referenced fields here (string form) so the option list gates and refreshes with the parent. | +| **allowCreate** | `boolean` | optional | Allow inline quick-create from the record picker: when no match exists the user can create a record from the typed text (optimistic dataSource.create with the display field). Best for simple objects whose only required field is the display field. | +| **expression** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Formula expression (CEL). e.g. F`record.amount * 0.1` | +| **returnType** | `Enum<'number' \| 'text' \| 'boolean' \| 'date'>` | optional | Inferred value type of a formula field (number/text/boolean/date) | +| **summaryOperations** | `{ object: string; field: string; function: Enum<'count' \| 'sum' \| 'min' \| 'max' \| 'avg'>; relationshipField?: string; … }` | optional | Roll-up summary definition. The engine recomputes the value when child records are inserted/updated/deleted. | +| **language** | `string` | optional | Programming language for syntax highlighting (e.g., javascript, python, sql) | +| **step** | `number` | optional | Step increment for slider (default: 1) | +| **currencyConfig** | `{ precision?: integer; currencyMode?: Enum<'dynamic' \| 'fixed'>; defaultCurrency?: string }` | optional | Configuration for currency field type | +| **dimensions** | `integer` | optional | Vector dimensionality (e.g., 1536 for OpenAI embeddings) | +| **trackHistory** | `boolean` | optional | Render this field's value changes as human-readable entries on the record activity timeline (ADR-0052 §5b). Opt-in per field. | +| **group** | `string` | optional | Field group name for organizing fields in forms and layouts (e.g., "contact_info", "billing", "system") | +| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is shown only when TRUE (else hidden). e.g. P`record.type == 'invoice'` | +| **readonlyWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is read-only when TRUE. e.g. P`record.status == 'paid'` | +| **requiredWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is required when TRUE. The only slot; the `conditionalRequired` alias was removed in protocol 17 (#3855). | +| **conditionalRequired** | `never` | optional | [REMOVED] `conditionalRequired` was removed in @objectstack/spec 17 (#3855) — use `requiredWhen`. Rename the key; the value (a CEL predicate) is unchanged. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **widget** | `string` | optional | Form widget override — names a registered field component (resolved as `field:`) to render this field instead of the `type` default. Degrades to the `type` renderer when unregistered. e.g. "object-ref", "filter-condition", "recipient-picker". | +| **hidden** | `boolean` | optional (default: `false`) | Hidden from default UI | +| **internal** | `boolean` | optional | [#7728] Never return this field's value on the generic data path — the engine OMITS the key from `find`/`findOne` results, the 201 create body and the by-id update body, on the default projection AND when a client names the field in `?select=`. Storage, filtering and indexing are untouched, so a server-side verifier can still match on the column and a purpose-built mint route can still return the value once at creation. The read protection for ADR-0100's third credential channel (auth-subsystem one-way hashes on `text` columns). Omission, not masking: a mask signals 'a value is set', which carries no information on a `required` column. | +| **readonly** | `boolean` | optional (default: `false`) | Read-only — never editable in forms, AND server-enforced on BOTH write paths: a non-system write to this field is silently dropped from the payload on UPDATE (#2948/#3003) and on INSERT (#3043; a create can no longer directly seed e.g. `approval_status: "approved"`), symmetric with `readonlyWhen`. A stripped INSERT field still falls back to its `defaultValue`. Exempt from the strip on BOTH paths: `isSystem` writes (seed replay, migration). Exempt on the UPDATE path ONLY: an opt-in "historical" import (`preserveAudit`, #3493) — which admits a whitelist (the audit/timestamp family plus author-declared business `readonly` fields). On INSERT the exemption does NOT apply (#6640): a non-system create that requests `preserveAudit` still has its readonly fields stripped, and is warned loudly that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. A normal (non-system) import is NOT system-context and still strips. | +| **requiredPermissions** | `string[]` | optional | [ADR-0066 D3] Capabilities required to read/edit this field (mask on read, deny on write; AND-gate). | +| **maskingRule** | `Enum<'phone' \| 'id_card' \| 'bank_account' \| 'email' \| 'name'> \| { keepHead: integer; keepTail: integer }` | optional | [#8993] Partial masking rule enforced by the runtime FieldMasker (single channel — API, UI, export and AI context all see the same masked value). A named preset ('phone' 138****5678, 'id_card' keep 6+4, 'bank_account' keep last 4, 'email' j***@example.com, 'name' keep first char) or `{ keepHead, keepTail }`. Masked for every non-system caller unless the field's `requiredPermissions` are ALL held (that evaluation is the unmask gate); a permission set marking the field non-readable still deletes it entirely. Deterministic, length-preserving output; masked callers cannot filter/sort/group/aggregate on the field. | +| **ackPlaintextMasking** | `boolean` | optional | [ADR-0100] Affirm a generic `password` field's plaintext-at-rest / masked-on-read contract is intended, silencing the author-time warning (#3420). No effect on non-password fields. | +| **system** | `boolean` | optional | Auto-injected system/audit field (e.g. created_at, updated_by, organization_id). Tools that surface system fields separately from author-declared business fields should branch on this flag. | +| **sortable** | `boolean` | optional (default: `true`) | Whether field is sortable in list views | +| **inlineHelpText** | `string` | optional | Help text displayed below the field in forms | +| **placeholder** | `string` | optional | Placeholder text rendered inside the empty input (the HTML placeholder attribute); disappears once a value is entered. Distinct from `inlineHelpText` (always-visible help rendered beside/under the input) and `description` (tooltip/developer documentation). | +| **autonumberFormat** | `string` | optional (default: `"{0000}"`) | Auto-number format: literal text + `{0000}` counter, `{YYYY}`/`{MM}`/`{DD}`/`{YYYYMMDD}` date tokens (business tz), and `{field_name}` interpolation. Counter resets per rendered prefix (e.g. AD`{YYYYMMDD}``{0000}` resets daily). Omitted on an `autonumber` field ⇒ the contract default `{0000}` (#6555). | +| **externalId** | `boolean` | optional (default: `false`) | Is external ID for upsert operations | +| **_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. | + --- #### Option 2 @@ -239,6 +450,54 @@ Create a new object | **type** | `'create_object'` | ✅ | | | **object** | `{ name: string; label?: string; pluralLabel?: string; description?: string; … }` | ✅ | Full object definition to create | +### Nested Shape: `MigrationOperation.object` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Machine unique key (snake_case). Immutable. | +| **label** | `string` | optional | Human readable singular label (e.g. "Account") | +| **pluralLabel** | `string` | optional | Human readable plural label (e.g. "Accounts") | +| **description** | `string` | optional | Developer documentation / description | +| **icon** | `string` | optional | Icon name (Lucide/Material) for UI representation | +| **isSystem** | `boolean` | optional (default: `false`) | Is system object (protected from deletion; defaults its org-wide sharing to public when no sharingModel is set — plugin-sharing) | +| **managedBy** | `Enum<'platform' \| 'config' \| 'system-data' \| 'engine-owned' \| 'append-only' \| 'better-auth'>` | optional | Lifecycle bucket — platform (user CRUD) \| config (admin authored) \| system-data (platform-defined schema, admin/user-writable data) \| engine-owned (engine owns the lifecycle, no user writes) \| append-only (audit) \| better-auth (identity). UI clients honour the resolved affordance matrix. | +| **ownership** | `Enum<'user' \| 'business_unit' \| 'org' \| 'none'>` | optional | Record-ownership model: user (default — injects reassignable owner_id plus owning_business_unit_id) \| business_unit (unit-owned: owning_business_unit_id only, no owner_id) \| org \| none (no per-record owner, neither anchor). Distinct from the package own/extend contribution kind. | +| **userActions** | `{ create?: boolean \| object; import?: boolean \| object; edit?: boolean \| object; delete?: boolean \| object; … }` | optional | Per-object override of the resolved CRUD affordance matrix. | +| **systemFields** | `false \| { tenant?: boolean; audit?: boolean }` | optional | Opt out of, or selectively disable, registry-level system-field auto-injection. | +| **datasource** | `string` | optional (default: `"default"`) | Target Datasource ID. "default" is the primary DB. | +| **external** | `{ remoteName?: string; remoteSchema?: string; writable?: boolean; columnMap?: Record; … }` | optional | Remote table binding for federated (external) objects. | +| **fields** | `Record; description?: string; … }>` | ✅ | Field definitions map. Keys must be snake_case identifiers. | +| **indexes** | `{ name?: string; fields: string[]; unique?: boolean \| 'global' \| 'organization' }[]` | optional | Database performance indexes | +| **fieldGroups** | `{ key: string; label: string; icon?: string; description?: string; … }[]` | optional | Ordered list of field groups (array order = display order). See ObjectFieldGroupSchema. | +| **tenancy** | `{ enabled: boolean; tenantField?: string; organizationField?: string }` | optional | Multi-tenancy configuration for SaaS applications | +| **access** | `{ default?: Enum<'public' \| 'private'> }` | optional | [ADR-0066 D2] Object exposure posture (public-by-default vs private secure-by-default). | +| **requiredPermissions** | `string[] \| { read?: string[]; create?: string[]; update?: string[]; delete?: string[] }` | optional | [ADR-0066 D3/⑤] Capabilities required to access this object (AND-gate) — `string[]` gates all CRUD, or a `{read,create,update,delete}` map gates per operation. | +| **lifecycle** | `{ class: Enum<'record' \| 'audit' \| 'telemetry' \| 'transient' \| 'event'>; retention?: object; ttl?: object; storage?: object; … }` | optional | Data lifecycle contract (ADR-0057): class + retention/ttl/rotation/archive policies enforced by the platform LifecycleService. | +| **fileAccessDelegate** | `string` | optional | Kernel service that authorizes downloads of files owned by this object's media fields, instead of testing whether the caller can read the owning row. For objects whose access is mediated by a service (e.g. sys_approval_action → approvals). Fails closed. | +| **validations** | `any[]` | optional | Object-level validation rules | +| **activityMilestones** | `{ field: string; value: string; summary: string; type?: string }[]` | optional | Declarative semantic activity milestones — emit a templated timeline row when a field transitions into a value, no hook code (ADR-0052 §5b.2). | +| **nameField** | `string` | optional | [ADR-0079] Canonical primary title field — the stored field used as the record display name (e.g. "name", "title"). | +| **displayNameField** | `string` | optional | [DEPRECATED → nameField] Field to use as the record display name (e.g., "name", "title"). Accepted as an alias for nameField. | +| **titleFormat** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | [DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField. | +| **highlightFields** | `string[]` | optional | [ADR-0085] Ordered most-important fields; first entry wins where only one fits. Drives default columns, cards, previews, detail highlight strip. Renamed from compactLayout. | +| **stageField** | `string \| false` | optional | [ADR-0085] Lifecycle stage field (linear/ordered), or false to declare the status field non-linear and suppress stage heuristics. Absent = heuristic detection allowed. | +| **editMode** | `Enum<'modal' \| 'page'>` | optional | Edit-interaction intent for records of this object: 'modal' opens the edit form as a dialog over the current view; 'page' navigates to a dedicated full-page edit route. Absent = the renderer picks its own default (objectui defaults to modal). Cross-renderer intent, not pixel styling (#11408, #10144 family). | +| **listViews** | `Record; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: object \| … +3 more; … }>` | optional | Built-in named list views (segmented tabs) shipped with the object schema — "views" mode, dropdown userFilters allowed, no page-only tabs (ADR-0047) | +| **searchableFields** | `string[]` | optional | Fields the `$search` query matches against (ADR-0061). Canonical default for the record picker, list quick-search and global search; views may narrow it. When unset, search auto-defaults to the name/title field plus short-text fields. Entries must name a STORED column: a virtual `formula` field is computed on read and materializes no column, so searching it can never match and it is refused (#6674) — mirror the value onto a stored text field and declare that. | +| **enable** | `{ trackHistory?: boolean; searchable?: boolean; apiEnabled?: boolean; apiMethods?: Enum<'get' \| 'list' \| 'create' \| 'update' \| 'delete' \| 'bulk'>[]; … }` | optional | Enabled system features modules | +| **sharingModel** | `Enum<'private' \| 'public_read' \| 'public_read_write' \| 'controlled_by_parent'>` | optional | Org-Wide Default record visibility (OWD) for INTERNAL users. Canonical four only (legacy aliases removed, ADR-0090 D4): private (owner-only) \| public_read (everyone reads, owner writes) \| public_read_write (everyone reads+writes) \| controlled_by_parent (derived from the master record). A CUSTOM object that omits this resolves to private at runtime (ADR-0090 D1). | +| **externalSharingModel** | `Enum<'private' \| 'public_read' \| 'public_read_write' \| 'controlled_by_parent'>` | optional | [ADR-0090 D11] OWD for external (portal/partner) principals. Defaults to private; must be <= sharingModel in openness. | +| **publicSharing** | `{ enabled?: boolean; allowedAudiences?: Enum<'public' \| 'link_only' \| 'signed_in' \| 'email'>[]; allowedPermissions?: Enum<'view' \| 'comment' \| 'edit'>[]; maxExpiryDays?: integer; … }` | optional | Public share-link policy (Notion/Figma-style link sharing) | +| **actions** | `{ name: string; label: string \| Record; description?: string \| Record; objectName?: string; … }[]` | optional | Actions associated with this object (auto-populated from top-level actions via objectName) | +| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this object. | +| **_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. | + --- #### Option 5 diff --git a/content/docs/references/system/object-storage.mdx b/content/docs/references/system/object-storage.mdx index f868dcf4bd..f7ae80cbae 100644 --- a/content/docs/references/system/object-storage.mdx +++ b/content/docs/references/system/object-storage.mdx @@ -50,6 +50,14 @@ const result = AccessControlConfigSchema.parse(data); | **allowedIps** | `string[]` | optional | Allowed IP addresses/CIDR blocks | | **blockedIps** | `string[]` | optional | Blocked IP addresses/CIDR blocks | +### Nested Shape: `AccessControlConfig.publicAccess` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **allowPublicRead** | `boolean` | optional (default: `false`) | Allow public read access | +| **allowPublicWrite** | `boolean` | optional (default: `false`) | Allow public write access | +| **allowPublicList** | `boolean` | optional (default: `false`) | Allow public bucket listing | + --- @@ -75,6 +83,47 @@ const result = AccessControlConfigSchema.parse(data); | **description** | `string` | optional | Bucket description | | **enabled** | `boolean` | optional (default: `true`) | Enable this bucket | +### Nested Shape: `BucketConfig.encryption` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `false`) | Enable server-side encryption | +| **algorithm** | `Enum<'AES256' \| 'aws:kms' \| 'azure:kms' \| 'gcp:kms'>` | optional (default: `"AES256"`) | Encryption algorithm | +| **kmsKeyId** | `string` | optional | KMS key ID for managed encryption | + +### Nested Shape: `BucketConfig.accessControl` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **acl** | `Enum<'private' \| 'public_read' \| 'public_read_write' \| 'authenticated_read' \| …>` | optional (default: `"private"`) | Default access control level | +| **allowedOrigins** | `string[]` | optional | CORS allowed origins | +| **allowedMethods** | `Enum<'GET' \| 'PUT' \| 'POST' \| 'DELETE' \| 'HEAD'>[]` | optional | CORS allowed HTTP methods | +| **allowedHeaders** | `string[]` | optional | CORS allowed headers | +| **exposeHeaders** | `string[]` | optional | CORS exposed headers | +| **maxAge** | `number` | optional | CORS preflight cache duration in seconds | +| **corsEnabled** | `boolean` | optional (default: `false`) | Enable CORS configuration | +| **publicAccess** | `{ allowPublicRead: boolean; allowPublicWrite: boolean; allowPublicList: boolean }` | optional | Public access control | +| **allowedIps** | `string[]` | optional | Allowed IP addresses/CIDR blocks | +| **blockedIps** | `string[]` | optional | Blocked IP addresses/CIDR blocks | + +### Nested Shape: `BucketConfig.lifecyclePolicy` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `false`) | Enable lifecycle policies | +| **rules** | `{ id: string; enabled: boolean; action: Enum<'transition' \| 'delete' \| 'abort'>; prefix?: string; … }[]` | optional (default: `[]`) | Lifecycle rules | + +### Nested Shape: `BucketConfig.multipartConfig` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | Enable multipart uploads | +| **partSize** | `number` | optional (default: `10485760`) | Part size in bytes (min 5MB, max 5GB) | +| **maxParts** | `number` | optional (default: `10000`) | Maximum number of parts (max 10,000) | +| **threshold** | `number` | optional (default: `104857600`) | File size threshold to trigger multipart upload (bytes) | +| **maxConcurrent** | `number` | optional (default: `4`) | Maximum concurrent part uploads | +| **abortIncompleteAfterDays** | `number` | optional | Auto-abort incomplete uploads after N days | + --- @@ -118,6 +167,19 @@ Lifecycle policy action type | **enabled** | `boolean` | optional (default: `false`) | Enable lifecycle policies | | **rules** | `{ id: string; enabled: boolean; action: Enum<'transition' \| 'delete' \| 'abort'>; prefix?: string; … }[]` | optional (default: `[]`) | Lifecycle rules | +### Nested Shape: `LifecyclePolicyConfig.rules[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Rule identifier | +| **enabled** | `boolean` | optional (default: `true`) | Enable this rule | +| **action** | `Enum<'transition' \| 'delete' \| 'abort'>` | ✅ | Action to perform | +| **prefix** | `string` | optional | Object key prefix filter (e.g., "uploads/") | +| **tags** | `Record` | optional | Object tag filters | +| **daysAfterCreation** | `number` | optional | Days after object creation | +| **daysAfterModification** | `number` | optional | Days after last modification | +| **targetStorageClass** | `Enum<'standard' \| 'intelligent' \| 'infrequent_access' \| 'glacier' \| 'deep_archive'>` | optional | Target storage class for transition action | + --- @@ -174,6 +236,13 @@ Lifecycle policy action type | **encryption** | `{ algorithm: string; keyId?: string }` | optional | Server-side encryption configuration | | **custom** | `Record` | optional | Custom user-defined metadata | +### Nested Shape: `ObjectMetadata.encryption` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **algorithm** | `string` | ✅ | Encryption algorithm (e.g., AES256, aws:kms) | +| **keyId** | `string` | optional | KMS key ID if using managed encryption | + --- @@ -196,6 +265,43 @@ Lifecycle policy action type | **enabled** | `boolean` | optional (default: `true`) | Enable this storage configuration | | **description** | `string` | optional | Configuration description | +### Nested Shape: `ObjectStorageConfig.connection` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **accessKeyId** | `string` | optional | AWS access key ID or MinIO access key | +| **secretAccessKey** | `string` | optional | AWS secret access key or MinIO secret key | +| **sessionToken** | `string` | optional | AWS session token for temporary credentials | +| **accountName** | `string` | optional | Azure storage account name | +| **accountKey** | `string` | optional | Azure storage account key | +| **sasToken** | `string` | optional | Azure SAS token | +| **environmentId** | `string` | optional | GCP project ID | +| **credentials** | `string` | optional | GCP service account credentials JSON | +| **endpoint** | `string` | optional | Custom endpoint URL | +| **region** | `string` | optional | Default region | +| **useSSL** | `boolean` | optional (default: `true`) | Use SSL/TLS for connections | +| **timeout** | `number` | optional | Connection timeout in milliseconds | + +### Nested Shape: `ObjectStorageConfig.buckets[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Bucket identifier in ObjectStack (snake_case) | +| **label** | `string` | ✅ | Display label | +| **bucketName** | `string` | ✅ | Actual bucket/container name in storage provider | +| **region** | `string` | optional | Storage region (e.g., us-east-1, westus) | +| **provider** | `Enum<'s3' \| 'azure_blob' \| 'gcs' \| 'minio' \| 'r2' \| 'spaces' \| 'wasabi' \| 'backblaze' \| 'local'>` | ✅ | Storage provider | +| **endpoint** | `string` | optional | Custom endpoint URL (for S3-compatible providers) | +| **pathStyle** | `boolean` | optional (default: `false`) | Use path-style URLs (for S3-compatible providers) | +| **versioning** | `boolean` | optional (default: `false`) | Enable object versioning | +| **encryption** | `{ enabled: boolean; algorithm: Enum<'AES256' \| 'aws:kms' \| 'azure:kms' \| 'gcp:kms'>; kmsKeyId?: string }` | optional | Server-side encryption configuration | +| **accessControl** | `{ acl: Enum<'private' \| 'public_read' \| 'public_read_write' \| 'authenticated_read' \| …>; allowedOrigins?: string[]; allowedMethods?: Enum<'GET' \| 'PUT' \| 'POST' \| 'DELETE' \| 'HEAD'>[]; allowedHeaders?: string[]; … }` | optional | Access control configuration | +| **lifecyclePolicy** | `{ enabled: boolean; rules: object[] }` | optional | Lifecycle policy configuration | +| **multipartConfig** | `{ enabled: boolean; partSize: number; maxParts: number; threshold: number; … }` | optional | Multipart upload configuration | +| **tags** | `Record` | optional | Bucket tags for organization | +| **description** | `string` | optional | Bucket description | +| **enabled** | `boolean` | optional (default: `true`) | Enable this bucket | + --- diff --git a/content/docs/references/system/registry-config.mdx b/content/docs/references/system/registry-config.mdx index 54f169c400..ca3ab28985 100644 --- a/content/docs/references/system/registry-config.mdx +++ b/content/docs/references/system/registry-config.mdx @@ -42,6 +42,26 @@ const result = RegistryConfigSchema.parse(data); | **cache** | `{ enabled: boolean; ttl: integer; maxSize?: integer }` | optional | | | **mirrors** | `{ url: string; priority: integer }[]` | optional | Mirror registries for redundancy | +### Nested Shape: `RegistryConfig.upstream[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **url** | `string` | ✅ | Upstream registry endpoint | +| **syncPolicy** | `Enum<'manual' \| 'auto' \| 'proxy'>` | optional (default: `"auto"`) | Registry synchronization strategy | +| **syncInterval** | `integer` | optional | Auto-sync interval in seconds | +| **auth** | `{ type: Enum<'none' \| 'basic' \| 'bearer' \| 'api-key' \| 'oauth2'>; username?: string; password?: string; token?: string; … }` | optional | | +| **tls** | `{ enabled: boolean; verifyCertificate: boolean; certificate?: string; privateKey?: string }` | optional | | +| **timeout** | `integer` | optional (default: `30000`) | Request timeout in milliseconds | +| **retry** | `{ maxAttempts: integer; backoff: Enum<'fixed' \| 'linear' \| 'exponential'> }` | optional | | + +### Nested Shape: `RegistryConfig.cache` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | | +| **ttl** | `integer` | optional (default: `3600`) | Cache TTL in seconds | +| **maxSize** | `integer` | optional | Maximum cache size in bytes | + --- diff --git a/content/docs/references/system/search-engine.mdx b/content/docs/references/system/search-engine.mdx index 78802ffe12..66b98d26d1 100644 --- a/content/docs/references/system/search-engine.mdx +++ b/content/docs/references/system/search-engine.mdx @@ -71,6 +71,39 @@ Top-level full-text search engine configuration | **synonyms** | `Record` | optional | Synonym mappings for search expansion | | **ranking** | `Enum<'typo' \| 'geo' \| 'words' \| 'filters' \| 'proximity' \| 'attribute' \| 'exact' \| 'custom'>[]` | optional | Custom ranking rule order | +### Nested Shape: `SearchConfig.indexes[number]` + +Search index definition mapping an ObjectQL object to a search engine index + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **indexName** | `string` | ✅ | Name of the search index | +| **objectName** | `string` | ✅ | Source ObjectQL object | +| **fields** | `{ name: string; type: Enum<'text' \| 'keyword' \| 'number' \| 'date' \| 'boolean' \| 'geo'>; analyzer?: string; searchable: boolean; … }[]` | ✅ | Fields to include in the search index | +| **replicas** | `number` | optional (default: `1`) | Number of index replicas for availability | +| **shards** | `number` | optional (default: `1`) | Number of index shards for distribution | + +### Nested Shape: `SearchConfig.analyzers[string]` + +Text analyzer configuration for index tokenization and normalization + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'standard' \| 'simple' \| 'whitespace' \| 'keyword' \| 'pattern' \| 'language'>` | ✅ | Text analyzer type | +| **language** | `string` | optional | Language for language-specific analysis | +| **stopwords** | `string[]` | optional | Custom stopwords to filter during analysis | +| **customFilters** | `string[]` | optional | Additional token filter names to apply | + +### Nested Shape: `SearchConfig.facets[number]` + +Faceted search configuration for a single field + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name to generate facets from | +| **maxValues** | `number` | optional (default: `10`) | Maximum number of facet values to return | +| **sort** | `Enum<'count' \| 'alpha'>` | optional (default: `"count"`) | Facet value sort order | + --- @@ -88,6 +121,18 @@ Search index definition mapping an ObjectQL object to a search engine index | **replicas** | `number` | optional (default: `1`) | Number of index replicas for availability | | **shards** | `number` | optional (default: `1`) | Number of index shards for distribution | +### Nested Shape: `SearchIndexConfig.fields[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Field name to index | +| **type** | `Enum<'text' \| 'keyword' \| 'number' \| 'date' \| 'boolean' \| 'geo'>` | ✅ | Index field data type | +| **analyzer** | `string` | optional | Named analyzer to use for this field | +| **searchable** | `boolean` | optional (default: `true`) | Include field in full-text search | +| **filterable** | `boolean` | optional (default: `false`) | Allow filtering on this field | +| **sortable** | `boolean` | optional (default: `false`) | Allow sorting by this field | +| **boost** | `number` | optional (default: `1`) | Relevance boost factor for this field | + --- diff --git a/content/docs/references/system/security-context.mdx b/content/docs/references/system/security-context.mdx index 34e4668601..c03dbd336f 100644 --- a/content/docs/references/system/security-context.mdx +++ b/content/docs/references/system/security-context.mdx @@ -160,6 +160,63 @@ Unified security context governance configuration | **enforceOnRead** | `boolean` | optional (default: `true`) | Enforce masking and audit requirements on data read operations | | **failOpen** | `boolean` | optional (default: `false`) | When false (default), deny access if security context cannot be evaluated | +### Nested Shape: `SecurityContextConfig.complianceAuditRequirements[number]` + +Compliance framework audit event requirements + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **framework** | `Enum<'gdpr' \| 'hipaa' \| 'sox' \| 'pci_dss' \| 'ccpa' \| 'iso27001'>` | ✅ | Compliance framework identifier | +| **requiredEvents** | `string[]` | ✅ | Audit event types required by this framework (e.g., "data.delete", "auth.login") | +| **retentionDays** | `number` | ✅ | Minimum audit log retention period required by this framework (in days) | +| **alertOnMissing** | `boolean` | optional (default: `true`) | Raise alert if a required audit event is not being captured | + +### Nested Shape: `SecurityContextConfig.complianceEncryptionRequirements[number]` + +Compliance framework encryption requirements + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **framework** | `Enum<'gdpr' \| 'hipaa' \| 'sox' \| 'pci_dss' \| 'ccpa' \| 'iso27001'>` | ✅ | Compliance framework identifier | +| **dataClassifications** | `Enum<'pii' \| 'phi' \| 'pci' \| 'financial' \| 'confidential' \| 'internal' \| 'public'>[]` | ✅ | Data classifications that must be encrypted under this framework | +| **minimumAlgorithm** | `Enum<'aes-256-gcm' \| 'aes-256-cbc' \| 'chacha20-poly1305'>` | optional (default: `"aes-256-gcm"`) | Minimum encryption algorithm strength required | +| **keyRotationMaxDays** | `number` | optional (default: `90`) | Maximum key rotation interval required (in days) | + +### Nested Shape: `SecurityContextConfig.maskingVisibility[number]` + +Masking visibility and audit rule per data classification + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **dataClassification** | `Enum<'pii' \| 'phi' \| 'pci' \| 'financial' \| 'confidential' \| 'internal' \| 'public'>` | ✅ | Data classification this rule applies to | +| **defaultMasked** | `boolean` | optional (default: `true`) | Whether data is masked by default | +| **unmaskRoles** | `string[]` | optional | Roles allowed to view unmasked data | +| **auditUnmask** | `boolean` | optional (default: `true`) | Log an audit event when data is unmasked | +| **requireApproval** | `boolean` | optional (default: `false`) | Require explicit approval before unmasking | +| **approvalRoles** | `string[]` | optional | Roles that can approve unmasking requests | + +### Nested Shape: `SecurityContextConfig.dataClassifications[number]` + +Security policy for a specific data classification level + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **classification** | `Enum<'pii' \| 'phi' \| 'pci' \| 'financial' \| 'confidential' \| 'internal' \| 'public'>` | ✅ | Data classification level | +| **requireEncryption** | `boolean` | optional (default: `false`) | Encryption required for this classification | +| **requireMasking** | `boolean` | optional (default: `false`) | Masking required for this classification | +| **requireAudit** | `boolean` | optional (default: `false`) | Audit trail required for access to this classification | +| **retentionDays** | `number` | optional | Data retention limit in days (for compliance) | + +### Nested Shape: `SecurityContextConfig.eventCorrelation` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | Enable cross-subsystem security event correlation | +| **correlationId** | `boolean` | optional (default: `true`) | Inject a shared correlation ID into audit, encryption, and masking events | +| **linkAuthToAudit** | `boolean` | optional (default: `true`) | Link authentication events to subsequent data operation audit trails | +| **linkEncryptionToAudit** | `boolean` | optional (default: `true`) | Log encryption/decryption operations in the audit trail | +| **linkMaskingToAudit** | `boolean` | optional (default: `true`) | Log masking/unmasking operations in the audit trail | + --- diff --git a/content/docs/references/system/settings-manifest.mdx b/content/docs/references/system/settings-manifest.mdx index 7bc072a42b..5c67be8332 100644 --- a/content/docs/references/system/settings-manifest.mdx +++ b/content/docs/references/system/settings-manifest.mdx @@ -91,6 +91,41 @@ const result = ResolvedSettingValueSchema.parse(data); | **featureFlag** | `string` | optional | Gate manifest visibility on a feature flag | | **beta** | `boolean` | optional | Show a Beta chip on the page | +### Nested Shape: `SettingsManifest.specifiers[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'group' \| 'child_pane' \| 'info_banner' \| 'title_value' \| 'text' \| 'textarea' \| …>` | ✅ | Specifier variant | +| **id** | `string` | optional | Stable identifier (snake_case) | +| **key** | `string` | optional | Storage key (snake_case) | +| **label** | `string \| Record` | ✅ | Display label | +| **description** | `string` | optional | Help text | +| **icon** | `string` | optional | Icon name (Lucide) | +| **default** | `any` | optional | Default value | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility expression evaluated against the namespace value map, e.g. `${data.provider === 'smtp'}`. Hidden specifiers are not rendered and their values are not validated. Grammar is NOT CEL: root `data` with one-level member access, `\|\|` `&&` `!`, `===` `!==` `==` `!=` `>=` `<=` `>` `<`, parentheses and string/number/bool/null literals, optionally wrapped in `${...}`; bare string or `{ dialect, source }` envelope. | +| **required** | `boolean` | optional (default: `false`) | Required field | +| **encrypted** | `boolean` | optional | Encrypt value at rest (forced true for password) | +| **scope** | `Enum<'global' \| 'tenant' \| 'user'>` | optional | Override manifest scope for this key | +| **availableScopes** | `Enum<'global' \| 'tenant' \| 'user'>[]` | optional | Scopes allowed to override this specifier | +| **lockable** | `boolean` | optional | Allow upper-scope locking of this specifier | +| **readPermission** | `string` | optional | Permission required to read this specifier | +| **writePermission** | `string` | optional | Permission required to write this specifier | +| **deprecated** | `boolean` | optional | Mark deprecated | +| **replacedBy** | `string` | optional | Replacement key (used when deprecated=true) | +| **options** | `{ value: string \| number \| boolean; label: string \| Record; description?: string; icon?: string }[]` | optional | Options for select/radio/multiselect | +| **valueDomain** | `Enum<'iana_time_zone' \| 'iso_4217_currency' \| 'iso_3166_alpha2'>` | optional | Standard value domain enforced on write (options degrade to a UI suggestion list) | +| **min** | `number` | optional | | +| **max** | `number` | optional | | +| **step** | `number` | optional | | +| **minLength** | `integer` | optional | | +| **maxLength** | `integer` | optional | | +| **pattern** | `string` | optional | Regex pattern (text only) | +| **rows** | `integer` | optional | | +| **handler** | `{ kind: 'http'; method?: Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH'>; url: string; body?: Record; … } \| { kind: 'action'; name: string; params?: Record; confirmText?: string \| Record } \| { kind: 'navigate'; url: string; target?: Enum<'_self' \| '_blank'> }` | optional | Action handler (action_button) | +| **childNamespace** | `string` | optional | Sub-namespace (child_pane) | +| **bannerText** | `string` | optional | Markdown body (info_banner) | +| **bannerSeverity** | `Enum<'info' \| 'success' \| 'warning' \| 'error'>` | optional | | + --- @@ -103,6 +138,36 @@ const result = ResolvedSettingValueSchema.parse(data); | **manifest** | `{ namespace: string; version?: integer; label: string \| Record; icon?: string; … }` | ✅ | | | **values** | `Record; locked: boolean; lockedReason?: string; … }>` | ✅ | Effective values keyed by specifier.key | +### Nested Shape: `SettingsNamespacePayload.manifest` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **namespace** | `string` | ✅ | Namespace (snake_case, globally unique) | +| **version** | `integer` | optional (default: `1`) | Manifest schema version | +| **label** | `string \| Record` | ✅ | Display label | +| **icon** | `string` | optional | Icon (Lucide) | +| **description** | `string` | optional | Short description | +| **helpText** | `string` | optional | Markdown help text shown above specifiers | +| **scope** | `Enum<'global' \| 'tenant' \| 'user'>` | optional (default: `"tenant"`) | Default scope for specifiers | +| **readPermission** | `string` | optional (default: `"setup.access"`) | Permission required to read | +| **writePermission** | `string` | optional (default: `"setup.write"`) | Permission required to write | +| **category** | `string` | optional | Settings hub category | +| **order** | `number` | optional | Display order | +| **specifiers** | `{ type: Enum<'group' \| 'child_pane' \| 'info_banner' \| 'title_value' \| 'text' \| 'textarea' \| …>; id?: string; key?: string; label: string \| Record; … }[]` | ✅ | Page contents (ordered) | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Whole-manifest visibility. Grammar is NOT CEL: root `data` with one-level member access, `\|\|` `&&` `!`, `===` `!==` `==` `!=` `>=` `<=` `>` `<`, parentheses and string/number/bool/null literals, optionally wrapped in `${...}`; bare string or `{ dialect, source }` envelope. | +| **featureFlag** | `string` | optional | Gate manifest visibility on a feature flag | +| **beta** | `boolean` | optional | Show a Beta chip on the page | + +### Nested Shape: `SettingsNamespacePayload.values[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **value** | `any` | ✅ | Effective value (post-resolution) | +| **source** | `Enum<'env' \| 'global' \| 'tenant' \| 'user' \| 'default'>` | ✅ | Resolution source | +| **locked** | `boolean` | ✅ | Cannot be overridden from UI | +| **lockedReason** | `string` | optional | Reason for the lock (UI tooltip) | +| **cascadeChain** | `{ scope: Enum<'env' \| 'global' \| 'tenant' \| 'user' \| 'default'>; value: any; locked?: boolean; lockedReason?: string; … }[]` | optional | Full cascade trace (env → global → tenant → user → default) | + --- @@ -165,6 +230,15 @@ const result = ResolvedSettingValueSchema.parse(data); * `json` * `action_button` +### Nested Shape: `Specifier.options[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **value** | `string \| number \| boolean` | ✅ | Stored value | +| **label** | `string \| Record` | ✅ | Display label | +| **description** | `string` | optional | Optional helper text | +| **icon** | `string` | optional | Optional Lucide icon name | + --- diff --git a/content/docs/references/system/stack-server.mdx b/content/docs/references/system/stack-server.mdx index 8a689f7e9d..e5db40f2dc 100644 --- a/content/docs/references/system/stack-server.mdx +++ b/content/docs/references/system/stack-server.mdx @@ -102,6 +102,12 @@ const result = ServerRateLimitConfigSchema.parse(data); | **security** | `{ rateLimit?: object }` | optional | Server-level security configuration. Today: the global inbound rate limit. | | **trustProxy** | `boolean` | optional (default: `false`) | Believe `X-Forwarded-For` / `X-Real-IP` when identifying a caller. Declare `true` ONLY when a reverse proxy you control overwrites those headers on every inbound request. Left `false` (the default) the caller IP is the transport's own peer address, which a client cannot forge. Consumed by the inbound rate limiter when `server.security.rateLimit.enabled` is set. | +### Nested Shape: `StackServerConfig.security` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **rateLimit** | `{ enabled: boolean; windowMs: integer; maxRequests: integer }` | optional | Global inbound rate limit. When `enabled`, every inbound request consumes from a token bucket derived from this budget (capacity = `maxRequests`, refill = `maxRequests / (windowMs / 1000)` tokens per second); an empty bucket answers 429 with a `Retry-After` header. The bucket is keyed by the RESOLVED PRINCIPAL, falling back to the caller IP for anonymous traffic — so one abusive session cannot exhaust another user's budget, and credential-stuffing traffic (which has no principal yet) is still metered per source. See `server.trustProxy` for how that IP is determined. | + --- @@ -113,6 +119,14 @@ const result = ServerRateLimitConfigSchema.parse(data); | :--- | :--- | :--- | :--- | | **rateLimit** | `{ enabled: boolean; windowMs: integer; maxRequests: integer }` | optional | Global inbound rate limit. When `enabled`, every inbound request consumes from a token bucket derived from this budget (capacity = `maxRequests`, refill = `maxRequests / (windowMs / 1000)` tokens per second); an empty bucket answers 429 with a `Retry-After` header. The bucket is keyed by the RESOLVED PRINCIPAL, falling back to the caller IP for anonymous traffic — so one abusive session cannot exhaust another user's budget, and credential-stuffing traffic (which has no principal yet) is still metered per source. See `server.trustProxy` for how that IP is determined. | +### Nested Shape: `StackServerSecurity.rateLimit` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `false`) | Enable rate limiting | +| **windowMs** | `integer` | optional (default: `60000`) | Time window in milliseconds | +| **maxRequests** | `integer` | optional (default: `100`) | Max requests per window | + --- diff --git a/content/docs/references/system/supplier-security.mdx b/content/docs/references/system/supplier-security.mdx index a427190ddd..67a9752c4e 100644 --- a/content/docs/references/system/supplier-security.mdx +++ b/content/docs/references/system/supplier-security.mdx @@ -78,6 +78,28 @@ Supplier security assessment record per ISO 27001:2022 A.5.19–A.5.21 | **remediationItems** | `{ requirementId: string; action: string; deadline: number; status: Enum<'pending' \| 'in_progress' \| 'completed'> }[]` | optional | Remediation items for non-compliant requirements | | **metadata** | `Record` | optional | Custom metadata key-value pairs | +### Nested Shape: `SupplierSecurityAssessment.requirements[number]` + +Individual supplier security requirement + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Requirement identifier | +| **description** | `string` | ✅ | Requirement description | +| **controlReference** | `string` | optional | ISO 27001 control reference | +| **mandatory** | `boolean` | optional (default: `true`) | Whether this requirement is mandatory | +| **compliant** | `boolean` | optional | Whether the supplier meets this requirement | +| **evidence** | `string` | optional | Compliance evidence or assessment notes | + +### Nested Shape: `SupplierSecurityAssessment.remediationItems[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **requirementId** | `string` | ✅ | Non-compliant requirement ID | +| **action** | `string` | ✅ | Required remediation action | +| **deadline** | `number` | ✅ | Remediation deadline timestamp | +| **status** | `Enum<'pending' \| 'in_progress' \| 'completed'>` | optional (default: `"pending"`) | Remediation status | + --- diff --git a/content/docs/references/system/tenant.mdx b/content/docs/references/system/tenant.mdx index a37613b885..66dacfb9f1 100644 --- a/content/docs/references/system/tenant.mdx +++ b/content/docs/references/system/tenant.mdx @@ -44,6 +44,40 @@ const result = DatabaseLevelIsolationStrategySchema.parse(data); | **backup** | `{ strategy: Enum<'individual' \| 'consolidated' \| 'on_demand'>; frequencyHours: integer; retentionDays: integer }` | optional | Backup configuration | | **encryption** | `{ perTenantKeys: boolean; algorithm: string; keyManagement?: Enum<'aws_kms' \| 'azure_key_vault' \| 'gcp_kms' \| 'hashicorp_vault' \| 'custom'> }` | optional | Encryption configuration | +### Nested Shape: `DatabaseLevelIsolationStrategy.database` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **namingPattern** | `string` | optional (default: `"tenant_{tenant_id}"`) | Database naming pattern | +| **serverStrategy** | `Enum<'shared' \| 'sharded' \| 'dedicated'>` | optional (default: `"shared"`) | Server assignment strategy | +| **separateCredentials** | `boolean` | optional (default: `true`) | Separate credentials per tenant | +| **autoCreateDatabase** | `boolean` | optional (default: `true`) | Auto-create database | + +### Nested Shape: `DatabaseLevelIsolationStrategy.connectionPool` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **poolSize** | `integer` | optional (default: `10`) | Connection pool size | +| **maxActivePools** | `integer` | optional (default: `100`) | Max active pools | +| **idleTimeout** | `integer` | optional (default: `300`) | Idle pool timeout | +| **usePooler** | `boolean` | optional (default: `true`) | Use connection pooler | + +### Nested Shape: `DatabaseLevelIsolationStrategy.backup` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **strategy** | `Enum<'individual' \| 'consolidated' \| 'on_demand'>` | optional (default: `"individual"`) | Backup strategy | +| **frequencyHours** | `integer` | optional (default: `24`) | Backup frequency | +| **retentionDays** | `integer` | optional (default: `30`) | Backup retention days | + +### Nested Shape: `DatabaseLevelIsolationStrategy.encryption` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **perTenantKeys** | `boolean` | optional (default: `false`) | Per-tenant encryption keys | +| **algorithm** | `string` | optional (default: `"AES-256-GCM"`) | Encryption algorithm | +| **keyManagement** | `Enum<'aws_kms' \| 'azure_key_vault' \| 'gcp_kms' \| 'hashicorp_vault' \| 'custom'>` | optional | Key management service | + --- @@ -87,6 +121,23 @@ Quota enforcement check result | **database** | `{ enableRLS: boolean; contextMethod: Enum<'session_variable' \| 'search_path' \| 'application_name'>; contextVariable: string; applicationValidation: boolean }` | optional | Database configuration | | **performance** | `{ usePartialIndexes: boolean; usePartitioning: boolean; poolSizePerTenant?: integer }` | optional | Performance settings | +### Nested Shape: `RowLevelIsolationStrategy.database` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enableRLS** | `boolean` | optional (default: `true`) | Enable PostgreSQL Row-Level Security | +| **contextMethod** | `Enum<'session_variable' \| 'search_path' \| 'application_name'>` | optional (default: `"session_variable"`) | How to set tenant context | +| **contextVariable** | `string` | optional (default: `"app.current_tenant"`) | Session variable name | +| **applicationValidation** | `boolean` | optional (default: `true`) | Application-level tenant validation | + +### Nested Shape: `RowLevelIsolationStrategy.performance` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **usePartialIndexes** | `boolean` | optional (default: `true`) | Use partial indexes per tenant | +| **usePartitioning** | `boolean` | optional (default: `false`) | Use table partitioning by tenant_id | +| **poolSizePerTenant** | `integer` | optional | Connection pool size per tenant | + --- @@ -101,6 +152,30 @@ Quota enforcement check result | **migrations** | `{ strategy: Enum<'parallel' \| 'sequential' \| 'on_demand'>; maxConcurrent: integer; rollbackOnError: boolean }` | optional | Migration configuration | | **performance** | `{ poolPerSchema: boolean; schemaCacheTTL: integer }` | optional | Performance settings | +### Nested Shape: `SchemaLevelIsolationStrategy.schema` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **namingPattern** | `string` | optional (default: `"tenant_{tenant_id}"`) | Schema naming pattern | +| **includePublicSchema** | `boolean` | optional (default: `true`) | Include public schema | +| **sharedSchema** | `string` | optional (default: `"public"`) | Schema for shared resources | +| **autoCreateSchema** | `boolean` | optional (default: `true`) | Auto-create schema | + +### Nested Shape: `SchemaLevelIsolationStrategy.migrations` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **strategy** | `Enum<'parallel' \| 'sequential' \| 'on_demand'>` | optional (default: `"parallel"`) | Migration strategy | +| **maxConcurrent** | `integer` | optional (default: `10`) | Max concurrent migrations | +| **rollbackOnError** | `boolean` | optional (default: `true`) | Rollback on error | + +### Nested Shape: `SchemaLevelIsolationStrategy.performance` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **poolPerSchema** | `boolean` | optional (default: `false`) | Separate pool per schema | +| **schemaCacheTTL** | `integer` | optional (default: `3600`) | Schema cache TTL | + --- @@ -120,6 +195,26 @@ Quota enforcement check result | **customizations** | `Record` | optional | Custom configuration values | | **quotas** | `{ maxUsers?: integer; maxStorage?: integer; apiRateLimit?: integer; maxObjects?: integer; … }` | optional | | +### Nested Shape: `Tenant.connectionConfig` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **url** | `string` | ✅ | Database connection URL | +| **authToken** | `string` | optional | Database auth token (encrypted at rest) | +| **group** | `string` | optional | Turso database group name | + +### Nested Shape: `Tenant.quotas` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **maxUsers** | `integer` | optional | Maximum number of users | +| **maxStorage** | `integer` | optional | Maximum storage in bytes | +| **apiRateLimit** | `integer` | optional | API requests per minute | +| **maxObjects** | `integer` | optional | Maximum number of custom objects | +| **maxRecordsPerObject** | `integer` | optional | Maximum records per object | +| **maxDeploymentsPerDay** | `integer` | optional | Maximum deployments per day | +| **maxStorageBytes** | `integer` | optional | Maximum storage in bytes | + --- @@ -154,6 +249,23 @@ This schema accepts one of the following structures: | **database** | `{ enableRLS: boolean; contextMethod: Enum<'session_variable' \| 'search_path' \| 'application_name'>; contextVariable: string; applicationValidation: boolean }` | optional | Database configuration | | **performance** | `{ usePartialIndexes: boolean; usePartitioning: boolean; poolSizePerTenant?: integer }` | optional | Performance settings | +### Nested Shape: `TenantIsolationConfig.database` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enableRLS** | `boolean` | optional (default: `true`) | Enable PostgreSQL Row-Level Security | +| **contextMethod** | `Enum<'session_variable' \| 'search_path' \| 'application_name'>` | optional (default: `"session_variable"`) | How to set tenant context | +| **contextVariable** | `string` | optional (default: `"app.current_tenant"`) | Session variable name | +| **applicationValidation** | `boolean` | optional (default: `true`) | Application-level tenant validation | + +### Nested Shape: `TenantIsolationConfig.performance` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **usePartialIndexes** | `boolean` | optional (default: `true`) | Use partial indexes per tenant | +| **usePartitioning** | `boolean` | optional (default: `false`) | Use table partitioning by tenant_id | +| **poolSizePerTenant** | `integer` | optional | Connection pool size per tenant | + --- #### Option 2 @@ -167,6 +279,30 @@ This schema accepts one of the following structures: | **migrations** | `{ strategy: Enum<'parallel' \| 'sequential' \| 'on_demand'>; maxConcurrent: integer; rollbackOnError: boolean }` | optional | Migration configuration | | **performance** | `{ poolPerSchema: boolean; schemaCacheTTL: integer }` | optional | Performance settings | +### Nested Shape: `TenantIsolationConfig.schema` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **namingPattern** | `string` | optional (default: `"tenant_{tenant_id}"`) | Schema naming pattern | +| **includePublicSchema** | `boolean` | optional (default: `true`) | Include public schema | +| **sharedSchema** | `string` | optional (default: `"public"`) | Schema for shared resources | +| **autoCreateSchema** | `boolean` | optional (default: `true`) | Auto-create schema | + +### Nested Shape: `TenantIsolationConfig.migrations` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **strategy** | `Enum<'parallel' \| 'sequential' \| 'on_demand'>` | optional (default: `"parallel"`) | Migration strategy | +| **maxConcurrent** | `integer` | optional (default: `10`) | Max concurrent migrations | +| **rollbackOnError** | `boolean` | optional (default: `true`) | Rollback on error | + +### Nested Shape: `TenantIsolationConfig.performance` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **poolPerSchema** | `boolean` | optional (default: `false`) | Separate pool per schema | +| **schemaCacheTTL** | `integer` | optional (default: `3600`) | Schema cache TTL | + --- #### Option 3 @@ -181,6 +317,40 @@ This schema accepts one of the following structures: | **backup** | `{ strategy: Enum<'individual' \| 'consolidated' \| 'on_demand'>; frequencyHours: integer; retentionDays: integer }` | optional | Backup configuration | | **encryption** | `{ perTenantKeys: boolean; algorithm: string; keyManagement?: Enum<'aws_kms' \| 'azure_key_vault' \| 'gcp_kms' \| 'hashicorp_vault' \| 'custom'> }` | optional | Encryption configuration | +### Nested Shape: `TenantIsolationConfig.database` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **namingPattern** | `string` | optional (default: `"tenant_{tenant_id}"`) | Database naming pattern | +| **serverStrategy** | `Enum<'shared' \| 'sharded' \| 'dedicated'>` | optional (default: `"shared"`) | Server assignment strategy | +| **separateCredentials** | `boolean` | optional (default: `true`) | Separate credentials per tenant | +| **autoCreateDatabase** | `boolean` | optional (default: `true`) | Auto-create database | + +### Nested Shape: `TenantIsolationConfig.connectionPool` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **poolSize** | `integer` | optional (default: `10`) | Connection pool size | +| **maxActivePools** | `integer` | optional (default: `100`) | Max active pools | +| **idleTimeout** | `integer` | optional (default: `300`) | Idle pool timeout | +| **usePooler** | `boolean` | optional (default: `true`) | Use connection pooler | + +### Nested Shape: `TenantIsolationConfig.backup` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **strategy** | `Enum<'individual' \| 'consolidated' \| 'on_demand'>` | optional (default: `"individual"`) | Backup strategy | +| **frequencyHours** | `integer` | optional (default: `24`) | Backup frequency | +| **retentionDays** | `integer` | optional (default: `30`) | Backup retention days | + +### Nested Shape: `TenantIsolationConfig.encryption` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **perTenantKeys** | `boolean` | optional (default: `false`) | Per-tenant encryption keys | +| **algorithm** | `string` | optional (default: `"AES-256-GCM"`) | Encryption algorithm | +| **keyManagement** | `Enum<'aws_kms' \| 'azure_key_vault' \| 'gcp_kms' \| 'hashicorp_vault' \| 'custom'>` | optional | Key management service | + --- @@ -224,6 +394,32 @@ This schema accepts one of the following structures: | **accessControl** | `{ requireMFA: boolean; requireSSO: boolean; ipWhitelist?: string[]; sessionTimeout: integer }` | optional | Access control requirements | | **compliance** | `{ standards?: Enum<'sox' \| 'hipaa' \| 'gdpr' \| 'pci_dss' \| 'iso_27001' \| 'fedramp'>[]; requireAuditLog: boolean; auditRetentionDays: integer; dataResidency?: object }` | optional | Compliance requirements | +### Nested Shape: `TenantSecurityPolicy.encryption` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **atRest** | `boolean` | optional (default: `true`) | Require encryption at rest | +| **inTransit** | `boolean` | optional (default: `true`) | Require encryption in transit | +| **fieldLevel** | `boolean` | optional (default: `false`) | Require field-level encryption | + +### Nested Shape: `TenantSecurityPolicy.accessControl` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **requireMFA** | `boolean` | optional (default: `false`) | Require MFA | +| **requireSSO** | `boolean` | optional (default: `false`) | Require SSO | +| **ipWhitelist** | `string[]` | optional | Allowed IP addresses | +| **sessionTimeout** | `integer` | optional (default: `3600`) | Session timeout | + +### Nested Shape: `TenantSecurityPolicy.compliance` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **standards** | `Enum<'sox' \| 'hipaa' \| 'gdpr' \| 'pci_dss' \| 'iso_27001' \| 'fedramp'>[]` | optional | Compliance standards | +| **requireAuditLog** | `boolean` | optional (default: `true`) | Require audit logging | +| **auditRetentionDays** | `integer` | optional (default: `365`) | Audit retention days | +| **dataResidency** | `{ region?: string; excludeRegions?: string[] }` | optional | Data residency requirements | + --- diff --git a/content/docs/references/system/tracing.mdx b/content/docs/references/system/tracing.mdx index 323bc273d5..bcbae79b68 100644 --- a/content/docs/references/system/tracing.mdx +++ b/content/docs/references/system/tracing.mdx @@ -44,6 +44,37 @@ OpenTelemetry compatibility configuration | **instrumentation** | `{ autoInstrumentation: boolean; libraries?: string[]; disabledLibraries?: string[] }` | optional | | | **semanticConventionsVersion** | `string` | optional | Semantic conventions version | +### Nested Shape: `OpenTelemetryCompatibility.exporter` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'otlp_http' \| 'otlp_grpc' \| 'jaeger' \| 'zipkin' \| 'console' \| 'datadog' \| …>` | ✅ | Exporter type | +| **endpoint** | `string` | optional | Exporter endpoint | +| **protocol** | `string` | optional | Protocol version | +| **headers** | `Record` | optional | HTTP headers | +| **timeout** | `integer` | optional (default: `10000`) | | +| **compression** | `Enum<'none' \| 'gzip'>` | optional (default: `"none"`) | | +| **batch** | `{ maxBatchSize: integer; maxQueueSize: integer; exportTimeout: integer; scheduledDelay: integer }` | optional | | + +### Nested Shape: `OpenTelemetryCompatibility.resource` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **serviceName** | `string` | ✅ | Service name | +| **serviceVersion** | `string` | optional | Service version | +| **serviceInstanceId** | `string` | optional | Service instance ID | +| **serviceNamespace** | `string` | optional | Service namespace | +| **deploymentEnvironment** | `string` | optional | Deployment environment | +| **attributes** | `Record` | optional | Additional resource attributes | + +### Nested Shape: `OpenTelemetryCompatibility.instrumentation` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **autoInstrumentation** | `boolean` | optional (default: `true`) | | +| **libraries** | `string[]` | optional | Enabled libraries | +| **disabledLibraries** | `string[]` | optional | Disabled libraries | + --- @@ -119,6 +150,51 @@ OpenTelemetry span | **resource** | `Record` | optional | Resource attributes | | **instrumentationLibrary** | `{ name: string; version?: string }` | optional | | +### Nested Shape: `Span.context` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **traceId** | `string` | ✅ | Trace ID (32 hex chars) | +| **spanId** | `string` | ✅ | Span ID (16 hex chars) | +| **traceFlags** | `integer` | optional (default: `1`) | Trace flags bitmap | +| **traceState** | `{ entries: Record }` | optional | Trace state | +| **parentSpanId** | `string` | optional | Parent span ID (16 hex chars) | +| **sampled** | `boolean` | optional (default: `true`) | | +| **remote** | `boolean` | optional (default: `false`) | | + +### Nested Shape: `Span.status` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'unset' \| 'ok' \| 'error'>` | ✅ | Status code | +| **message** | `string` | optional | Status message | + +### Nested Shape: `Span.events[number]` + +Span event + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Event name | +| **timestamp** | `string` | ✅ | Event timestamp | +| **attributes** | `Record` | optional | Event attributes | + +### Nested Shape: `Span.links[number]` + +Span link + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **context** | `{ traceId: string; spanId: string; traceFlags: integer; traceState?: object; … }` | ✅ | Linked trace context | +| **attributes** | `Record` | optional | Link attributes | + +### Nested Shape: `Span.instrumentationLibrary` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Library name | +| **version** | `string` | optional | Library version | + --- @@ -219,6 +295,18 @@ Span link | **context** | `{ traceId: string; spanId: string; traceFlags: integer; traceState?: object; … }` | ✅ | Linked trace context | | **attributes** | `Record` | optional | Link attributes | +### Nested Shape: `SpanLink.context` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **traceId** | `string` | ✅ | Trace ID (32 hex chars) | +| **spanId** | `string` | ✅ | Span ID (16 hex chars) | +| **traceFlags** | `integer` | optional (default: `1`) | Trace flags bitmap | +| **traceState** | `{ entries: Record }` | optional | Trace state | +| **parentSpanId** | `string` | optional | Parent span ID (16 hex chars) | +| **sampled** | `boolean` | optional (default: `true`) | | +| **remote** | `boolean` | optional (default: `false`) | | + --- @@ -251,6 +339,12 @@ Trace context (W3C Trace Context) | **sampled** | `boolean` | optional (default: `true`) | | | **remote** | `boolean` | optional (default: `false`) | | +### Nested Shape: `TraceContext.traceState` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **entries** | `Record` | ✅ | Trace state entries | + --- @@ -313,6 +407,32 @@ Trace sampling configuration | **rules** | `{ name: string; match?: object; decision: Enum<'drop' \| 'record_only' \| 'record_and_sample'>; rate?: number }[]` | optional (default: `[]`) | | | **customSamplerId** | `string` | optional | Custom sampler identifier | +### Nested Shape: `TraceSamplingConfig.parentBased` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **whenParentSampled** | `Enum<'always_on' \| 'always_off' \| 'trace_id_ratio' \| 'rate_limiting' \| 'parent_based' \| …>` | optional (default: `"always_on"`) | Sampling strategy type | +| **whenParentNotSampled** | `Enum<'always_on' \| 'always_off' \| 'trace_id_ratio' \| 'rate_limiting' \| 'parent_based' \| …>` | optional (default: `"always_off"`) | Sampling strategy type | +| **root** | `Enum<'always_on' \| 'always_off' \| 'trace_id_ratio' \| 'rate_limiting' \| 'parent_based' \| …>` | optional (default: `"trace_id_ratio"`) | Sampling strategy type | +| **rootRatio** | `number` | optional (default: `0.1`) | | + +### Nested Shape: `TraceSamplingConfig.composite[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **strategy** | `Enum<'always_on' \| 'always_off' \| 'trace_id_ratio' \| 'rate_limiting' \| 'parent_based' \| …>` | ✅ | Strategy type | +| **ratio** | `number` | optional | | +| **condition** | `Record \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Condition for this strategy — structured filter or CEL predicate | + +### Nested Shape: `TraceSamplingConfig.rules[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Rule name | +| **match** | `{ service?: string; spanName?: string; attributes?: Record }` | optional | | +| **decision** | `Enum<'drop' \| 'record_only' \| 'record_and_sample'>` | ✅ | Sampling decision | +| **rate** | `number` | optional | | + --- @@ -348,6 +468,28 @@ Tracing configuration | **customTraceIdGeneratorId** | `string` | optional | Custom generator identifier | | **performance** | `{ asyncExport?: boolean; exportInterval?: integer }` | optional | | +### Nested Shape: `TracingConfig.sampling` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'always_on' \| 'always_off' \| 'trace_id_ratio' \| 'rate_limiting' \| 'parent_based' \| …>` | ✅ | Sampling strategy | +| **ratio** | `number` | optional | Sample ratio (0-1) | +| **rateLimit** | `number` | optional | Traces per second | +| **parentBased** | `{ whenParentSampled?: Enum<'always_on' \| 'always_off' \| 'trace_id_ratio' \| 'rate_limiting' \| 'parent_based' \| …>; whenParentNotSampled?: Enum<'always_on' \| 'always_off' \| 'trace_id_ratio' \| 'rate_limiting' \| 'parent_based' \| …>; root?: Enum<'always_on' \| 'always_off' \| 'trace_id_ratio' \| 'rate_limiting' \| 'parent_based' \| …>; rootRatio?: number }` | optional | | +| **composite** | `{ strategy: Enum<'always_on' \| 'always_off' \| 'trace_id_ratio' \| 'rate_limiting' \| 'parent_based' \| …>; ratio?: number; condition?: Record \| string \| object }[]` | optional | | +| **rules** | `{ name: string; match?: object; decision: Enum<'drop' \| 'record_only' \| 'record_and_sample'>; rate?: number }[]` | optional (default: `[]`) | | +| **customSamplerId** | `string` | optional | Custom sampler identifier | + +### Nested Shape: `TracingConfig.openTelemetry` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **sdkVersion** | `string` | optional | OTel SDK version | +| **exporter** | `{ type: Enum<'otlp_http' \| 'otlp_grpc' \| 'jaeger' \| 'zipkin' \| 'console' \| 'datadog' \| …>; endpoint?: string; protocol?: string; headers?: Record; … }` | ✅ | Exporter configuration | +| **resource** | `{ serviceName: string; serviceVersion?: string; serviceInstanceId?: string; serviceNamespace?: string; … }` | ✅ | Resource attributes | +| **instrumentation** | `{ autoInstrumentation?: boolean; libraries?: string[]; disabledLibraries?: string[] }` | optional | | +| **semanticConventionsVersion** | `string` | optional | Semantic conventions version | + --- diff --git a/content/docs/references/system/training.mdx b/content/docs/references/system/training.mdx index 8c848875a1..d8145cfe0b 100644 --- a/content/docs/references/system/training.mdx +++ b/content/docs/references/system/training.mdx @@ -112,6 +112,23 @@ Organizational training plan per ISO 27001:2022 A.6.3 | **sendReminders** | `boolean` | optional (default: `true`) | Send reminders for upcoming training deadlines | | **reminderDaysBefore** | `number` | optional (default: `14`) | Days before deadline to send first reminder | +### Nested Shape: `TrainingPlan.courses[number]` + +Security training course definition + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique course identifier | +| **title** | `string` | ✅ | Course title | +| **description** | `string` | ✅ | Course description and learning objectives | +| **category** | `Enum<'security_awareness' \| 'data_protection' \| 'incident_response' \| …>` | ✅ | Training category | +| **durationMinutes** | `number` | ✅ | Estimated course duration in minutes | +| **mandatory** | `boolean` | optional (default: `false`) | Whether training is mandatory | +| **targetRoles** | `string[]` | ✅ | Target roles or groups | +| **validityDays** | `number` | optional | Certification validity period in days | +| **passingScore** | `number` | optional | Minimum passing score percentage | +| **version** | `string` | optional | Course content version | + --- diff --git a/content/docs/references/system/translation.mdx b/content/docs/references/system/translation.mdx index f70261e709..c0d5aa4d45 100644 --- a/content/docs/references/system/translation.mdx +++ b/content/docs/references/system/translation.mdx @@ -95,6 +95,49 @@ Translation data for a single object | **_sections** | `Record` | optional | Section translations keyed by section name | | **_tabs** | `Record` | optional | Filter-preset tab translations keyed by tab name | +### Nested Shape: `ObjectTranslationData.fields[string]` + +Translation data for a single field + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string` | optional | Translated field label | +| **help** | `string` | optional | Translated help text | +| **placeholder** | `string` | optional | Translated placeholder text for form inputs | +| **options** | `Record` | optional | Option value to translated label map | + +### Nested Shape: `ObjectTranslationData._views[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string` | optional | Translated view label | +| **description** | `string` | optional | Translated view description | +| **emptyState** | `{ title?: string; message?: string }` | optional | Translated empty-state copy shown when the view has no rows | + +### Nested Shape: `ObjectTranslationData._actions[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string` | optional | Translated action label | +| **description** | `string` | optional | Translated action description — the explanatory line under the title in the action's param dialog | +| **confirmText** | `string` | optional | Translated confirmation prompt | +| **successMessage** | `string` | optional | Translated success toast/message | +| **params** | `Record }>` | optional | Action parameter translations keyed by parameter name | +| **resultDialog** | `{ title?: string; description?: string; acknowledge?: string; fields?: Record }` | optional | Translations for the action result dialog | + +### Nested Shape: `ObjectTranslationData._sections[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string` | optional | Translated section label | +| **description** | `string` | optional | Translated section description | + +### Nested Shape: `ObjectTranslationData._tabs[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string` | optional | Translated tab label | + --- @@ -141,6 +184,31 @@ Aggregated translation coverage result | **items** | `{ key: string; status: Enum<'missing' \| 'redundant' \| 'stale'>; objectName?: string; locale: string; … }[]` | ✅ | Detailed diff items | | **breakdown** | `{ group: string; totalKeys: integer; translatedKeys: integer; coveragePercent: number }[]` | optional | Per-group coverage breakdown | +### Nested Shape: `TranslationCoverageResult.items[number]` + +A single translation diff item + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **key** | `string` | ✅ | Dot-path translation key | +| **status** | `Enum<'missing' \| 'redundant' \| 'stale'>` | ✅ | Diff status of this translation key | +| **objectName** | `string` | optional | Associated object name (snake_case) | +| **locale** | `string` | ✅ | BCP-47 locale code | +| **sourceHash** | `string` | optional | Hash of source metadata for precise stale detection | +| **aiSuggested** | `string` | optional | AI-suggested translation for this key | +| **aiConfidence** | `number` | optional | AI suggestion confidence score (0–1) | + +### Nested Shape: `TranslationCoverageResult.breakdown[number]` + +Coverage breakdown for a single translation group + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **group** | `string` | ✅ | Translation group category | +| **totalKeys** | `integer` | ✅ | Total keys in this group | +| **translatedKeys** | `integer` | ✅ | Translated keys in this group | +| **coveragePercent** | `number` | ✅ | Coverage percentage for this group | + --- @@ -163,6 +231,91 @@ Translation data for objects, apps, and UI messages | **metadataForms** | `Record; fields?: Record }>` | optional | Translations for metadata-type configuration forms keyed by metadata type | | **settingsCommon** | `{ sourceLabels?: object }` | optional | Cross-namespace Settings UI strings | +### Nested Shape: `TranslationData.objects[string]` + +Translation data for a single object + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string` | optional | Translated singular label | +| **pluralLabel** | `string` | optional | Translated plural label | +| **description** | `string` | optional | Translated object description | +| **fields** | `Record }>` | optional | Field-level translations | +| **_views** | `Record` | optional | View translations keyed by view name | +| **_actions** | `Record` | optional | Action translations keyed by action name | +| **_sections** | `Record` | optional | Section translations keyed by section name | +| **_tabs** | `Record` | optional | Filter-preset tab translations keyed by tab name | + +### Nested Shape: `TranslationData.apps[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string` | ✅ | Translated app label | +| **description** | `string` | optional | Translated app description | +| **navigation** | `Record` | optional | Navigation group translations keyed by group ID | + +### Nested Shape: `TranslationData.globalActions[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string` | optional | Translated action label | +| **description** | `string` | optional | Translated action description — the explanatory line under the title in the action's param dialog | +| **confirmText** | `string` | optional | Translated confirmation prompt | +| **successMessage** | `string` | optional | Translated success toast/message | +| **params** | `Record }>` | optional | Action parameter translations keyed by parameter name | +| **resultDialog** | `{ title?: string; description?: string; acknowledge?: string; fields?: Record }` | optional | Translations for the action result dialog | + +### Nested Shape: `TranslationData.dashboards[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string` | optional | Translated dashboard title | +| **description** | `string` | optional | Translated dashboard description | +| **actions** | `Record` | optional | Header action label translations keyed by action url/key | +| **widgets** | `Record` | optional | Widget translations keyed by widget id | + +### Nested Shape: `TranslationData.pages[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string` | optional | Translated page label (nav / breadcrumb) | +| **description** | `string` | optional | Translated page description | +| **title** | `string` | optional | Translated `page:header` title (defaults to `label`) | +| **subtitle** | `string` | optional | Translated `page:header` subtitle | +| **components** | `Record` | optional | Per-component copy keyed by component id (`PageComponentSchema.id`) | + +### Nested Shape: `TranslationData.flows[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string` | optional | Translated flow label | +| **screens** | `Record }>` | optional | Screen translations keyed by screen node id (`FlowNode.id`, the client's `ScreenSpec.nodeId`) | + +### Nested Shape: `TranslationData.settings[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **title** | `string` | optional | Translated settings manifest title | +| **description** | `string` | optional | Translated settings manifest description | +| **groups** | `Record` | optional | Group translations keyed by group key | +| **keys** | `Record }>` | optional | Per-setting field translations keyed by setting key | +| **actions** | `Record` | optional | Action button translations keyed by action id | + +### Nested Shape: `TranslationData.metadataForms[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string` | optional | Translated metadata-type display label (overrides registry label) | +| **description** | `string` | optional | Translated metadata-type description | +| **sections** | `Record` | optional | Section translations keyed by section.name | +| **fields** | `Record` | optional | Field translations keyed by field path (dot-notation for nested fields) | + +### Nested Shape: `TranslationData.settingsCommon` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **sourceLabels** | `{ env?: string; global?: string; tenant?: string; user?: string; … }` | optional | Source badge labels by resolution layer | + --- @@ -227,6 +380,91 @@ One locale of translations — the `translation` metadata type | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `TranslationItem.objects[string]` + +Translation data for a single object + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string` | optional | Translated singular label | +| **pluralLabel** | `string` | optional | Translated plural label | +| **description** | `string` | optional | Translated object description | +| **fields** | `Record }>` | optional | Field-level translations | +| **_views** | `Record` | optional | View translations keyed by view name | +| **_actions** | `Record` | optional | Action translations keyed by action name | +| **_sections** | `Record` | optional | Section translations keyed by section name | +| **_tabs** | `Record` | optional | Filter-preset tab translations keyed by tab name | + +### Nested Shape: `TranslationItem.apps[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string` | ✅ | Translated app label | +| **description** | `string` | optional | Translated app description | +| **navigation** | `Record` | optional | Navigation group translations keyed by group ID | + +### Nested Shape: `TranslationItem.globalActions[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string` | optional | Translated action label | +| **description** | `string` | optional | Translated action description — the explanatory line under the title in the action's param dialog | +| **confirmText** | `string` | optional | Translated confirmation prompt | +| **successMessage** | `string` | optional | Translated success toast/message | +| **params** | `Record }>` | optional | Action parameter translations keyed by parameter name | +| **resultDialog** | `{ title?: string; description?: string; acknowledge?: string; fields?: Record }` | optional | Translations for the action result dialog | + +### Nested Shape: `TranslationItem.dashboards[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string` | optional | Translated dashboard title | +| **description** | `string` | optional | Translated dashboard description | +| **actions** | `Record` | optional | Header action label translations keyed by action url/key | +| **widgets** | `Record` | optional | Widget translations keyed by widget id | + +### Nested Shape: `TranslationItem.pages[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string` | optional | Translated page label (nav / breadcrumb) | +| **description** | `string` | optional | Translated page description | +| **title** | `string` | optional | Translated `page:header` title (defaults to `label`) | +| **subtitle** | `string` | optional | Translated `page:header` subtitle | +| **components** | `Record` | optional | Per-component copy keyed by component id (`PageComponentSchema.id`) | + +### Nested Shape: `TranslationItem.flows[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string` | optional | Translated flow label | +| **screens** | `Record }>` | optional | Screen translations keyed by screen node id (`FlowNode.id`, the client's `ScreenSpec.nodeId`) | + +### Nested Shape: `TranslationItem.settings[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **title** | `string` | optional | Translated settings manifest title | +| **description** | `string` | optional | Translated settings manifest description | +| **groups** | `Record` | optional | Group translations keyed by group key | +| **keys** | `Record }>` | optional | Per-setting field translations keyed by setting key | +| **actions** | `Record` | optional | Action button translations keyed by action id | + +### Nested Shape: `TranslationItem.metadataForms[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string` | optional | Translated metadata-type display label (overrides registry label) | +| **description** | `string` | optional | Translated metadata-type description | +| **sections** | `Record` | optional | Section translations keyed by section.name | +| **fields** | `Record` | optional | Field translations keyed by field path (dot-notation for nested fields) | + +### Nested Shape: `TranslationItem.settingsCommon` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **sourceLabels** | `{ env?: string; global?: string; tenant?: string; user?: string; … }` | optional | Source badge labels by resolution layer | + --- diff --git a/content/docs/references/system/worker.mdx b/content/docs/references/system/worker.mdx index 3ac7d9cfd4..d431b0efa1 100644 --- a/content/docs/references/system/worker.mdx +++ b/content/docs/references/system/worker.mdx @@ -81,6 +81,33 @@ const result = BatchProgressSchema.parse(data); | **priority** | `integer` | optional (default: `0`) | Queue priority (lower = higher priority) | | **autoScale** | `{ enabled: boolean; minWorkers: integer; maxWorkers: integer; scaleUpThreshold: integer; … }` | optional | Auto-scaling configuration | +### Nested Shape: `QueueConfig.rateLimit` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **max** | `integer` | ✅ | Maximum tasks per duration | +| **duration** | `integer` | ✅ | Duration in milliseconds | + +### Nested Shape: `QueueConfig.defaultRetryPolicy` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **maxRetries** | `integer` | optional (default: `3`) | Maximum retry attempts | +| **backoffStrategy** | `Enum<'fixed' \| 'linear' \| 'exponential'>` | optional (default: `"exponential"`) | Backoff strategy between retries | +| **initialDelayMs** | `integer` | optional (default: `1000`) | Initial retry delay in milliseconds | +| **maxDelayMs** | `integer` | optional (default: `60000`) | Maximum retry delay in milliseconds | +| **backoffMultiplier** | `number` | optional (default: `2`) | Multiplier for exponential backoff | + +### Nested Shape: `QueueConfig.autoScale` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `false`) | Enable auto-scaling | +| **minWorkers** | `integer` | optional (default: `1`) | Minimum workers | +| **maxWorkers** | `integer` | optional (default: `10`) | Maximum workers | +| **scaleUpThreshold** | `integer` | optional (default: `100`) | Queue size to scale up | +| **scaleDownThreshold** | `integer` | optional (default: `10`) | Queue size to scale down | + --- @@ -102,6 +129,25 @@ const result = BatchProgressSchema.parse(data); | **status** | `Enum<'pending' \| 'queued' \| 'processing' \| 'completed' \| 'failed' \| 'cancelled' \| 'timeout' \| 'dead'>` | optional (default: `"pending"`) | Current task status | | **metadata** | `{ createdAt?: string; updatedAt?: string; createdBy?: string; tags?: string[] }` | optional | Task metadata | +### Nested Shape: `Task.retryPolicy` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **maxRetries** | `integer` | optional (default: `3`) | Maximum retry attempts | +| **backoffStrategy** | `Enum<'fixed' \| 'linear' \| 'exponential'>` | optional (default: `"exponential"`) | Backoff strategy between retries | +| **initialDelayMs** | `integer` | optional (default: `1000`) | Initial retry delay in milliseconds | +| **maxDelayMs** | `integer` | optional (default: `60000`) | Maximum retry delay in milliseconds | +| **backoffMultiplier** | `number` | optional (default: `2`) | Multiplier for exponential backoff | + +### Nested Shape: `Task.metadata` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **createdAt** | `string` | optional | When task was created | +| **updatedAt** | `string` | optional | Last update time | +| **createdBy** | `string` | optional | User who created task | +| **tags** | `string[]` | optional | Task tags for filtering | + --- @@ -121,6 +167,14 @@ const result = BatchProgressSchema.parse(data); | **attempt** | `integer` | ✅ | Attempt number (1-indexed) | | **willRetry** | `boolean` | ✅ | Whether task will be retried | +### Nested Shape: `TaskExecutionResult.error` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **message** | `string` | ✅ | Error message | +| **stack** | `string` | optional | Error stack trace | +| **code** | `string` | optional | Error code | + --- @@ -183,6 +237,15 @@ const result = BatchProgressSchema.parse(data); | **uptimeMs** | `integer` | ✅ | Worker uptime in milliseconds | | **queues** | `Record` | optional | Per-queue statistics | +### Nested Shape: `WorkerStats.queues[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **pending** | `integer` | ✅ | Pending tasks | +| **active** | `integer` | ✅ | Active tasks | +| **completed** | `integer` | ✅ | Completed tasks | +| **failed** | `integer` | ✅ | Failed tasks | + --- diff --git a/content/docs/references/ui/action.mdx b/content/docs/references/ui/action.mdx index 9f01bf364a..ebec09be03 100644 --- a/content/docs/references/ui/action.mdx +++ b/content/docs/references/ui/action.mdx @@ -106,6 +106,64 @@ const result = ActionSchema.parse(data); | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `Action.params[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional | | +| **field** | `string` | optional | Snake case identifier (lowercase with underscores only) | +| **objectOverride** | `string` | optional | Snake case identifier (lowercase with underscores only) | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>` | optional | | +| **required** | `boolean` | optional (default: `false`) | | +| **options** | `{ label: string \| Record; value: string; visibleWhen?: string \| object }[]` | optional | | +| **placeholder** | `string` | optional | | +| **helpText** | `string` | optional | | +| **defaultValue** | `any` | optional | | +| **multiple** | `boolean` | optional | Allow multiple values (array value shape); mirrors FieldSchema.multiple. | +| **accept** | `string[]` | optional | Accepted upload types (MIME types / extensions) for file/image params. | +| **maxSize** | `integer` | optional | Max upload size in bytes for file/image params. | +| **reference** | `string` | optional | Reference target object for inline lookup/master_detail params; mirrors FieldSchema.reference. | +| **defaultFromRow** | `boolean` | optional | | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Param visibility predicate (CEL); omits the param when false. | +| **requiresFeature** | `Enum<'twoFactor' \| 'organization' \| 'multiOrgEnabled' \| 'degradedTenancy' \| …>` | optional | Public auth feature flag gating this param; lowered into `visible` at parse time. | + +### Nested Shape: `Action.resultDialog` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **title** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **description** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **acknowledge** | `string \| Record` | optional | Acknowledge button label, e.g. "I have saved this" | +| **format** | `Enum<'qrcode' \| 'code-list' \| 'secret' \| 'text' \| 'json'>` | optional | Default format for fields without their own format. Defaults to json when omitted. | +| **fields** | `{ path: string; label?: string \| Record; format?: Enum<'qrcode' \| 'code-list' \| 'secret' \| 'text' \| 'json'> }[]` | optional | Which fields from result.data to render. Omit to dump full JSON. | + +### Nested Shape: `Action.ai` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **exposed** | `boolean` | optional (default: `false`) | Expose this action to AI agents. Requires `description` when true. | +| **description** | `string` | optional | LLM-facing description (≥40 chars). Required when exposed. | +| **category** | `Enum<'data' \| 'action' \| 'flow' \| 'integration' \| 'vector_search' \| 'analytics' \| 'utility'>` | optional | Tool category override (defaults to "action"). | +| **paramHints** | `Record` | optional | Per-parameter AI hints keyed by param name. | +| **outputSchema** | `Record` | optional | JSON Schema for the action return value. | +| **requiresConfirmation** | `boolean` | optional | Override HITL confirmation for AI invocations. | + +### Nested Shape: `Action.onSuccess` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **navigate** | `string` | ✅ | Route/URL template navigated to after the action succeeds. Interpolates $`{param.*}` (params-dialog values), $`{ctx.*}` (origin/apiBase/user/org/recordId/selection) and $`{result.*}` (the action's server response payload — NEW with this key, e.g. $`{result.id}`). Relative = SPA route hop; renderers MUST encodeURIComponent values in query positions. | +| **openIn** | `Enum<'self' \| 'newTab'>` | optional (default: `"self"`) | Where to perform the post-success navigation: 'self' (default — in-place SPA navigation, immune to popup blocking) or 'newTab'. Closed enum — no general navigation DSL. | + +### Nested Shape: `Action.aria` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ariaLabel** | `string \| Record` | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. | +| **ariaDescribedBy** | `string` | optional | ID of element providing additional description (WAI-ARIA aria-describedby) | +| **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | + --- @@ -215,6 +273,14 @@ const result = ActionSchema.parse(data); * `tags` * `vector` +### Nested Shape: `ActionParam.options[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string \| Record` | ✅ | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **value** | `string` | ✅ | | +| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Per-option visibility predicate (CEL) — option is offered only when TRUE (else omitted). Same env as the field-level per-option visibleWhen (record + current_user). e.g. P`record.tier == 'gold'` | + --- @@ -252,6 +318,28 @@ const result = ActionSchema.parse(data); | **refreshAfter** | `boolean` | optional (default: `false`) | Refresh view after execution | | **opensInNewTab** | `boolean` | optional | Open the action result in a new tab. The renderer pre-opens the tab synchronously on click (popup-blocker-safe) and navigates it to the handler's redirectUrl. | +### Nested Shape: `InlineAction.params[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional | | +| **field** | `string` | optional | Snake case identifier (lowercase with underscores only) | +| **objectOverride** | `string` | optional | Snake case identifier (lowercase with underscores only) | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>` | optional | | +| **required** | `boolean` | optional (default: `false`) | | +| **options** | `{ label: string \| Record; value: string; visibleWhen?: string \| object }[]` | optional | | +| **placeholder** | `string` | optional | | +| **helpText** | `string` | optional | | +| **defaultValue** | `any` | optional | | +| **multiple** | `boolean` | optional | Allow multiple values (array value shape); mirrors FieldSchema.multiple. | +| **accept** | `string[]` | optional | Accepted upload types (MIME types / extensions) for file/image params. | +| **maxSize** | `integer` | optional | Max upload size in bytes for file/image params. | +| **reference** | `string` | optional | Reference target object for inline lookup/master_detail params; mirrors FieldSchema.reference. | +| **defaultFromRow** | `boolean` | optional | | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Param visibility predicate (CEL); omits the param when false. | +| **requiresFeature** | `Enum<'twoFactor' \| 'organization' \| 'multiOrgEnabled' \| 'degradedTenancy' \| …>` | optional | Public auth feature flag gating this param; lowered into `visible` at parse time. | + --- diff --git a/content/docs/references/ui/app.mdx b/content/docs/references/ui/app.mdx index ec41b0b742..224ebce57a 100644 --- a/content/docs/references/ui/app.mdx +++ b/content/docs/references/ui/app.mdx @@ -55,6 +55,13 @@ const result = ActionNavItemSchema.parse(data); | **type** | `'action'` | ✅ | | | **actionDef** | `{ actionName: string; params?: Record }` | ✅ | Action definition to execute when clicked | +### Nested Shape: `ActionNavItem.actionDef` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **actionName** | `string` | ✅ | Action machine name to execute | +| **params** | `Record` | optional | Parameters passed to the action | + --- @@ -95,6 +102,44 @@ const result = ActionNavItemSchema.parse(data); | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `App.branding` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **primaryColor** | `string` | optional | Primary theme color hex code | +| **accentColor** | `string` | optional | Accent color hex code (highlights, active states). Declared to match the objectui ConsoleLayout read of branding.accentColor (inverse-drift fix, liveness audit #1878/#1891/#1894). | +| **logo** | `string` | optional | Custom logo URL for this app | +| **favicon** | `string` | optional | Custom favicon URL for this app | + +### Nested Shape: `App.areas[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique area identifier (lowercase snake_case) | +| **label** | `string \| Record` | ✅ | Area display label | +| **icon** | `string` | optional | Area icon name | +| **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: `App.contextSelectors[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Selector id; selected value is exposed as the nav template var `{}` | +| **label** | `string \| Record` | ✅ | Dropdown label | +| **icon** | `string` | optional | Icon name | +| **optionsSource** | `{ endpoint: string; valueKey?: string; labelKey?: string; filter?: object[] }` | ✅ | Option data source | +| **allValue** | `string` | optional (default: `""`) | Sentinel value meaning "no concrete selection yet" (empty string is almost always right) | +| **persist** | `Enum<'query' \| 'session' \| 'none'>` | optional (default: `"query"`) | Persist selection via URL query, sessionStorage, or not at all | + +### Nested Shape: `App.protection` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | ✅ | Lock policy — none \| no-overlay \| no-delete \| full. | +| **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | +| **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | + --- @@ -125,6 +170,15 @@ const result = ActionNavItemSchema.parse(data); | **allValue** | `string` | optional (default: `""`) | Sentinel value meaning "no concrete selection yet" (empty string is almost always right) | | **persist** | `Enum<'query' \| 'session' \| 'none'>` | optional (default: `"query"`) | Persist selection via URL query, sessionStorage, or not at all | +### Nested Shape: `AppContextSelector.optionsSource` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **endpoint** | `string` | ✅ | REST endpoint returning the option rows (e.g. /api/v1/packages) | +| **valueKey** | `string` | optional (default: `"id"`) | Row property used as the option value (dotted path allowed, e.g. "manifest.id") | +| **labelKey** | `string` | optional (default: `"name"`) | Row property used as the option label (dotted path allowed, e.g. "manifest.name") | +| **filter** | `{ key: string; op: Enum<'eq' \| 'ne' \| 'in' \| 'nin'>; value: string \| string[] }[]` | optional | Predicates (AND) each option row must satisfy | + --- @@ -376,6 +430,13 @@ This schema accepts one of the following structures: | **type** | `'action'` | ✅ | | | **actionDef** | `{ actionName: string; params?: Record }` | ✅ | Action definition to execute when clicked | +### Nested Shape: `NavigationItem.actionDef` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **actionName** | `string` | ✅ | Action machine name to execute | +| **params** | `Record` | optional | Parameters passed to the action | + --- #### Option 7 diff --git a/content/docs/references/ui/bulk-action.mdx b/content/docs/references/ui/bulk-action.mdx index cf4515a65d..2a4f8b1bea 100644 --- a/content/docs/references/ui/bulk-action.mdx +++ b/content/docs/references/ui/bulk-action.mdx @@ -54,6 +54,22 @@ const result = BulkActionDefSchema.parse(data); | **maxRecords** | `integer` | optional | Selection size above which the run is blocked. Set it on defs whose server work is expensive — an aggregate def carries every selected id in one request. | | **batchSize** | `integer` | optional | Records per executor batch (default 200). Data-plane operations only — an aggregate run is a single call by definition. | +### Nested Shape: `BulkActionDef.params[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Param key — becomes params[name] in the patch / action params bag. | +| **label** | `string` | optional | Field label in the dialog. Plain string: an authored def is not i18n-resolved (see module header). | +| **help** | `string` | optional | Help text under the field. (An ActionParam spells this `helpText` — known divergence, module header.) | +| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>` | ✅ | Field widget to render, from the standard field-type vocabulary (text/number/select/lookup/date/…). | +| **required** | `boolean` | optional | Blocks the Confirm button until a value is present. | +| **default** | `any` | optional | Value applied when the dialog opens. (An ActionParam spells this `defaultValue`.) | +| **options** | `({ label: string; value: string \| number \| boolean } & Record)[]` | optional | Static options for select-style widgets. Each entry is `{ label, value }` plus any extra widget config — the entry is open (`.passthrough()`) because the renderer forwards unknown option keys to the field widget, which reads `color` / `icon` / `disabled` / `visibleWhen` beyond the declared pair. | +| **object** | `string` | optional | Target object for a `lookup` widget. (An ActionParam spells this `reference`.) | +| **labelField** | `string` | optional | Related-object field used as the option label for a `lookup` widget (defaults to name/full_name/email/id). | +| **multiple** | `boolean` | optional | Allow picking multiple values — the param value becomes an array and is written to the patch as-is. | +| **placeholder** | `string` | optional | Placeholder text. | + --- @@ -148,6 +164,13 @@ const result = BulkActionDefSchema.parse(data); * `tags` * `vector` +### Nested Shape: `BulkActionParam.options[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string` | ✅ | Option label (plain string — not i18n-resolved on this path). | +| **value** | `string \| number \| boolean` | ✅ | Stored value. | + --- diff --git a/content/docs/references/ui/chart.mdx b/content/docs/references/ui/chart.mdx index cd15f19d60..1719bab1c8 100644 --- a/content/docs/references/ui/chart.mdx +++ b/content/docs/references/ui/chart.mdx @@ -38,6 +38,14 @@ Inline aggregation for an object-bound chart | **function** | `Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max'>` | ✅ | Aggregation function | | **groupBy** | `string \| { field: string; dateGranularity?: Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>; alias?: string }` | ✅ | Field the rows are grouped by — the chart category axis | +### Nested Shape: `ChartAggregate.groupBy` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field to group by | +| **dateGranularity** | `Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>` | optional | Bucket date values into uniform periods | +| **alias** | `string` | optional | Alias for the projected group value (defaults to `field`) — this becomes the category column | + --- @@ -134,6 +142,75 @@ Inline aggregation for an object-bound chart * `table` * `pivot` +### Nested Shape: `ChartConfig.xAxis` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Data field key | +| **title** | `string \| Record` | optional | Axis display title | +| **format** | `string` | optional | Value format string (e.g., "$0,0.00") | +| **min** | `number` | optional | Minimum value | +| **max** | `number` | optional | Maximum value | +| **stepSize** | `number` | optional | Step size for ticks | +| **showGridLines** | `boolean` | optional (default: `true`) | | +| **position** | `Enum<'left' \| 'right' \| 'top' \| 'bottom'>` | optional | Axis position | +| **logarithmic** | `boolean` | optional (default: `false`) | | + +### Nested Shape: `ChartConfig.yAxis[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Data field key | +| **title** | `string \| Record` | optional | Axis display title | +| **format** | `string` | optional | Value format string (e.g., "$0,0.00") | +| **min** | `number` | optional | Minimum value | +| **max** | `number` | optional | Maximum value | +| **stepSize** | `number` | optional | Step size for ticks | +| **showGridLines** | `boolean` | optional (default: `true`) | | +| **position** | `Enum<'left' \| 'right' \| 'top' \| 'bottom'>` | optional | Axis position | +| **logarithmic** | `boolean` | optional (default: `false`) | | + +### Nested Shape: `ChartConfig.series[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Field name or series identifier | +| **label** | `string \| Record` | optional | Series display label | +| **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| …>` | optional | Override chart type for this series | +| **color** | `string` | optional | Series color (hex/rgb/token) | +| **stack** | `string` | optional | Stack identifier to group series | +| **yAxis** | `Enum<'left' \| 'right'>` | optional (default: `"left"`) | Bind to specific Y-Axis | +| **variant** | `Enum<'primary' \| 'comparison'>` | optional (default: `"primary"`) | Series visual role | +| **dashArray** | `string` | optional | SVG stroke-dasharray override | +| **opacity** | `number` | optional | Series opacity override | + +### Nested Shape: `ChartConfig.annotations[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'line' \| 'region'>` | optional (default: `"line"`) | | +| **axis** | `Enum<'x' \| 'y'>` | optional (default: `"y"`) | | +| **value** | `number \| string` | ✅ | Start value | +| **endValue** | `number \| string` | optional | End value for regions | +| **color** | `string` | optional | | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **style** | `Enum<'solid' \| 'dashed' \| 'dotted'>` | optional (default: `"dashed"`) | | + +### Nested Shape: `ChartConfig.interaction` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **tooltips** | `boolean` | optional (default: `true`) | Show the hover tooltip | +| **brush** | `boolean` | optional (default: `false`) | Show the range selector under the plot | + +### Nested Shape: `ChartConfig.aria` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ariaLabel** | `string \| Record` | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. | +| **ariaDescribedBy** | `string` | optional | ID of element providing additional description (WAI-ARIA aria-describedby) | +| **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | + --- diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index 47e21db4d4..754d51720c 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -34,6 +34,14 @@ const result = AIChatWindowProps.parse(data); | **context** | `Record` | optional | Contextual data to pass to the AI | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +### Nested Shape: `AIChatWindowProps.aria` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ariaLabel** | `string \| Record` | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. | +| **ariaDescribedBy** | `string` | optional | ID of element providing additional description (WAI-ARIA aria-describedby) | +| **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | + --- @@ -52,6 +60,32 @@ const result = AIChatWindowProps.parse(data); | **action** | `{ type?: Enum<'script' \| 'url' \| 'modal' \| 'flow' \| 'api' \| 'form'>; name?: string; label?: string \| Record; target?: string; … }` | optional | Inline action executed on click | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +### Nested Shape: `ElementButtonProps.action` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'script' \| 'url' \| 'modal' \| 'flow' \| 'api' \| 'form'>` | optional (default: `"script"`) | Action functionality type | +| **name** | `string` | optional | Machine name (lowercase snake_case) | +| **label** | `string \| Record` | optional | Display label | +| **target** | `string` | optional | URL, Script Name, Flow ID, or API Endpoint. Supports $`{param.X}` and $`{ctx.X}` interpolation. | +| **openIn** | `Enum<'self' \| 'new-tab'>` | optional | For type:'url' — where to open `target`. 'new-tab' opens a new browser tab; 'self' navigates in place. When omitted, external/absolute URLs open in a new tab and relative URLs navigate in place. Static execution option — keep it OUT of `params` (which is user-input-collection only). | +| **method** | `Enum<'POST' \| 'PATCH' \| 'PUT' \| 'DELETE'>` | optional | HTTP method for type:"api" actions. Defaults to POST. | +| **params** | `{ name?: string; field?: string; objectOverride?: string; label?: string \| Record; … }[]` | optional | Input parameters required from user — an ActionParam[] DEFINITION array, never a payload map (a static request body goes in `bodyExtra`). | +| **bodyExtra** | `Record` | optional | Static request-body fields for a type:"api" action, merged last (overrides user params). `{{page.}}` tokens are resolved by the runtime. This — not `params` — is where a payload goes. | +| **confirmText** | `string \| Record` | optional | Confirmation message before execution. On a registered action, pairing this with a non-empty `params` is refused (#7428) — that opens a second dialog for one decision; put the question on `description` instead. Correct on a param-LESS action, where the confirm is the only dialog there is. | +| **successMessage** | `string \| Record` | optional | Success message to show after execution | +| **errorMessage** | `string \| Record` | optional | Error message to show when the action fails (overrides the raw error). | +| **refreshAfter** | `boolean` | optional (default: `false`) | Refresh view after execution | +| **opensInNewTab** | `boolean` | optional | Open the action result in a new tab. The renderer pre-opens the tab synchronously on click (popup-blocker-safe) and navigates it to the handler's redirectUrl. | + +### Nested Shape: `ElementButtonProps.aria` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ariaLabel** | `string \| Record` | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. | +| **ariaDescribedBy** | `string` | optional | ID of element providing additional description (WAI-ARIA aria-describedby) | +| **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | + --- @@ -99,6 +133,14 @@ const result = AIChatWindowProps.parse(data); | **height** | `number` | optional | Fixed height in pixels | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +### Nested Shape: `ElementImageProps.aria` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ariaLabel** | `string \| Record` | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. | +| **ariaDescribedBy** | `string` | optional | ID of element providing additional description (WAI-ARIA aria-describedby) | +| **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | + --- @@ -115,6 +157,14 @@ const result = AIChatWindowProps.parse(data); | **detail** | `Enum<'business' \| 'technical'>` | optional (default: `"business"`) | Authoring altitude (ADR-0051 §3.4): business collapses technical flow nodes to business steps + approvals. NOT access (cf. book.audience); permission projection is automatic and render-time, never set here | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +### Nested Shape: `ElementMetadataViewerProps.aria` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ariaLabel** | `string \| Record` | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. | +| **ariaDescribedBy** | `string` | optional | ID of element providing additional description (WAI-ARIA aria-describedby) | +| **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | + --- @@ -133,6 +183,14 @@ const result = AIChatWindowProps.parse(data); | **suffix** | `string` | optional | Suffix text (e.g. "%") | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +### Nested Shape: `ElementNumberProps.aria` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ariaLabel** | `string \| Record` | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. | +| **ariaDescribedBy** | `string` | optional | ID of element providing additional description (WAI-ARIA aria-describedby) | +| **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | + --- @@ -157,6 +215,23 @@ const result = AIChatWindowProps.parse(data); | **multiple** | `never` | optional | [REMOVED] `element:record_picker` property `multiple` was removed in @objectstack/spec 17.0.0 (#5775, ADR-0049) — the picker is a single-select `Select` and the bound page variable holds one record id, so `multiple: true` selected nothing extra and reported success. Delete the key; multi-record selection is not implemented on this element. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +### Nested Shape: `ElementRecordPickerProps.sort[number]` + +Sort field and direction pair + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name to sort by | +| **order** | `Enum<'asc' \| 'desc'>` | ✅ | Sort direction | + +### Nested Shape: `ElementRecordPickerProps.aria` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ariaLabel** | `string \| Record` | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. | +| **ariaDescribedBy** | `string` | optional | ID of element providing additional description (WAI-ARIA aria-describedby) | +| **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | + --- @@ -176,6 +251,14 @@ const result = AIChatWindowProps.parse(data); | **targetVariable** | `never` | optional | [REMOVED] `element:text_input` property `targetVariable` was removed in @objectstack/spec 17 (#9198, ADR-0049) — it was a declarative hint no renderer ever read: the live binding runs the other direction, resolved from the page variable whose `source` names this component's `id`, so authoring only `targetVariable` bound nothing while reporting success. Delete the key; to bind the typed value, declare it on the variable — `variables: [{ name: '', type: 'string', source: '' }]`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +### Nested Shape: `ElementTextInputProps.aria` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ariaLabel** | `string \| Record` | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. | +| **ariaDescribedBy** | `string` | optional | ID of element providing additional description (WAI-ARIA aria-describedby) | +| **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | + --- @@ -190,6 +273,14 @@ const result = AIChatWindowProps.parse(data); | **align** | `Enum<'left' \| 'center' \| 'right'>` | optional (default: `"left"`) | Text alignment | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +### Nested Shape: `ElementTextProps.aria` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ariaLabel** | `string \| Record` | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. | +| **ariaDescribedBy** | `string` | optional | ID of element providing additional description (WAI-ARIA aria-describedby) | +| **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | + --- @@ -394,6 +485,23 @@ const result = AIChatWindowProps.parse(data); | **variant** | `Enum<'flush' \| 'card'>` | optional (default: `"flush"`) | Panel framing: 'flush' draws a divider under each panel; 'card' leaves the border to each panel's own content | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +### Nested Shape: `PageAccordionProps.items[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string \| Record` | ✅ | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **icon** | `string` | optional | Lucide icon name rendered in the panel trigger, left of the label. Read on this component — the renderer draws it via `LazyIcon`; contrast the item `value` beside it, which the renderer overwrites with `panel-`. | +| **collapsed** | `boolean` | optional (default: `false`) | | +| **children** | `any[]` | ✅ | Child components | + +### Nested Shape: `PageAccordionProps.aria` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ariaLabel** | `string \| Record` | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. | +| **ariaDescribedBy** | `string` | optional | ID of element providing additional description (WAI-ARIA aria-describedby) | +| **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | + --- @@ -411,6 +519,14 @@ const result = AIChatWindowProps.parse(data); | **footer** | `any[]` | optional | Card footer components (slot) | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +### Nested Shape: `PageCardProps.aria` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ariaLabel** | `string \| Record` | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. | +| **ariaDescribedBy** | `string` | optional | ID of element providing additional description (WAI-ARIA aria-describedby) | +| **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | + --- @@ -443,6 +559,14 @@ const result = AIChatWindowProps.parse(data); | **mobileMaxVisible** | `integer` | optional | The `maxVisible` budget on mobile viewports (renderer default 1). | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +### Nested Shape: `PageHeaderProps.aria` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ariaLabel** | `string \| Record` | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. | +| **ariaDescribedBy** | `string` | optional | ID of element providing additional description (WAI-ARIA aria-describedby) | +| **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | + --- @@ -459,6 +583,25 @@ const result = AIChatWindowProps.parse(data); | **items** | `{ label: string \| Record; icon?: string; visibleWhen?: string \| object; value?: string; … }[]` | ✅ | | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +### Nested Shape: `PageTabsProps.items[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string \| Record` | ✅ | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **icon** | `string` | optional | Lucide icon name rendered in the tab trigger, left of the label. Read on this component — the renderer draws it via `LazyIcon`; contrast the item `key` beside it, which no read point takes and which the alias table answers with `value`. | +| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL) — the whole tab (header + panel) is omitted when FALSE; the renderer falls back to the first visible tab when the active one is hidden. Contract-bound roots: `record`, `current_user` (ADR-0068 aliases `user` / `ctx.user`), `page.`. ⚠️ NOT the same environment as page-component `visibleWhen`: this surface's own evaluator binds `data` to the record ROW (not the data-source adapter) and also spreads the row's bare fields — renderer behaviour, NOT contract-guaranteed. ADR-0089 canonical name — `visible`/`showWhen`/`visibility`/`visibleOn` are all rejected here (not folded in), each with a pointer at this key. | +| **value** | `string` | optional | Stable `?tab=` URL token for this tab (default: index-derived `tab-`, which is not durable across item-list changes) | +| **count** | `integer` | optional | Badge count shown next to the tab label (default: derived from `record:related_list` descendants) | +| **children** | `any[]` | ✅ | Child components | + +### Nested Shape: `PageTabsProps.aria` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ariaLabel** | `string \| Record` | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. | +| **ariaDescribedBy** | `string` | optional | ID of element providing additional description (WAI-ARIA aria-describedby) | +| **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | + --- @@ -481,6 +624,14 @@ const result = AIChatWindowProps.parse(data); | **showSubscriptionToggle** | `boolean` | optional (default: `true`) | Show bell icon for record-level notification subscription | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +### Nested Shape: `RecordActivityProps.aria` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ariaLabel** | `string \| Record` | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. | +| **ariaDescribedBy** | `string` | optional | ID of element providing additional description (WAI-ARIA aria-describedby) | +| **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | + --- @@ -512,6 +663,14 @@ const result = AIChatWindowProps.parse(data); | **dismissible** | `boolean` | optional | Render an X control; dismissal is remembered per object/record in localStorage (renderer default: off). | | **dismissKey** | `string` | optional | Stable key the dismissal is remembered under, so reworded titles do not resurrect a dismissed banner (renderer default: the English resolution of `title`, else the severity). | +### Nested Shape: `RecordAlertProps.action` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **actionName** | `string` | ✅ | Name of an action declared on this object (`actions[]`) — resolved from object metadata and run through the shared action engine, so confirm/param dialogs, toast and reload behave exactly as in `record:quick_actions`. | +| **label** | `string \| Record` | optional | CTA button label — a string or an inline locale map, resolved with the same pickLocalized chain as `title`/`body` (default: the action's own label). | +| **variant** | `Enum<'default' \| 'destructive' \| 'outline' \| 'secondary' \| 'ghost' \| 'link'>` | optional | Button variant — the Button primitive's own vocabulary (renderer default: `destructive` when severity is `error`, else `default`). | + --- @@ -528,6 +687,31 @@ const result = AIChatWindowProps.parse(data); | **feed** | `{ types?: Enum<'comment' \| 'field_change' \| 'task' \| 'event' \| 'email' \| 'call' \| 'note' \| …>[]; filterMode: Enum<'all' \| 'comments_only' \| 'changes_only' \| 'tasks_only'>; showFilterToggle: boolean; limit: integer; … }` | optional | Embedded activity feed configuration | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +### Nested Shape: `RecordChatterProps.feed` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **types** | `Enum<'comment' \| 'field_change' \| 'task' \| 'event' \| 'email' \| 'call' \| 'note' \| …>[]` | optional | Feed item types to show (default: all) | +| **filterMode** | `Enum<'all' \| 'comments_only' \| 'changes_only' \| 'tasks_only'>` | optional (default: `"all"`) | Default activity filter | +| **showFilterToggle** | `boolean` | optional (default: `true`) | Show filter dropdown in panel header | +| **limit** | `integer` | optional (default: `20`) | Number of items to load per page | +| **showCompleted** | `boolean` | optional (default: `false`) | Include completed activities | +| **unifiedTimeline** | `boolean` | optional (default: `true`) | Mix field changes and comments in one timeline (Airtable style) | +| **showCommentInput** | `boolean` | optional (default: `true`) | Show "Leave a comment" input at the bottom | +| **enableMentions** | `boolean` | optional (default: `true`) | Enable @mentions in comments | +| **enableReactions** | `boolean` | optional (default: `false`) | Enable emoji reactions on feed items | +| **enableThreading** | `boolean` | optional (default: `false`) | Enable threaded replies on comments | +| **showSubscriptionToggle** | `boolean` | optional (default: `true`) | Show bell icon for record-level notification subscription | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | + +### Nested Shape: `RecordChatterProps.aria` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ariaLabel** | `string \| Record` | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. | +| **ariaDescribedBy** | `string` | optional | ID of element providing additional description (WAI-ARIA aria-describedby) | +| **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | + --- @@ -546,6 +730,29 @@ const result = AIChatWindowProps.parse(data); | **showHeader** | `boolean` | optional | Render the detail body's own heading (renderer default: off). | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +### Nested Shape: `RecordDetailsProps.sections[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional | Stable section identifier for i18n lookup (snake_case) — resolves `objects.._sections..label`; a nameless section renders its authored label in every locale | +| **label** | `string \| Record` | optional | Section heading (omit for an untitled, borderless section) | +| **columns** | `integer` | optional | Field-grid columns for this section (1-4). Omitted → the renderer derives the width. | +| **fields** | `string[]` | ✅ | Field names rendered in this section, in order | +| **hideEmpty** | `boolean` | optional | Hide this section's empty fields (renderer default: on — and a section whose fields are ALL empty then renders nothing at all: no heading, no skeleton). Set `false` to render empty rows, keeping the section's label skeleton on an all-empty record (e.g. a brand-new one). | +| **collapsible** | `boolean` | optional | Render this section as a collapsible card — the heading becomes a chevron toggle, initially expanded (renderer default: off). | +| **showBorder** | `boolean` | optional | Draw this section's card chrome (renderer default: derived — on for a titled section, off for an untitled one). Set `false` for a borderless titled section, or `true` for a bordered untitled one. | +| **defaultCollapsed** | `boolean` | optional | Start a `collapsible: true` section collapsed (renderer default: expanded). Consulted only when `collapsible` is on — a non-collapsible section never reads its collapse state. | +| **icon** | `string` | optional | Heading icon, as a lucide icon name (kebab-case, e.g. `building-2`). A value that is not an ASCII identifier (emoji, CJK text) renders as literal text beside the heading instead. Shown where the section heading renders: a titled section, or any collapsible section. | +| **description** | `string` | optional | Sub-heading text rendered under the section heading (plain string — the renderer applies no translation to it, unlike `label`). Renders on a titled or collapsible section; a collapsible section hides it while collapsed. | + +### Nested Shape: `RecordDetailsProps.aria` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ariaLabel** | `string \| Record` | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. | +| **ariaDescribedBy** | `string` | optional | ID of element providing additional description (WAI-ARIA aria-describedby) | +| **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | + --- @@ -589,6 +796,23 @@ Type: `string` | **layout** | `Enum<'horizontal' \| 'vertical'>` | optional (default: `"horizontal"`) | Layout orientation for highlight fields | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +### Nested Shape: `RecordHighlightsProps.fields[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Field name on the record | +| **label** | `string` | optional | Display label (overrides schema label) | +| **type** | `string` | optional | Override cell renderer type (rare) | +| **readonly** | `boolean` | optional | Render this chip read-only — suppresses inline editing on the highlight card. Use for hook/automation-maintained columns that must not be hand-edited from the record header. | + +### Nested Shape: `RecordHighlightsProps.aria` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ariaLabel** | `string \| Record` | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. | +| **ariaDescribedBy** | `string` | optional | ID of element providing additional description (WAI-ARIA aria-describedby) | +| **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | + --- @@ -615,6 +839,22 @@ Type: `string` | **stages** | `{ value: string; label: string \| Record; terminal?: Enum<'won' \| 'lost'> }[]` | optional | Explicit stage definitions (if not using field metadata) | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +### Nested Shape: `RecordPathProps.stages[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **value** | `string` | ✅ | | +| **label** | `string \| Record` | ✅ | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **terminal** | `Enum<'won' \| 'lost'>` | optional | Mark this stage a terminus and its kind — overrides the renderer's value/label token heuristic | + +### Nested Shape: `RecordPathProps.aria` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ariaLabel** | `string \| Record` | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. | +| **ariaDescribedBy** | `string` | optional | ID of element providing additional description (WAI-ARIA aria-describedby) | +| **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | + --- @@ -644,6 +884,16 @@ Type: `string` | **entries** | `{ objectName: string; relationshipField: string; title?: string; limit?: integer; … }[]` | ✅ | Related collections to summarize — one compact card per entry (icon-less title, total-count badge, top-N preview rows). An empty rail renders nothing, so at least one entry is required. | | **hideEmpty** | `boolean` | optional | Fold entries whose related count is 0 into a single "+ N empty" expander chip (renderer default: on; set `false` to always render every card). | +### Nested Shape: `RecordReferenceRailProps.entries[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **objectName** | `string` | ✅ | Related object name whose records this card summarizes (e.g. "task", "opportunity_quote") | +| **relationshipField** | `string` | ✅ | Field on the related object that points back to this record (e.g. "account_id") | +| **title** | `string` | optional | Literal card title. Rendered as-is in EVERY locale (no inline locale map — the rail renders it as a raw React child); omit to use the related object's localized label. | +| **limit** | `integer` | optional | Preview rows per card, and the `$top` of the one query this entry issues (renderer default: 3). | +| **displayField** | `string` | optional | Field of the related record rendered in each preview row (renderer fallback when omitted: name / title / subject / label / … / id). | + --- @@ -666,6 +916,32 @@ Type: `string` | **add** | `{ picker: object; linkField?: string; label?: string \| Record }` | optional | Add-existing-via-picker config (generic m2m/junction assignment). | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +### Nested Shape: `RecordRelatedListProps.filter[number]` + +View filter rule + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name to filter on | +| **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>` | ✅ | Filter operator | +| **value** | `string \| number \| boolean \| null \| (string \| number)[]` | optional | Filter value. The accepted SHAPE depends on the operator: `in` / `not_in` take an array (any length, including []), `between` takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. | + +### Nested Shape: `RecordRelatedListProps.add` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **picker** | `{ object: string; valueField: string; labelField?: string; filter?: object[] }` | ✅ | Where the Add affordance sources records from. | +| **linkField** | `string` | optional | Field on `objectName` that stores the picked record id (junction case). Omit for a 1:m re-parent. | +| **label** | `string \| Record` | optional | Label for the Add button (default "Add"). | + +### Nested Shape: `RecordRelatedListProps.aria` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ariaLabel** | `string \| Record` | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. | +| **ariaDescribedBy** | `string` | optional | ID of element providing additional description (WAI-ARIA aria-describedby) | +| **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | + --- diff --git a/content/docs/references/ui/dashboard.mdx b/content/docs/references/ui/dashboard.mdx index ffb11d642c..816ba580d2 100644 --- a/content/docs/references/ui/dashboard.mdx +++ b/content/docs/references/ui/dashboard.mdx @@ -50,6 +50,72 @@ const result = DashboardSchema.parse(data); | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `Dashboard.header` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **showTitle** | `boolean` | optional (default: `true`) | Show dashboard title in header | +| **showDescription** | `boolean` | optional (default: `true`) | Show dashboard description in header | +| **actions** | `{ label: string \| Record; actionUrl: string; actionType?: Enum<'script' \| 'url' \| 'modal' \| 'flow' \| 'api' \| 'form'>; icon?: string }[]` | optional | Header action buttons | + +### Nested Shape: `Dashboard.widgets[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique widget identifier (snake_case) | +| **title** | `string \| Record` | optional | Widget title | +| **description** | `string \| Record` | optional | Widget description text below the header | +| **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| …>` | optional (default: `"metric"`) | Visualization type | +| **chartConfig** | `{ type: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| …>; title?: string \| Record; subtitle?: string \| Record; description?: string \| Record; … }` | optional | Chart visualization configuration | +| **colorVariant** | `Enum<'default' \| 'blue' \| 'teal' \| 'orange' \| 'purple' \| 'success' \| 'warning' \| 'danger'>` | optional | Widget color variant for theming | +| **requiresObject** | `string` | optional | Hide the widget unless the named object is registered | +| **requiresService** | `string` | optional | Hide the widget unless the named kernel service is registered | +| **actionUrl** | `never` | optional | [REMOVED] `dashboard.widgets[].actionUrl` was removed in @objectstack/spec 17.0.0 (#5010, ADR-0049 enforce-or-remove) — a dashboard widget has NO action button, and never had one. No renderer draws per-widget chrome for it: every action the dashboard dispatches comes from `header.actions[]`. The three keys `actionUrl` / `actionType` / `actionIcon` went together; delete all three. Put the affordance on the dashboard header instead — `header: { actions: [{ label, actionUrl, actionType, icon }] }` — which IS dispatched (`DashboardHeaderAction`, same vocabulary, and `icon` is the header spelling of `actionIcon`). For a per-ROW affordance, the widget to reach for is a `table`/`pivot` bound to a dataset: its rows are clickable and drill through the semantic layer. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **actionType** | `never` | optional | [REMOVED] `dashboard.widgets[].actionType` was removed in @objectstack/spec 17.0.0 (#5010, ADR-0049 enforce-or-remove) — a dashboard widget has NO action button, and never had one. No renderer draws per-widget chrome for it: every action the dashboard dispatches comes from `header.actions[]`. The three keys `actionUrl` / `actionType` / `actionIcon` went together; delete all three. Put the affordance on the dashboard header instead — `header: { actions: [{ label, actionUrl, actionType, icon }] }` — which IS dispatched (`DashboardHeaderAction`, same vocabulary, and `icon` is the header spelling of `actionIcon`). For a per-ROW affordance, the widget to reach for is a `table`/`pivot` bound to a dataset: its rows are clickable and drill through the semantic layer. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **actionIcon** | `never` | optional | [REMOVED] `dashboard.widgets[].actionIcon` was removed in @objectstack/spec 17.0.0 (#5010, ADR-0049 enforce-or-remove) — a dashboard widget has NO action button, and never had one. No renderer draws per-widget chrome for it: every action the dashboard dispatches comes from `header.actions[]`. The three keys `actionUrl` / `actionType` / `actionIcon` went together; delete all three. Put the affordance on the dashboard header instead — `header: { actions: [{ label, actionUrl, actionType, icon }] }` — which IS dispatched (`DashboardHeaderAction`, same vocabulary, and `icon` is the header spelling of `actionIcon`). For a per-ROW affordance, the widget to reach for is a `table`/`pivot` bound to a dataset: its rows are clickable and drill through the semantic layer. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **filter** | `any` | optional | Presentation-scope filter (runtimeFilter) | +| **compareTo** | `{ kind: Enum<'previousPeriod' \| 'previousYear'>; dimension?: string }` | optional | Period-over-period comparison window (`{ kind, dimension? }`) | +| **dataset** | `string` | ✅ | Dataset name to bind (ADR-0021) | +| **dimensions** | `string[]` | optional | Dimension names — X/group/split | +| **values** | `string[]` | ✅ | Measure names — Y (at least one) | +| **layout** | `{ x: number; y: number; w: number; h: number }` | optional | Grid layout position (auto-flowed when omitted) | +| **options** | `{ dateGranularity?: Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>; sortBy?: string; sortOrder?: Enum<'asc' \| 'desc'>; limit?: integer; … } & Record` | optional | Widget specific configuration | +| **filterBindings** | `Record` | optional | Per-widget dashboard-filter bindings: filter name → this widget's field, or false to opt out | +| **suppressWarnings** | `string[]` | optional | Build diagnostic rule ids suppressed on this widget | +| **responsive** | `never` | optional | [REMOVED] `dashboard.widgets[].responsive` was removed in @objectstack/spec 17.0.0 (#4876, ADR-0049 D2) — no renderer ever read it, so per-widget breakpoint overrides were never applied: the value parsed, validated, and then did nothing. The dashboard grid reflows by its own layout rules (`columns` + `gap` on the dashboard, the `layout` box on each widget). Delete the key. This message used to point at `page.components[].responsive` as the live home of the shared `ResponsiveConfig` shape; that key was measured equally unread and removed with the shape in #11027. For breakpoint behaviour that IS applied, use `responsiveStyles` on a page component (ADR-0065) — per-breakpoint CSS maps compiled to id-scoped CSS at render. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **aria** | `never` | optional | [REMOVED] `dashboard.widgets[].aria` was removed in @objectstack/spec 17.0.0 (#5010, ADR-0049 D2) — no renderer ever applied it, so ARIA attributes declared on a widget silently did not reach the DOM: the key promised accessibility compliance it did not deliver. This is the same removal the dashboard-level `aria` got in 17.0.0 (#3896). Delete the key. The dashboard renderer emits its own `aria-*` attributes for the widget grid; author a `title` (and `description`) on the widget instead — those ARE what the renderer labels the card with. The shared `AriaProps` shape is NOT gone: it stays live on `page.aria`, `page.components[].aria` and the list view `aria`. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | + +### Nested Shape: `Dashboard.dateRange` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | optional | Default date field name for time-based filtering | +| **defaultRange** | `Enum<'today' \| 'yesterday' \| 'this_week' \| 'last_week' \| 'this_month' \| 'last_month' \| …>` | optional (default: `"this_month"`) | Default date range preset | +| **allowCustomRange** | `boolean` | optional (default: `true`) | Allow users to pick a custom date range | + +### Nested Shape: `Dashboard.globalFilters[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional | Stable filter name (variable key); defaults to field | +| **field** | `string` | ✅ | Field name to filter on | +| **object** | `string` | optional | Object whose `fields..` translation-bundle entry resolves this filter's field label and option labels (#7804) | +| **label** | `string \| Record` | optional | Display label for the filter | +| **type** | `Enum<'text' \| 'select' \| 'date' \| 'number' \| 'lookup'>` | optional | Filter input type | +| **options** | `{ value: string \| number \| boolean; label: string \| Record }[]` | optional | Static filter options | +| **optionsFrom** | `{ object: string; valueField: string; labelField: string; filter?: any }` | optional | Dynamic filter options from object | +| **defaultValue** | `string \| number \| boolean` | optional | Default filter value | +| **scope** | `Enum<'dashboard' \| 'widget'>` | optional (default: `"dashboard"`) | Filter application scope | +| **targetWidgets** | `string[]` | optional | Widget IDs to apply this filter to | + +### Nested Shape: `Dashboard.protection` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | ✅ | Lock policy — none \| no-overlay \| no-delete \| full. | +| **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | +| **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | + --- @@ -65,6 +131,17 @@ Dashboard header configuration | **showDescription** | `boolean` | optional (default: `true`) | Show dashboard description in header | | **actions** | `{ label: string \| Record; actionUrl: string; actionType?: Enum<'script' \| 'url' \| 'modal' \| 'flow' \| 'api' \| 'form'>; icon?: string }[]` | optional | Header action buttons | +### Nested Shape: `DashboardHeader.actions[number]` + +Dashboard header action + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string \| Record` | ✅ | Action button label | +| **actionUrl** | `string` | ✅ | URL or target for the action | +| **actionType** | `Enum<'script' \| 'url' \| 'modal' \| 'flow' \| 'api' \| 'form'>` | optional | Type of action | +| **icon** | `string` | optional | Icon identifier for the action button | + --- @@ -136,6 +213,42 @@ Dashboard header action * `table` * `pivot` +### Nested Shape: `DashboardWidget.chartConfig` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| …>` | ✅ | | +| **title** | `string \| Record` | optional | Chart title | +| **subtitle** | `string \| Record` | optional | Chart subtitle | +| **description** | `string \| Record` | optional | Accessibility description — announced to screen readers as the chart’s label | +| **xAxis** | `{ field: string; title?: string \| Record; format?: string; min?: number; … }` | optional | X-Axis configuration | +| **yAxis** | `{ field: string; title?: string \| Record; format?: string; min?: number; … }[]` | optional | Y-Axis configuration (support dual axis) | +| **series** | `{ name: string; label?: string \| Record; type?: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| …>; color?: string; … }[]` | optional | Defined series configuration | +| **colors** | `string[] \| Record` | optional | Color palette (string[]) or value→color map (`{ value: color }`) | +| **height** | `number` | optional | Fixed plot height in pixels (overrides the container default) | +| **showLegend** | `boolean` | optional (default: `true`) | Display legend | +| **showDataLabels** | `boolean` | optional (default: `false`) | Display data labels | +| **annotations** | `{ type: Enum<'line' \| 'region'>; axis: Enum<'x' \| 'y'>; value: number \| string; endValue?: number \| string; … }[]` | optional | Reference lines/bands drawn over the plot: `{ type: "line" \| "region", axis: "x" \| "y", value, endValue?, color?, label?, style? }` | +| **interaction** | `{ tooltips: boolean; brush: boolean }` | optional | Interaction toggles: `{ tooltips?, brush? }` | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | + +### Nested Shape: `DashboardWidget.compareTo` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **kind** | `Enum<'previousPeriod' \| 'previousYear'>` | ✅ | Comparison window: previousPeriod (equal-length, immediately before) or previousYear (−1 calendar year) | +| **dimension** | `string` | optional | Time dimension to shift; omit when the selection has exactly one dated time dimension | + +### Nested Shape: `DashboardWidget.options` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **dateGranularity** | `Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>` | optional | Bucket selected date dimensions (day/week/month/quarter/year) | +| **sortBy** | `string` | optional | Dimension/measure name to order by | +| **sortOrder** | `Enum<'asc' \| 'desc'>` | optional | Sort direction for sortBy | +| **limit** | `integer` | optional | Max rows (applied after ordering) | +| **stageOrder** | `(string \| number \| boolean)[]` | optional | Explicit category order for funnel/pyramid stages (stored values) | + --- @@ -173,6 +286,22 @@ Widget configuration — declared query keys + open renderer extras | **scope** | `Enum<'dashboard' \| 'widget'>` | optional (default: `"dashboard"`) | Filter application scope | | **targetWidgets** | `string[]` | optional | Widget IDs to apply this filter to | +### Nested Shape: `GlobalFilter.options[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **value** | `string \| number \| boolean` | ✅ | Option value | +| **label** | `string \| Record` | ✅ | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | + +### Nested Shape: `GlobalFilter.optionsFrom` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **object** | `string` | ✅ | Source object name | +| **valueField** | `string` | ✅ | Field to use as option value | +| **labelField** | `string` | ✅ | Field to use as option label | +| **filter** | `any` | optional | Filter to apply to source object | + --- diff --git a/content/docs/references/ui/dataset.mdx b/content/docs/references/ui/dataset.mdx index 5c9f697fc2..96b4344cc9 100644 --- a/content/docs/references/ui/dataset.mdx +++ b/content/docs/references/ui/dataset.mdx @@ -65,6 +65,37 @@ const result = DatasetSchema.parse(data); | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `Dataset.dimensions[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Dimension name — referenced by presentations | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **field** | `string` | ✅ | Base field, or `relationship[.relationship].field` path | +| **type** | `Enum<'string' \| 'number' \| 'date' \| 'boolean' \| 'lookup'>` | optional | | +| **dateGranularity** | `Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>` | optional | | + +### Nested Shape: `Dataset.measures[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Measure name — e.g. "revenue"; defined once | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **aggregate** | `Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>` | optional | Aggregation (sum/avg/count/...); omit when `derived` is set | +| **field** | `string` | optional | Aggregated field; optional for count(*) | +| **filter** | `any` | optional | | +| **format** | `string` | optional | | +| **currency** | `string` | optional | Display currency code (ISO 4217) | +| **derived** | `{ op: Enum<'ratio' \| 'sum' \| 'difference' \| 'product'>; of: string[] }` | optional | | + +### Nested Shape: `Dataset.protection` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | ✅ | Lock policy — none \| no-overlay \| no-delete \| full. | +| **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | +| **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | + --- diff --git a/content/docs/references/ui/page.mdx b/content/docs/references/ui/page.mdx index 4baab160bb..96f51be169 100644 --- a/content/docs/references/ui/page.mdx +++ b/content/docs/references/ui/page.mdx @@ -36,6 +36,15 @@ const result = ElementDataSourceSchema.parse(data); | **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Sort order | | **limit** | `integer` | optional | Max records to display | +### Nested Shape: `ElementDataSource.sort[number]` + +Sort field and direction pair + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name to sort by | +| **order** | `Enum<'asc' \| 'desc'>` | ✅ | Sort direction | + --- @@ -62,6 +71,86 @@ Interface-level page configuration (Airtable parity) | **showRecordCount** | `boolean` | optional | Show record count at page bottom | | **allowPrinting** | `boolean` | optional | Allow users to print the page | +### Nested Shape: `InterfacePageConfig.columns[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name (snake_case) | +| **label** | `string \| Record` | optional | Display label override | +| **width** | `number` | optional | Column width in pixels | +| **align** | `Enum<'left' \| 'center' \| 'right'>` | optional | Text alignment | +| **hidden** | `boolean` | optional | Hide column by default | +| **sortable** | `boolean` | optional | Allow sorting by this column | +| **resizable** | `boolean` | optional | Allow resizing this column | +| **wrap** | `boolean` | optional | Allow text wrapping | +| **type** | `string` | optional | Renderer type override (e.g., "currency", "date") | +| **pinned** | `Enum<'left' \| 'right'>` | optional | Pin/freeze column to left or right side | +| **summary** | `Enum<'none' \| 'count' \| 'count_empty' \| 'count_filled' \| 'count_unique' \| …> \| { type: Enum<'none' \| 'count' \| 'count_empty' \| 'count_filled' \| 'count_unique' \| …>; field?: string }` | optional | Footer aggregation for this column — the function alone, or `{ type, field }` to aggregate another field | +| **prefix** | `{ field: string; type: Enum<'badge' \| 'text'> }` | optional | Field rendered inline before this cell value | +| **link** | `boolean` | optional | Functions as the primary navigation link (triggers View navigation) | +| **action** | `string` | optional | Registered Action ID to execute when clicked | + +### Nested Shape: `InterfacePageConfig.sort[number]` + +Sort field and direction pair + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name to sort by | +| **order** | `Enum<'asc' \| 'desc'>` | ✅ | Sort direction | + +### Nested Shape: `InterfacePageConfig.filterBy[number]` + +View filter rule + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name to filter on | +| **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>` | ✅ | Filter operator | +| **value** | `string \| number \| boolean \| null \| (string \| number)[]` | optional | Filter value. The accepted SHAPE depends on the operator: `in` / `not_in` take an array (any length, including []), `between` takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. | + +### Nested Shape: `InterfacePageConfig.appearance` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **showDescription** | `boolean` | optional (default: `true`) | Show the view description text | +| **allowedVisualizations** | `Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>[]` | optional | Whitelist of visualization types users can switch between (e.g. ["grid", "gallery", "kanban"]) | + +### Nested Shape: `InterfacePageConfig.userFilters` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **element** | `Enum<'dropdown' \| 'tabs' \| 'toggle'>` | optional (default: `"dropdown"`) | Filter control style: "dropdown" (per-field value selectors) or "tabs" (named presets). "toggle" is deprecated. | +| **fields** | `{ field: string; label?: string \| Record; type?: Enum<'select' \| 'multi-select' \| 'boolean' \| 'date-range' \| 'text'>; options?: object[]; … }[]` | optional | Fields exposed as quick filters (dropdown/toggle elements) | +| **tabs** | `{ name: string; label?: string \| Record; icon?: string; view?: string; … }[]` | optional | Named filter presets rendered as tabs (tabs element). Reuses ViewTabSchema | +| **showAllRecords** | `boolean` | optional | Show an "All records" tab before the presets (tabs element) | +| **allowAddTab** | `boolean` | optional | Let end users add their own tab after the presets (tabs element): the affordance asks for a name and snapshots the filters currently applied as a new tab. SESSION-SCOPED — an added tab lives only for the current mount, is never written back as metadata (ADR-0047), and carries a remove control the authored presets do not. Page lists only — object views use `listViews` for named presets | + +### Nested Shape: `InterfacePageConfig.userActions` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **sort** | `boolean` | optional (default: `true`) | Allow users to sort records | +| **search** | `boolean` | optional (default: `true`) | Allow users to search records | +| **filter** | `boolean` | optional (default: `true`) | Allow users to filter records | +| **refresh** | `boolean` | optional (default: `true`) | Allow users to reload the view data from the backend without a full page reload | +| **rowHeight** | `boolean` | optional (default: `true`) | Allow users to toggle row height/density | +| **group** | `boolean` | optional (default: `true`) | Allow users to change record grouping from the toolbar. Toggle only — the grouping itself is configured in the view-level `grouping` block. | +| **addRecordForm** | `boolean` | optional (default: `false`) | Add records through a form instead of inline | +| **editInline** | `boolean` | optional (default: `false`) | Allow users to edit records inline — click a cell to edit it with the field's type-aware widget (the same control the form uses). Off by default: the list is read-only unless the author opts in. | +| **hideFields** | `boolean` | optional (default: `false`) | Allow users to hide/show fields from the toolbar (the affordance behind the view-level `hiddenFields` list). Boolean toggle — distinct from the record-details component's `hideFields`, which is an array of field names to omit. Off by default: column hiding is opt-in. | +| **rowColor** | `boolean` | optional (default: `false`) | Allow users to configure row colouring from the toolbar. Boolean toggle — the colour rules themselves live in the view-level `rowColor` block. Off by default: row colouring is opt-in. | +| **buttons** | `string[]` | optional | Custom action button IDs to show in the toolbar | + +### Nested Shape: `InterfacePageConfig.addRecord` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | Show the add record entry point | +| **position** | `Enum<'top' \| 'bottom' \| 'both'>` | optional (default: `"bottom"`) | Position of the add record button | +| **mode** | `Enum<'inline' \| 'form' \| 'modal'>` | optional (default: `"inline"`) | How to add a new record | +| **formView** | `string` | optional | Named form view to use when mode is "form" or "modal" | + --- @@ -96,6 +185,50 @@ Interface-level page configuration (Airtable parity) | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `Page.variables[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Variable name. Exposed to expressions as `page.`. | +| **type** | `Enum<'string' \| 'number' \| 'boolean' \| 'object' \| 'array' \| 'record_id'>` | optional (default: `"string"`) | | +| **defaultValue** | `any` | optional | Initial value. Defaults to a type-appropriate empty value when omitted. | +| **source** | `string` | optional | Component id that writes this variable (e.g. an element:record_picker whose `id` matches). | + +### Nested Shape: `Page.regions[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Region name (e.g. "sidebar", "main", "header") | +| **width** | `Enum<'small' \| 'medium' \| 'large' \| 'full'>` | optional | | +| **components** | `{ type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| …> \| string; id?: string; label?: string \| Record; properties?: Record; … }[]` | ✅ | Components in this region | + +### Nested Shape: `Page.interfaceConfig` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **source** | `string` | optional | Source object name for the page | +| **columns** | `string[] \| { field: string; label?: string \| Record; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | optional | Columns shown by the page. Blank = all object fields. Defined directly on the page (no view inheritance). | +| **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Default sort order for the page, defined directly on the page. | +| **filterBy** | `{ field: string; operator?: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Always-on page filter (base filter). | +| **levels** | `integer` | optional | Number of hierarchy levels to display | +| **sourceView** | `string` | optional | @deprecated Legacy named-view inheritance. Define columns/sort/filterBy on the page instead. | +| **appearance** | `{ showDescription?: boolean; allowedVisualizations?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>[] }` | optional | Appearance and visualization configuration | +| **userFilters** | `{ element?: Enum<'dropdown' \| 'tabs' \| 'toggle'>; fields?: object[]; tabs?: object[]; showAllRecords?: boolean; … }` | optional | End-user quick-filter bar for this page (overrides the source view's userFilters) | +| **userActions** | `{ sort?: boolean; search?: boolean; filter?: boolean; refresh?: boolean; … }` | optional | User action toggles | +| **addRecord** | `{ enabled?: boolean; position?: Enum<'top' \| 'bottom' \| 'both'>; mode?: Enum<'inline' \| 'form' \| 'modal'>; formView?: string }` | optional | Add record entry point configuration | +| **buttons** | `string[]` | optional | Toolbar buttons — names of the source object's actions to surface in the page toolbar | +| **recordAction** | `Enum<'drawer' \| 'page' \| 'modal' \| 'none'>` | optional | How clicking a record opens its detail (drawer \| page \| modal \| none). Default: drawer | +| **showRecordCount** | `boolean` | optional | Show record count at page bottom | +| **allowPrinting** | `boolean` | optional | Allow users to print the page | + +### Nested Shape: `Page.aria` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ariaLabel** | `string \| Record` | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. | +| **ariaDescribedBy** | `string` | optional | ID of element providing additional description (WAI-ARIA aria-describedby) | +| **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | + --- @@ -119,6 +252,33 @@ Interface-level page configuration (Airtable parity) | **responsive** | `never` | optional | [REMOVED] `page.components[].responsive` was removed in @objectstack/spec 17 (#11027, ADR-0049 D2) — no renderer ever read it, so per-breakpoint layout overrides (columns/order/visibility) parsed, validated, and then did nothing. Delete the key. For breakpoint behaviour that IS applied, use the sibling `responsiveStyles` (ADR-0065) — per-breakpoint CSS maps compiled to id-scoped CSS at render, e.g. `responsiveStyles: { xsmall: { display: 'none' } }` to hide a component on the narrowest screens. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +### Nested Shape: `PageComponent.responsiveStyles` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **large** | `Record` | optional | Unconditional base (desktop-first) | +| **medium** | `Record` | optional | Applied at ≤ medium breakpoint | +| **small** | `Record` | optional | Applied at ≤ small breakpoint | +| **xsmall** | `Record` | optional | Applied at ≤ xsmall breakpoint | + +### Nested Shape: `PageComponent.dataSource` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **object** | `string` | ✅ | Object to query | +| **view** | `string` | optional | Named view to apply | +| **filter** | `any` | optional | Additional filter criteria | +| **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Sort order | +| **limit** | `integer` | optional | Max records to display | + +### Nested Shape: `PageComponent.aria` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ariaLabel** | `string \| Record` | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. | +| **ariaDescribedBy** | `string` | optional | ID of element providing additional description (WAI-ARIA aria-describedby) | +| **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | + --- @@ -173,6 +333,24 @@ Interface-level page configuration (Airtable parity) | **width** | `Enum<'small' \| 'medium' \| 'large' \| 'full'>` | optional | | | **components** | `{ type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| …> \| string; id?: string; label?: string \| Record; properties?: Record; … }[]` | ✅ | Components in this region | +### Nested Shape: `PageRegion.components[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| …> \| string` | ✅ | Component Type (Standard enum or custom string) | +| **id** | `string` | optional | Unique instance ID | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **properties** | `Record` | optional (default: `{}`) | Component props passed to the widget. See component.zod.ts for schemas. | +| **events** | `Record` | optional | Event handlers map | +| **style** | `Record` | optional | Inline styles or utility classes | +| **className** | `string` | optional | CSS class names | +| **responsiveStyles** | `{ large?: Record; medium?: Record; small?: Record; xsmall?: Record }` | optional | Per-breakpoint scoped style maps (ADR-0065) | +| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL) — component rendered only when TRUE. Contract-bound roots: `record`, `current_user` (ADR-0068 aliases `user` / `ctx.user` — one object, three spellings), and page state as `page.`. The shipping renderer additionally mounts `app`, `features`, `os.user` and binds `data` to the data-source ADAPTER here — renderer behaviour, NOT contract-guaranteed (ADR-0068 rules the user object only). ⚠️ `data` is surface-dependent: on a `page:tabs` item `visibleWhen` it is the record ROW instead. e.g. "page.selectedProjectId != ''" | +| **visibility** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | [DEPRECATED → `visibleWhen`] Visibility predicate (CEL). Normalized to `visibleWhen` at parse. | +| **dataSource** | `{ object: string; view?: string; filter?: any; sort?: object[]; … }` | optional | Per-element data binding for multi-object pages | +| **responsive** | `never` | optional | [REMOVED] `page.components[].responsive` was removed in @objectstack/spec 17 (#11027, ADR-0049 D2) — no renderer ever read it, so per-breakpoint layout overrides (columns/order/visibility) parsed, validated, and then did nothing. Delete the key. For breakpoint behaviour that IS applied, use the sibling `responsiveStyles` (ADR-0065) — per-breakpoint CSS maps compiled to id-scoped CSS at render, e.g. `responsiveStyles: { xsmall: { display: 'none' } }` to hide a component on the narrowest screens. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | + --- diff --git a/content/docs/references/ui/report.mdx b/content/docs/references/ui/report.mdx index 777954c19c..2bbd99b179 100644 --- a/content/docs/references/ui/report.mdx +++ b/content/docs/references/ui/report.mdx @@ -41,6 +41,32 @@ const result = JoinedReportBlockSchema.parse(data); | **runtimeFilter** | `any` | optional | Render-time scope filter (dataset-bound) | | **order** | `{ by: string; direction: Enum<'asc' \| 'desc'> }[]` | optional | Result ordering, most significant key first | +### Nested Shape: `JoinedReportBlock.chart` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| …>` | ✅ | | +| **title** | `string \| Record` | optional | Chart title | +| **subtitle** | `string \| Record` | optional | Chart subtitle | +| **description** | `string \| Record` | optional | Accessibility description — announced to screen readers as the chart’s label | +| **xAxis** | `string` | ✅ | Dataset dimension name for the X-axis (bound-dataset dimension, not a raw field) | +| **yAxis** | `string` | ✅ | Dataset measure name for the Y-axis (bound-dataset measure, not a raw field) | +| **series** | `{ name: string; label?: string \| Record; type?: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| …>; color?: string; … }[]` | optional | Defined series configuration | +| **colors** | `string[] \| Record` | optional | Color palette (string[]) or value→color map (`{ value: color }`) | +| **height** | `number` | optional | Fixed plot height in pixels (overrides the container default) | +| **showLegend** | `boolean` | optional (default: `true`) | Display legend | +| **showDataLabels** | `boolean` | optional (default: `false`) | Display data labels | +| **annotations** | `{ type: Enum<'line' \| 'region'>; axis: Enum<'x' \| 'y'>; value: number \| string; endValue?: number \| string; … }[]` | optional | Reference lines/bands drawn over the plot: `{ type: "line" \| "region", axis: "x" \| "y", value, endValue?, color?, label?, style? }` | +| **interaction** | `{ tooltips: boolean; brush: boolean }` | optional | Interaction toggles: `{ tooltips?, brush? }` | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | + +### Nested Shape: `JoinedReportBlock.order[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **by** | `string` | ✅ | Dimension or measure name to order by (must be selected by this report) | +| **direction** | `Enum<'asc' \| 'desc'>` | optional (default: `"asc"`) | Sort direction (default ascending) | + --- @@ -72,6 +98,56 @@ const result = JoinedReportBlockSchema.parse(data); | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `Report.order[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **by** | `string` | ✅ | Dimension or measure name to order by (must be selected by this report) | +| **direction** | `Enum<'asc' \| 'desc'>` | optional (default: `"asc"`) | Sort direction (default ascending) | + +### Nested Shape: `Report.chart` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| …>` | ✅ | | +| **title** | `string \| Record` | optional | Chart title | +| **subtitle** | `string \| Record` | optional | Chart subtitle | +| **description** | `string \| Record` | optional | Accessibility description — announced to screen readers as the chart’s label | +| **xAxis** | `string` | ✅ | Dataset dimension name for the X-axis (bound-dataset dimension, not a raw field) | +| **yAxis** | `string` | ✅ | Dataset measure name for the Y-axis (bound-dataset measure, not a raw field) | +| **series** | `{ name: string; label?: string \| Record; type?: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| …>; color?: string; … }[]` | optional | Defined series configuration | +| **colors** | `string[] \| Record` | optional | Color palette (string[]) or value→color map (`{ value: color }`) | +| **height** | `number` | optional | Fixed plot height in pixels (overrides the container default) | +| **showLegend** | `boolean` | optional (default: `true`) | Display legend | +| **showDataLabels** | `boolean` | optional (default: `false`) | Display data labels | +| **annotations** | `{ type: Enum<'line' \| 'region'>; axis: Enum<'x' \| 'y'>; value: number \| string; endValue?: number \| string; … }[]` | optional | Reference lines/bands drawn over the plot: `{ type: "line" \| "region", axis: "x" \| "y", value, endValue?, color?, label?, style? }` | +| **interaction** | `{ tooltips: boolean; brush: boolean }` | optional | Interaction toggles: `{ tooltips?, brush? }` | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | + +### Nested Shape: `Report.blocks[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Snake case identifier (lowercase with underscores only) | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **description** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **type** | `Enum<'tabular' \| 'summary' \| 'matrix'>` | optional (default: `"tabular"`) | | +| **chart** | `{ type: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| …>; title?: string \| Record; subtitle?: string \| Record; description?: string \| Record; … }` | optional | | +| **dataset** | `string` | optional | Dataset name to bind (ADR-0021) | +| **rows** | `string[]` | optional | Dimension names down (dataset-bound) | +| **columns** | `string[]` | optional | Dimension names across (matrix, dataset-bound) | +| **values** | `string[]` | optional | Measure names to show (dataset-bound) | +| **runtimeFilter** | `any` | optional | Render-time scope filter (dataset-bound) | +| **order** | `{ by: string; direction: Enum<'asc' \| 'desc'> }[]` | optional | Result ordering, most significant key first | + +### Nested Shape: `Report.protection` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | ✅ | Lock policy — none \| no-overlay \| no-delete \| full. | +| **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | +| **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | + --- @@ -119,6 +195,47 @@ const result = JoinedReportBlockSchema.parse(data); * `table` * `pivot` +### Nested Shape: `ReportChart.series[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Field name or series identifier | +| **label** | `string \| Record` | optional | Series display label | +| **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| …>` | optional | Override chart type for this series | +| **color** | `string` | optional | Series color (hex/rgb/token) | +| **stack** | `string` | optional | Stack identifier to group series | +| **yAxis** | `Enum<'left' \| 'right'>` | optional (default: `"left"`) | Bind to specific Y-Axis | +| **variant** | `Enum<'primary' \| 'comparison'>` | optional (default: `"primary"`) | Series visual role | +| **dashArray** | `string` | optional | SVG stroke-dasharray override | +| **opacity** | `number` | optional | Series opacity override | + +### Nested Shape: `ReportChart.annotations[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'line' \| 'region'>` | optional (default: `"line"`) | | +| **axis** | `Enum<'x' \| 'y'>` | optional (default: `"y"`) | | +| **value** | `number \| string` | ✅ | Start value | +| **endValue** | `number \| string` | optional | End value for regions | +| **color** | `string` | optional | | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **style** | `Enum<'solid' \| 'dashed' \| 'dotted'>` | optional (default: `"dashed"`) | | + +### Nested Shape: `ReportChart.interaction` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **tooltips** | `boolean` | optional (default: `true`) | Show the hover tooltip | +| **brush** | `boolean` | optional (default: `false`) | Show the range selector under the plot | + +### Nested Shape: `ReportChart.aria` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ariaLabel** | `string \| Record` | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. | +| **ariaDescribedBy** | `string` | optional | ID of element providing additional description (WAI-ARIA aria-describedby) | +| **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | + --- diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index d485c7bda3..582678d00b 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -217,6 +217,36 @@ Column footer summary configuration * `tags` * `vector` +### Nested Shape: `FormField.options[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string` | ✅ | Display label (human-readable, any case allowed) | +| **value** | `string` | ✅ | Stored value (lowercase machine identifier) | +| **color** | `string` | optional | Color code for badges/charts | +| **default** | `boolean` | optional | Is default option | +| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Per-option visibility predicate (CEL) — option is offered only when TRUE (else omitted). Env: the live `record` plus the host predicate scope, which binds `current_user` — wider than field-level visibleWhen, which has no `current_user`. e.g. P`record.country == 'cn'` or P`'admin' in current_user.positions` | + +### Nested Shape: `FormField.publicPicker` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **displayFields** | `string[]` | optional | Fields projected into each picker result (with `id`); the visitor's search matches `contains` on the first entry. At most 5 (the route projects no more); omitted → ['name']. | +| **maxResults** | `integer` | optional | Maximum rows a lookup returns (default 20, hard ceiling 50 — the route clamps; anonymous visitors cannot paginate past it). | +| **filter** | `{ field: string; operator?: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Static pre-filter rows ANDed ahead of the visitor's search (e.g. only active records are searchable). Same `{ field, operator, value }` dialect as list-view filters. | +| **object** | `string` | optional | Referenced-object override for the picker search; omitted → resolved from the field definition (`referenceTo`). | + +### Nested Shape: `FormField.keyField` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | optional (default: `"name"`) | Property name that holds the key inside each item (defaults to "name") | +| **label** | `string \| Record` | optional | Display label for the key column | +| **placeholder** | `string \| Record` | optional | Placeholder when entering a new key | +| **helpText** | `string \| Record` | optional | Help text under the key input | +| **regex** | `string` | optional | JS regex source string the key must match (no flags) | +| **immutable** | `boolean` | optional (default: `true`) | If true, the key is read-only after creation | + --- @@ -233,6 +263,16 @@ Public-lookup opt-in: enables GET /forms/:slug/lookup/:field for this field on a | **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Static pre-filter rows ANDed ahead of the visitor's search (e.g. only active records are searchable). Same `{ field, operator, value }` dialect as list-view filters. | | **object** | `string` | optional | Referenced-object override for the picker search; omitted → resolved from the field definition (`referenceTo`). | +### Nested Shape: `FormFieldPublicPicker.filter[number]` + +View filter rule + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name to filter on | +| **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>` | ✅ | Filter operator | +| **value** | `string \| number \| boolean \| null \| (string \| number)[]` | optional | Filter value. The accepted SHAPE depends on the operator: `in` / `not_in` take an array (any length, including []), `between` takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. | + --- @@ -253,6 +293,40 @@ Public-lookup opt-in: enables GET /forms/:slug/lookup/:field for this field on a | **pane** | `Enum<'primary' \| 'secondary'>` | optional | Split pane this section renders in (split forms only; a parse error elsewhere). Omitted → first section 'primary', others 'secondary'. | | **fields** | `(string \| { field: string; type?: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; options?: object[]; reference?: string; … })[]` | ✅ | | +### Nested Shape: `FormSection.fields[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name (snake_case) | +| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>` | optional | Field type (auto-infers widget if omitted) | +| **options** | `{ label: string; value: string; color?: string; default?: boolean; … }[]` | optional | Options for select/multiselect/radio/checkboxes fields | +| **reference** | `string` | optional | Target object name for lookup/master_detail fields | +| **publicPicker** | `{ displayFields?: string[]; maxResults?: integer; filter?: object[]; object?: string }` | optional | Opt this field into the anonymous public-form lookup picker (GET /forms/:slug/lookup/:field). Without it the route answers 403 LOOKUP_NOT_PUBLIC and the field is stripped from the rendered public form. | +| **maxLength** | `number` | optional | Maximum character length (for text/textarea/email/url/phone) | +| **minLength** | `number` | optional | Minimum character length | +| **min** | `number` | optional | Minimum value (for number/currency/percent/slider) | +| **max** | `number` | optional | Maximum value | +| **precision** | `number` | optional | Total digits (for number/currency) | +| **scale** | `number` | optional | Decimal places | +| **multiple** | `boolean` | optional | Allow multiple values (for select/lookup/file/image) | +| **label** | `string \| Record` | optional | Display label override | +| **placeholder** | `string \| Record` | optional | Placeholder text | +| **helpText** | `string \| Record` | optional | Help/hint text | +| **readonly** | `boolean` | optional | Read-only override | +| **immutable** | `boolean` | optional | Editable on create, locked once the record exists (e.g. machine names). | +| **required** | `boolean` | optional | Required override | +| **hidden** | `boolean` | optional | Hidden override | +| **colSpan** | `integer` | optional | [legacy — prefer `span`] Absolute column span (1-4). Fragile when the column count is derived per surface (mobile 1 / modal 2 / page 3-4): a fixed span only lines up at the width the author imagined. The renderer clamps it to the current column count. Prefer `span`. | +| **span** | `Enum<'auto' \| 'full'>` | optional (default: `"auto"`) | Relative field width. 'auto' (default — omit it): the renderer sizes the field from its widget type × the current column count (wide widgets like textarea/richtext/json/file/subform take the whole row). 'full': whole row at any column count. Prefer this over the absolute `colSpan`. | +| **widget** | `string` | optional | Custom widget/component name (overrides type-based inference) | +| **language** | `string` | optional | Code editor language (for type=code) | +| **keyField** | `{ field?: string; label?: string \| Record; placeholder?: string \| Record; helpText?: string \| Record; … }` | optional | Key column config for record-typed fields | +| **dependsOn** | `string` | optional | Parent field name for cascading | +| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL) — field shown only when TRUE. Root: `record` (+ `previous`, `parent`) in runtime forms, or `data` in metadata forms. No `current_user` at field level — it is unbound here and the predicate would fault open (per-option `visibleWhen` is the surface that binds it). Inside a repeater `data` is the ROW, but it is still spelled `data` — a bare identifier is unbound and faults open too. e.g. P`record.priority == 'urgent'` | +| **visibleOn** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | [DEPRECATED → `visibleWhen`] Visibility predicate (CEL). Normalized to `visibleWhen` at parse. | +| **disclosure** | `Enum<'inline' \| 'popover'>` | optional | Composite rendering: inline bordered box (default) or a summary line + gear popover (progressive disclosure). | +| **fields** | `{ field: string; type?: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; options?: object[]; reference?: string; … }[]` | optional | Sub-fields for composite/repeater/record types | + --- @@ -288,6 +362,69 @@ Public-lookup opt-in: enables GET /forms/:slug/lookup/:field for this field on a | **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.sections[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional | Stable section identifier for i18n lookup (snake_case) | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **description** | `string` | optional | Optional description rendered under the section header. | +| **collapsible** | `boolean` | optional (default: `false`) | | +| **collapsed** | `boolean` | optional (default: `false`) | | +| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL) — section shown only when TRUE. Root: `record` (+ `previous`, `parent`) in runtime forms, or `data` in metadata forms. No `current_user` at section level — it is unbound here and the predicate would fault open. | +| **visibleOn** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | [DEPRECATED → `visibleWhen`] Visibility predicate (CEL). Hides the whole section when false. Normalized to `visibleWhen` at parse. | +| **columns** | `Enum<'1' \| '2' \| '3' \| '4'> \| 1 \| 2 \| 3 \| 4` | optional (default: `1`) | | +| **pane** | `Enum<'primary' \| 'secondary'>` | optional | Split pane this section renders in (split forms only; a parse error elsewhere). Omitted → first section 'primary', others 'secondary'. | +| **fields** | `(string \| { field: string; type?: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; options?: object[]; reference?: string; … })[]` | ✅ | | + +### Nested Shape: `FormView.groups[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional | Stable section identifier for i18n lookup (snake_case) | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **description** | `string` | optional | Optional description rendered under the section header. | +| **collapsible** | `boolean` | optional (default: `false`) | | +| **collapsed** | `boolean` | optional (default: `false`) | | +| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL) — section shown only when TRUE. Root: `record` (+ `previous`, `parent`) in runtime forms, or `data` in metadata forms. No `current_user` at section level — it is unbound here and the predicate would fault open. | +| **visibleOn** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | [DEPRECATED → `visibleWhen`] Visibility predicate (CEL). Hides the whole section when false. Normalized to `visibleWhen` at parse. | +| **columns** | `Enum<'1' \| '2' \| '3' \| '4'> \| 1 \| 2 \| 3 \| 4` | optional (default: `1`) | | +| **pane** | `Enum<'primary' \| 'secondary'>` | optional | Split pane this section renders in (split forms only; a parse error elsewhere). Omitted → first section 'primary', others 'secondary'. | +| **fields** | `(string \| { field: string; type?: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; options?: object[]; reference?: string; … })[]` | ✅ | | + +### Nested Shape: `FormView.subforms[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **childObject** | `string` | ✅ | Child object whose records are entered inline | +| **relationshipField** | `string` | optional | FK on the child pointing back to the parent (auto-detected when omitted) | +| **columns** | `any[]` | optional | Editable grid columns (derived from the child object when omitted) | +| **amountField** | `string` | optional | Numeric child column summed for the running total | +| **totalField** | `string` | optional | Parent field to receive the rolled-up sum | +| **title** | `string` | optional | Section title | +| **addLabel** | `string` | optional | Add-row button label | +| **minRows** | `number` | optional | | +| **maxRows** | `number` | optional | | + +### Nested Shape: `FormView.sharing` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `false`) | Enable public sharing | +| **publicLink** | `string` | optional | Generated public share URL | +| **password** | `string` | optional | Password required to access shared link | +| **allowedDomains** | `string[]` | optional | Restrict access to specific email domains (e.g. ["example.com"]) | +| **expiresAt** | `string` | optional | Expiration date/time in ISO 8601 format | +| **allowAnonymous** | `boolean` | optional (default: `false`) | Allow access without authentication | + +### Nested Shape: `FormView.buttons` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **submit** | `{ show?: boolean; label?: string \| Record }` | optional | Submit button | +| **cancel** | `{ show?: boolean; label?: string \| Record }` | optional | Cancel button | +| **reset** | `{ show?: boolean; label?: string \| Record }` | optional | Reset button | + --- @@ -334,6 +471,14 @@ Gallery/card view configuration | **autoZoomToFilter** | `boolean` | optional | When true (default), filtering zooms the range to the filtered tasks | | **viewMode** | `Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>` | optional | Timeline granularity — one column per day/week/month/quarter/year (also the resource-view column granularity; renderer default 'day') | +### Nested Shape: `GanttConfig.quickFilters[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Record field / dot-path the dimension filters on | +| **label** | `string` | optional | Trigger label (falls back to the field label) | +| **options** | `(string \| { value: string \| number; label?: string })[]` | optional | Explicit option override for fixed enums | + --- @@ -360,6 +505,14 @@ Record grouping configuration | :--- | :--- | :--- | :--- | | **fields** | `{ field: string; order: Enum<'asc' \| 'desc'>; collapsed: boolean }[]` | ✅ | Fields to group by, in nesting order — the first entry is the outermost group and each later entry nests one level deeper (at least one field) | +### Nested Shape: `GroupingConfig.fields[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name to group by | +| **order** | `Enum<'asc' \| 'desc'>` | optional (default: `"asc"`) | Group sort order | +| **collapsed** | `boolean` | optional (default: `false`) | Collapse groups by default | + --- @@ -456,6 +609,22 @@ List chart view configuration | **link** | `boolean` | optional | Functions as the primary navigation link (triggers View navigation) | | **action** | `string` | optional | Registered Action ID to execute when clicked | +### Nested Shape: `ListColumn.summary` + +Column footer summary configuration + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'none' \| 'count' \| 'count_empty' \| 'count_filled' \| 'count_unique' \| …>` | ✅ | Aggregation function | +| **field** | `string` | optional | Field to aggregate (defaults to the column field) | + +### Nested Shape: `ListColumn.prefix` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field whose value renders before the cell value | +| **type** | `Enum<'badge' \| 'text'>` | optional (default: `"text"`) | How the prefix value is rendered | + --- @@ -534,136 +703,696 @@ 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. | - ---- - -## NavigationConfig - -### Properties +### Nested Shape: `ListView.columns[number]` | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **mode** | `Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>` | optional (default: `"page"`) | | -| **view** | `string` | optional | Name of the form view to use for details (e.g. "summary_view", "edit_form") | -| **preventNavigation** | `boolean` | optional (default: `false`) | Disable standard navigation entirely | -| **openNewTab** | `boolean` | optional (default: `false`) | Force open in new tab (applies to page mode) | -| **size** | `Enum<'auto' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'>` | optional (default: `"auto"`) | [#2578] Overlay size bucket for drawer/modal detail: 'auto' (default — renderer derives from field count + viewport; AI writes nothing) or a coarse override sm/md/lg/xl/full. Prefer this over the pixel `width`; page mode ignores it. | -| **width** | `string \| number` | optional | [DEPRECATED → size] Pixel/percent width of the drawer/modal (e.g. "600px"). A pixel width cannot be chosen at authoring time without knowing the client viewport — use the `size` bucket. | - - ---- - -## NavigationMode - -### Allowed Values - -* `page` -* `drawer` -* `modal` -* `split` -* `popover` -* `new_window` -* `none` - - ---- +| **field** | `string` | ✅ | Field name (snake_case) | +| **label** | `string \| Record` | optional | Display label override | +| **width** | `number` | optional | Column width in pixels | +| **align** | `Enum<'left' \| 'center' \| 'right'>` | optional | Text alignment | +| **hidden** | `boolean` | optional | Hide column by default | +| **sortable** | `boolean` | optional | Allow sorting by this column | +| **resizable** | `boolean` | optional | Allow resizing this column | +| **wrap** | `boolean` | optional | Allow text wrapping | +| **type** | `string` | optional | Renderer type override (e.g., "currency", "date") | +| **pinned** | `Enum<'left' \| 'right'>` | optional | Pin/freeze column to left or right side | +| **summary** | `Enum<'none' \| 'count' \| 'count_empty' \| 'count_filled' \| 'count_unique' \| …> \| { type: Enum<'none' \| 'count' \| 'count_empty' \| 'count_filled' \| 'count_unique' \| …>; field?: string }` | optional | Footer aggregation for this column — the function alone, or `{ type, field }` to aggregate another field | +| **prefix** | `{ field: string; type?: Enum<'badge' \| 'text'> }` | optional | Field rendered inline before this cell value | +| **link** | `boolean` | optional | Functions as the primary navigation link (triggers View navigation) | +| **action** | `string` | optional | Registered Action ID to execute when clicked | -## ObjectListView +### Nested Shape: `ListView.filter[number]` -### Properties +View filter rule | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **name** | `string` | optional | Internal view name (lowercase snake_case) | -| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | -| **type** | `Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>` | optional (default: `"grid"`) | | -| **data** | `{ provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source configuration (defaults to "object" provider) | -| **columns** | `string[] \| { field: string; label?: string \| Record; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | ✅ | Fields to display as columns | -| **filter** | `{ field: string; operator?: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Filter criteria (JSON Rules) | -| **sort** | `string \| { field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | | -| **searchableFields** | `string[]` | optional | Fields enabled for search | -| **filterableFields** | `string[]` | optional | Legacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters | -| **resizable** | `boolean` | optional | Enable column resizing | -| **compactToolbar** | `boolean` | optional | Collapse Group/Color/Density/Hide-fields into a single View settings popover | -| **selection** | `{ type?: Enum<'none' \| 'single' \| 'multiple'> }` | optional | Row selection configuration | -| **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; view?: string; preventNavigation?: boolean; openNewTab?: boolean; … }` | optional | Configuration for item click navigation (page, drawer, modal, etc.) | -| **pagination** | `{ pageSize?: integer; pageSizeOptions?: integer[] }` | optional | Pagination configuration | -| **kanban** | `{ groupByField: string; summarizeField?: string; columns: string[] }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout | -| **calendar** | `{ startDateField: string; endDateField?: string; titleField: string; colorField?: string }` | optional | Calendar configuration — applies when the view renders as a calendar layout | -| **gantt** | `{ startDateField: string; endDateField: string; titleField: string; progressField?: string; … } & Record` | optional | Gantt-timeline configuration — applies when the view renders as a gantt layout | -| **gallery** | `{ coverField?: string; coverFit?: Enum<'cover' \| 'contain'>; cardSize?: Enum<'small' \| 'medium' \| 'large'>; titleField?: string; … }` | optional | Gallery/card view configuration | -| **timeline** | `{ startDateField: string; endDateField?: string; titleField: string; groupByField?: string; … }` | optional | Timeline view configuration | -| **chart** | `{ chartType?: Enum<'bar' \| 'line' \| 'pie' \| 'area' \| 'scatter'>; dataset: string; dimensions?: string[]; values: string[] }` | optional | List chart view configuration | -| **map** | `{ latitudeField?: string; longitudeField?: string; locationField?: string; titleField?: string; … }` | optional | Map configuration — applies when the view renders as a map layout | -| **tree** | `{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer } & Record` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout | -| **description** | `string \| Record` | optional | View description for documentation/tooltips | -| **sharing** | `{ type?: Enum<'personal' \| 'collaborative'>; lockedBy?: string }` | optional | View sharing and access configuration | -| **rowHeight** | `Enum<'compact' \| 'short' \| 'medium' \| 'tall' \| 'extra_tall'>` | optional | Row height / density setting | -| **grouping** | `{ fields: object[] }` | optional | Group records by one or more fields | -| **rowColor** | `{ field: string; colors?: Record }` | optional | Color rows based on field value | -| **hiddenFields** | `string[]` | optional | Fields to hide in this specific view | -| **fieldOrder** | `string[]` | optional | Explicit field display order for this view | -| **rowActions** | `string[]` | optional | Actions available for individual row items | -| **bulkActions** | `string[]` | optional | Actions available when multiple rows are selected | -| **bulkActionDefs** | `{ name: string; label?: string; icon?: string; variant?: Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'outline'>; … }[]` | optional | Rich bulk action definitions (schema-driven, executed via BulkActionDialog). Use a def for a mass data-plane mutation ('update' with a `patch` / 'delete') that no action expresses, or for an `operation: 'custom'` + `execution: 'aggregate'` entry (objectui#3139) that dispatches the action it NAMES once for the whole selection — the renderer injects `params._selectedIds: string[]` (read that on the server, not `recordId`) so a single call can produce one aggregate artifact (zip of QR codes, merged PDF, batch print). Aggregate results are all-or-nothing: a handler that cannot cover the whole selection must reject, and per-row retry is replaced by re-running the action. `batchSize` does not apply (the call is never chunked); set `maxRecords` on defs whose server work is expensive. For the PER-RECORD dispatch use `bulkActions: ['']` instead — the bare-string form, promoted with the action's own label, params and `visible`; a 'custom' def without `execution: 'aggregate'` has no dispatcher and is refused at parse time (#4457). Toolbar url/api actions can also interpolate the current selection via `${ctx.selection.ids}` / `${ctx.selection.count}`. | -| **conditionalFormatting** | `{ condition: string \| object; style: Record }[]` | optional | Conditional formatting rules for list rows | -| **inlineEdit** | `boolean` | optional | Allow inline editing of records directly in the list view | -| **exportOptions** | `Enum<'csv' \| 'xlsx' \| 'json'>[] \| { formats?: Enum<'csv' \| 'xlsx' \| 'json'>[]; maxRecords?: integer; includeHeaders?: boolean; fileNamePrefix?: string; … }` | optional | Export configuration for the list toolbar export menu: `{ formats?, maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }`. A bare format array is the legacy spelling and lifts to `{ formats: [...] }` at parse. | -| **userActions** | `{ sort?: boolean; search?: boolean; filter?: boolean; refresh?: boolean; … }` | optional | User action toggles for the view toolbar | -| **appearance** | `{ showDescription?: boolean; allowedVisualizations?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>[] }` | optional | Appearance and visualization configuration | -| **tabs** | `{ name: string; label?: string \| Record; icon?: string; view?: string; … }[]` | optional | Tab definitions for multi-tab view interface | -| **addRecord** | `{ enabled?: boolean; position?: Enum<'top' \| 'bottom' \| 'both'>; mode?: Enum<'inline' \| 'form' \| 'modal'>; formView?: string }` | optional | Add record entry point configuration | -| **showRecordCount** | `boolean` | optional | Show record count at the bottom of the list | -| **allowPrinting** | `boolean` | optional | Allow users to print the view | -| **emptyState** | `{ title?: string \| Record; message?: string \| Record; icon?: string }` | optional | Empty state configuration when no records found | -| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes for the list view | -| **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | -| **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | -| **striped** | `never` | optional | [REMOVED] `view.striped` 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, so authoring it was a parse-clean no-op. There is no authorable striped-rows switch; delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | -| **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. | -| **userFilters** | `{ element?: Enum<'dropdown' \| 'toggle'>; fields?: object[] }` | optional | | - - ---- - -## ObjectUserFilters +| **field** | `string` | ✅ | Field name to filter on | +| **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>` | optional | Filter operator | +| **value** | `string \| number \| boolean \| null \| (string \| number)[]` | optional | Filter value. The accepted SHAPE depends on the operator: `in` / `not_in` take an array (any length, including []), `between` takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. | -### Properties +### Nested Shape: `ListView.userFilters` | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **element** | `Enum<'dropdown' \| 'toggle'>` | optional (default: `"dropdown"`) | Filter control style on object views: "dropdown" (per-field value chips). "toggle" is deprecated. "tabs" is page-only — use `listViews` for named presets. | +| **element** | `Enum<'dropdown' \| 'tabs' \| 'toggle'>` | optional (default: `"dropdown"`) | Filter control style: "dropdown" (per-field value selectors) or "tabs" (named presets). "toggle" is deprecated. | | **fields** | `{ field: string; label?: string \| Record; type?: Enum<'select' \| 'multi-select' \| 'boolean' \| 'date-range' \| 'text'>; options?: object[]; … }[]` | optional | Fields exposed as quick filters (dropdown/toggle elements) | +| **tabs** | `{ name: string; label?: string \| Record; icon?: string; view?: string; … }[]` | optional | Named filter presets rendered as tabs (tabs element). Reuses ViewTabSchema | +| **showAllRecords** | `boolean` | optional | Show an "All records" tab before the presets (tabs element) | +| **allowAddTab** | `boolean` | optional | Let end users add their own tab after the presets (tabs element): the affordance asks for a name and snapshots the filters currently applied as a new tab. SESSION-SCOPED — an added tab lives only for the current mount, is never written back as metadata (ADR-0047), and carries a remove control the authored presets do not. Page lists only — object views use `listViews` for named presets | +### Nested Shape: `ListView.selection` ---- +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'none' \| 'single' \| 'multiple'>` | optional (default: `"none"`) | Selection mode | -## PaginationConfig +### Nested Shape: `ListView.navigation` -### Properties +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **mode** | `Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>` | optional (default: `"page"`) | | +| **view** | `string` | optional | Name of the form view to use for details (e.g. "summary_view", "edit_form") | +| **preventNavigation** | `boolean` | optional (default: `false`) | Disable standard navigation entirely | +| **openNewTab** | `boolean` | optional (default: `false`) | Force open in new tab (applies to page mode) | +| **size** | `Enum<'auto' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'>` | optional (default: `"auto"`) | [#2578] Overlay size bucket for drawer/modal detail: 'auto' (default — renderer derives from field count + viewport; AI writes nothing) or a coarse override sm/md/lg/xl/full. Prefer this over the pixel `width`; page mode ignores it. | +| **width** | `string \| number` | optional | [DEPRECATED → size] Pixel/percent width of the drawer/modal (e.g. "600px"). A pixel width cannot be chosen at authoring time without knowing the client viewport — use the `size` bucket. | + +### Nested Shape: `ListView.pagination` | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **pageSize** | `integer` | optional (default: `25`) | Number of records per page | | **pageSizeOptions** | `integer[]` | optional | Available page size options | - ---- - -## RowColorConfig - -Row color configuration based on field values - -### Properties +### Nested Shape: `ListView.kanban` | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **field** | `string` | ✅ | Field to derive color from (typically a select/status field) | -| **colors** | `Record` | optional | Map of field value to color (hex/token) | +| **groupByField** | `string` | ✅ | Field to group columns by (usually status/select) | +| **summarizeField** | `string` | optional | Field to sum at top of column (e.g. amount) | +| **columns** | `string[]` | ✅ | Fields to show on cards | +### Nested Shape: `ListView.calendar` ---- +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **startDateField** | `string` | ✅ | Field providing the event start date/time | +| **endDateField** | `string` | optional | Field providing the event end date/time (defaults to a single-day event) | +| **titleField** | `string` | ✅ | Field displayed as the event title | +| **colorField** | `string` | optional | Field whose value determines the event color | + +### Nested Shape: `ListView.gantt` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **startDateField** | `string` | ✅ | Field providing the task start date | +| **endDateField** | `string` | ✅ | Field providing the task end date | +| **titleField** | `string` | ✅ | Field displayed as the task title | +| **progressField** | `string` | optional | Field providing the task completion percentage | +| **dependenciesField** | `string` | optional | Field listing the task's predecessor (dependency) record ids | +| **colorField** | `string` | optional | Field that drives the bar color | +| **parentField** | `string` | optional | Field holding the parent task id (builds the summary → step tree) | +| **typeField** | `string` | optional | Field whose value maps to task/summary/milestone | +| **baselineStartField** | `string` | optional | Baseline (planned) start field | +| **baselineEndField** | `string` | optional | Baseline (planned) end field | +| **groupByField** | `string` | optional | Field to group leaf tasks by (synthesized summary rows) | +| **resourceView** | `boolean` | optional | Render a per-resource workload histogram instead of the timeline | +| **assigneeField** | `string` | optional | Resource field to bucket load by (resource view) | +| **effortField** | `string` | optional | Per-task load units (resource view; default 1) | +| **capacity** | `number` | optional | Per-resource capacity ceiling; loads above this flag overload | +| **tooltipFields** | `(string \| { field: string; label?: string })[]` | optional | Fields to surface in the hover tooltip, in display order | +| **quickFilters** | `{ field: string; label?: string; options?: (string \| object)[] }[]` | optional | Multi-select filter dropdowns rendered above the chart | +| **autoZoomToFilter** | `boolean` | optional | When true (default), filtering zooms the range to the filtered tasks | +| **viewMode** | `Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>` | optional | Timeline granularity — one column per day/week/month/quarter/year (also the resource-view column granularity; renderer default 'day') | + +### Nested Shape: `ListView.gallery` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **coverField** | `string` | optional | Attachment/image field to display as card cover | +| **coverFit** | `Enum<'cover' \| 'contain'>` | optional (default: `"cover"`) | Image fit mode for card cover | +| **cardSize** | `Enum<'small' \| 'medium' \| 'large'>` | optional (default: `"medium"`) | Card size in gallery view | +| **titleField** | `string` | optional | Field to display as card title | +| **visibleFields** | `string[]` | optional | Fields to display on card body | + +### Nested Shape: `ListView.timeline` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **startDateField** | `string` | ✅ | Field for timeline item start date | +| **endDateField** | `string` | optional | Field for timeline item end date | +| **titleField** | `string` | ✅ | Field to display as timeline item title | +| **groupByField** | `string` | optional | Field to group timeline rows | +| **colorField** | `string` | optional | Field to determine item color | +| **scale** | `Enum<'hour' \| 'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>` | optional (default: `"week"`) | Default timeline scale | + +### Nested Shape: `ListView.chart` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **chartType** | `Enum<'bar' \| 'line' \| 'pie' \| 'area' \| 'scatter'>` | optional (default: `"bar"`) | Chart visualisation type | +| **dataset** | `string` | ✅ | Dataset name to bind (ADR-0021) | +| **dimensions** | `string[]` | optional | Dimension names — X/group/split | +| **values** | `string[]` | ✅ | Measure names — Y (at least one) | + +### Nested Shape: `ListView.map` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **latitudeField** | `string` | optional | Field providing the marker latitude (used with longitudeField) | +| **longitudeField** | `string` | optional | Field providing the marker longitude (used with latitudeField) | +| **locationField** | `string` | optional | Field providing a combined location — a "lat,lng" string or a `{ lat, lng }` object — as the alternative to the latitudeField/longitudeField pair | +| **titleField** | `string` | optional | Field displayed as the marker title (popup heading, mobile record card, and what the map search box matches on) | +| **descriptionField** | `string` | optional | Field displayed as the marker description | +| **zoom** | `number` | optional | Initial zoom level (1-20). Omit to let the renderer fit the camera to the queried records | +| **center** | `any[]` | optional | Initial camera center as [latitude, longitude]. Omit to let the renderer fit the camera to the queried records | + +### Nested Shape: `ListView.tree` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **parentField** | `string` | optional | Single-parent pointer field (auto-detected from the object schema when omitted) | +| **labelField** | `string` | optional | Field rendered indented in the first column (defaults to "name") | +| **fields** | `string[]` | optional | Additional fields rendered as flat columns alongside the label | +| **defaultExpandedDepth** | `integer` | optional | Initial expansion depth (0 = roots only; omit = expand all) | + +### Nested Shape: `ListView.sharing` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'personal' \| 'collaborative'>` | optional (default: `"collaborative"`) | View ownership type | +| **lockedBy** | `string` | optional | User who locked the view configuration | + +### Nested Shape: `ListView.grouping` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **fields** | `{ field: string; order?: Enum<'asc' \| 'desc'>; collapsed?: boolean }[]` | ✅ | Fields to group by, in nesting order — the first entry is the outermost group and each later entry nests one level deeper (at least one field) | + +### Nested Shape: `ListView.rowColor` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field to derive color from (typically a select/status field) | +| **colors** | `Record` | optional | Map of field value to color (hex/token) | + +### Nested Shape: `ListView.bulkActionDefs[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Stable identifier — the audit-log action key, and (for an aggregate def) the name of the object action to dispatch. | +| **label** | `string` | optional | Button + dialog-header text. Plain string: an authored def is not i18n-resolved (declare a real action and name it in `bulkActions` to get localization). | +| **icon** | `string` | optional | Lucide icon name (e.g. "user-check", "trash-2"). | +| **variant** | `Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'outline'>` | optional | Visual treatment of the button. | +| **operation** | `Enum<'update' \| 'delete' \| 'custom'>` | ✅ | What the executor does: 'update'/'delete' are data-plane mass mutations; 'custom' dispatches an object action (see `execution`). | +| **execution** | `Enum<'perRecord' \| 'aggregate'>` | optional | For `operation: 'custom'` — 'aggregate' dispatches the named action ONCE for the whole selection, carrying every id in `params._selectedIds` (objectui#3139). Required on a custom def: the per-record form is declared as `bulkActions: ['']` instead. | +| **patch** | `Record` | optional | For `operation: 'update'` — static field values applied to every selected record, merged UNDER the user-supplied params so a fixed value can be declared without exposing it in the dialog. | +| **params** | `({ name: string; label?: string; help?: string; type: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; … } & Record)[]` | optional | Inputs collected once before the run. Omit to skip the params step and go straight to confirm. | +| **confirmText** | `string` | optional | Confirmation text shown above the affected-record summary. | +| **confirmLabel** | `string` | optional | Custom Confirm button label (default: "Run"). | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Eligibility predicate (CEL) — a string or a `{dialect, source}` envelope, i.e. `action.visible` without its boolean-literal arm (#5970): a per-record predicate has nothing to say as a constant. Evaluated once PER SELECTED RECORD with that record bound: the button is offered when at least one passes, the run covers only those, and the rest are reported as skipped. A record-free predicate (`features.x`, `current_user.y`) therefore behaves as a plain button-level gate. Fail-closed — a predicate that faults excludes the record. | +| **requiredPermissions** | `string[]` | optional | [ADR-0066 D4] Capability gate on the button, `action.requiredPermissions` semantics verbatim: absent or empty always passes, several are AND-ed, and a client that cannot resolve the caller's capabilities fails OPEN (the server stays the authority). This key exists for INLINE defs — notably the `update`/`delete` data-plane forms, which dispatch no action and so have nothing to inherit a gate from; a def promoted from `bulkActions: ['']` (or an aggregate def naming a declared action) inherits the action's own declaration instead. On a data-plane def the gate governs visibility only — the write itself is still authorized by the data API's object permissions and server hooks. | +| **maxRecords** | `integer` | optional | Selection size above which the run is blocked. Set it on defs whose server work is expensive — an aggregate def carries every selected id in one request. | +| **batchSize** | `integer` | optional | Records per executor batch (default 200). Data-plane operations only — an aggregate run is a single call by definition. | + +### Nested Shape: `ListView.conditionalFormatting[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL) to evaluate. | +| **style** | `Record` | ✅ | CSS styles to apply when condition is true | + +### Nested Shape: `ListView.exportOptions` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **formats** | `Enum<'csv' \| 'xlsx' \| 'json'>[]` | optional | Formats offered in the export menu (default: ['csv', 'json']). XLSX is delivered by the server stream only. | +| **maxRecords** | `integer` | optional | Maximum number of records to export; 0 or absent = unlimited | +| **includeHeaders** | `boolean` | optional | Include column headers in the exported file (default true) | +| **fileNamePrefix** | `string` | optional | Download file name prefix — replaces the object label and suppresses the view label in the generated file name | +| **streaming** | `boolean` | optional | Set false to force the client-side export path (csv/json only) instead of the server stream | + +### Nested Shape: `ListView.userActions` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **sort** | `boolean` | optional (default: `true`) | Allow users to sort records | +| **search** | `boolean` | optional (default: `true`) | Allow users to search records | +| **filter** | `boolean` | optional (default: `true`) | Allow users to filter records | +| **refresh** | `boolean` | optional (default: `true`) | Allow users to reload the view data from the backend without a full page reload | +| **rowHeight** | `boolean` | optional (default: `true`) | Allow users to toggle row height/density | +| **group** | `boolean` | optional (default: `true`) | Allow users to change record grouping from the toolbar. Toggle only — the grouping itself is configured in the view-level `grouping` block. | +| **addRecordForm** | `boolean` | optional (default: `false`) | Add records through a form instead of inline | +| **editInline** | `boolean` | optional (default: `false`) | Allow users to edit records inline — click a cell to edit it with the field's type-aware widget (the same control the form uses). Off by default: the list is read-only unless the author opts in. | +| **hideFields** | `boolean` | optional (default: `false`) | Allow users to hide/show fields from the toolbar (the affordance behind the view-level `hiddenFields` list). Boolean toggle — distinct from the record-details component's `hideFields`, which is an array of field names to omit. Off by default: column hiding is opt-in. | +| **rowColor** | `boolean` | optional (default: `false`) | Allow users to configure row colouring from the toolbar. Boolean toggle — the colour rules themselves live in the view-level `rowColor` block. Off by default: row colouring is opt-in. | +| **buttons** | `string[]` | optional | Custom action button IDs to show in the toolbar | + +### Nested Shape: `ListView.appearance` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **showDescription** | `boolean` | optional (default: `true`) | Show the view description text | +| **allowedVisualizations** | `Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>[]` | optional | Whitelist of visualization types users can switch between (e.g. ["grid", "gallery", "kanban"]) | + +### Nested Shape: `ListView.tabs[number]` + +Tab configuration for multi-tab view interface + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Tab identifier (snake_case) | +| **label** | `string \| Record` | optional | Display label | +| **icon** | `string` | optional | Tab icon name | +| **view** | `string` | optional | Referenced list view name from listViews | +| **filter** | `{ field: string; operator?: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Tab-specific filter criteria | +| **order** | `integer` | optional | Tab display order | +| **pinned** | `boolean` | optional (default: `false`) | Pin tab (cannot be removed by users) | +| **isDefault** | `boolean` | optional (default: `false`) | Set as the default active tab | +| **visible** | `boolean` | optional (default: `true`) | Tab visibility | + +### Nested Shape: `ListView.addRecord` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | Show the add record entry point | +| **position** | `Enum<'top' \| 'bottom' \| 'both'>` | optional (default: `"bottom"`) | Position of the add record button | +| **mode** | `Enum<'inline' \| 'form' \| 'modal'>` | optional (default: `"inline"`) | How to add a new record | +| **formView** | `string` | optional | Named form view to use when mode is "form" or "modal" | + +### Nested Shape: `ListView.emptyState` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **title** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **message** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **icon** | `string` | optional | | + +### Nested Shape: `ListView.aria` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ariaLabel** | `string \| Record` | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. | +| **ariaDescribedBy** | `string` | optional | ID of element providing additional description (WAI-ARIA aria-describedby) | +| **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | + + +--- + +## NavigationConfig + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **mode** | `Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>` | optional (default: `"page"`) | | +| **view** | `string` | optional | Name of the form view to use for details (e.g. "summary_view", "edit_form") | +| **preventNavigation** | `boolean` | optional (default: `false`) | Disable standard navigation entirely | +| **openNewTab** | `boolean` | optional (default: `false`) | Force open in new tab (applies to page mode) | +| **size** | `Enum<'auto' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'>` | optional (default: `"auto"`) | [#2578] Overlay size bucket for drawer/modal detail: 'auto' (default — renderer derives from field count + viewport; AI writes nothing) or a coarse override sm/md/lg/xl/full. Prefer this over the pixel `width`; page mode ignores it. | +| **width** | `string \| number` | optional | [DEPRECATED → size] Pixel/percent width of the drawer/modal (e.g. "600px"). A pixel width cannot be chosen at authoring time without knowing the client viewport — use the `size` bucket. | + + +--- + +## NavigationMode + +### Allowed Values + +* `page` +* `drawer` +* `modal` +* `split` +* `popover` +* `new_window` +* `none` + + +--- + +## ObjectListView + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional | Internal view name (lowercase snake_case) | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **type** | `Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>` | optional (default: `"grid"`) | | +| **data** | `{ provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source configuration (defaults to "object" provider) | +| **columns** | `string[] \| { field: string; label?: string \| Record; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | ✅ | Fields to display as columns | +| **filter** | `{ field: string; operator?: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Filter criteria (JSON Rules) | +| **sort** | `string \| { field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | | +| **searchableFields** | `string[]` | optional | Fields enabled for search | +| **filterableFields** | `string[]` | optional | Legacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters | +| **resizable** | `boolean` | optional | Enable column resizing | +| **compactToolbar** | `boolean` | optional | Collapse Group/Color/Density/Hide-fields into a single View settings popover | +| **selection** | `{ type?: Enum<'none' \| 'single' \| 'multiple'> }` | optional | Row selection configuration | +| **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; view?: string; preventNavigation?: boolean; openNewTab?: boolean; … }` | optional | Configuration for item click navigation (page, drawer, modal, etc.) | +| **pagination** | `{ pageSize?: integer; pageSizeOptions?: integer[] }` | optional | Pagination configuration | +| **kanban** | `{ groupByField: string; summarizeField?: string; columns: string[] }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout | +| **calendar** | `{ startDateField: string; endDateField?: string; titleField: string; colorField?: string }` | optional | Calendar configuration — applies when the view renders as a calendar layout | +| **gantt** | `{ startDateField: string; endDateField: string; titleField: string; progressField?: string; … } & Record` | optional | Gantt-timeline configuration — applies when the view renders as a gantt layout | +| **gallery** | `{ coverField?: string; coverFit?: Enum<'cover' \| 'contain'>; cardSize?: Enum<'small' \| 'medium' \| 'large'>; titleField?: string; … }` | optional | Gallery/card view configuration | +| **timeline** | `{ startDateField: string; endDateField?: string; titleField: string; groupByField?: string; … }` | optional | Timeline view configuration | +| **chart** | `{ chartType?: Enum<'bar' \| 'line' \| 'pie' \| 'area' \| 'scatter'>; dataset: string; dimensions?: string[]; values: string[] }` | optional | List chart view configuration | +| **map** | `{ latitudeField?: string; longitudeField?: string; locationField?: string; titleField?: string; … }` | optional | Map configuration — applies when the view renders as a map layout | +| **tree** | `{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer } & Record` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout | +| **description** | `string \| Record` | optional | View description for documentation/tooltips | +| **sharing** | `{ type?: Enum<'personal' \| 'collaborative'>; lockedBy?: string }` | optional | View sharing and access configuration | +| **rowHeight** | `Enum<'compact' \| 'short' \| 'medium' \| 'tall' \| 'extra_tall'>` | optional | Row height / density setting | +| **grouping** | `{ fields: object[] }` | optional | Group records by one or more fields | +| **rowColor** | `{ field: string; colors?: Record }` | optional | Color rows based on field value | +| **hiddenFields** | `string[]` | optional | Fields to hide in this specific view | +| **fieldOrder** | `string[]` | optional | Explicit field display order for this view | +| **rowActions** | `string[]` | optional | Actions available for individual row items | +| **bulkActions** | `string[]` | optional | Actions available when multiple rows are selected | +| **bulkActionDefs** | `{ name: string; label?: string; icon?: string; variant?: Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'outline'>; … }[]` | optional | Rich bulk action definitions (schema-driven, executed via BulkActionDialog). Use a def for a mass data-plane mutation ('update' with a `patch` / 'delete') that no action expresses, or for an `operation: 'custom'` + `execution: 'aggregate'` entry (objectui#3139) that dispatches the action it NAMES once for the whole selection — the renderer injects `params._selectedIds: string[]` (read that on the server, not `recordId`) so a single call can produce one aggregate artifact (zip of QR codes, merged PDF, batch print). Aggregate results are all-or-nothing: a handler that cannot cover the whole selection must reject, and per-row retry is replaced by re-running the action. `batchSize` does not apply (the call is never chunked); set `maxRecords` on defs whose server work is expensive. For the PER-RECORD dispatch use `bulkActions: ['']` instead — the bare-string form, promoted with the action's own label, params and `visible`; a 'custom' def without `execution: 'aggregate'` has no dispatcher and is refused at parse time (#4457). Toolbar url/api actions can also interpolate the current selection via `${ctx.selection.ids}` / `${ctx.selection.count}`. | +| **conditionalFormatting** | `{ condition: string \| object; style: Record }[]` | optional | Conditional formatting rules for list rows | +| **inlineEdit** | `boolean` | optional | Allow inline editing of records directly in the list view | +| **exportOptions** | `Enum<'csv' \| 'xlsx' \| 'json'>[] \| { formats?: Enum<'csv' \| 'xlsx' \| 'json'>[]; maxRecords?: integer; includeHeaders?: boolean; fileNamePrefix?: string; … }` | optional | Export configuration for the list toolbar export menu: `{ formats?, maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }`. A bare format array is the legacy spelling and lifts to `{ formats: [...] }` at parse. | +| **userActions** | `{ sort?: boolean; search?: boolean; filter?: boolean; refresh?: boolean; … }` | optional | User action toggles for the view toolbar | +| **appearance** | `{ showDescription?: boolean; allowedVisualizations?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>[] }` | optional | Appearance and visualization configuration | +| **tabs** | `{ name: string; label?: string \| Record; icon?: string; view?: string; … }[]` | optional | Tab definitions for multi-tab view interface | +| **addRecord** | `{ enabled?: boolean; position?: Enum<'top' \| 'bottom' \| 'both'>; mode?: Enum<'inline' \| 'form' \| 'modal'>; formView?: string }` | optional | Add record entry point configuration | +| **showRecordCount** | `boolean` | optional | Show record count at the bottom of the list | +| **allowPrinting** | `boolean` | optional | Allow users to print the view | +| **emptyState** | `{ title?: string \| Record; message?: string \| Record; icon?: string }` | optional | Empty state configuration when no records found | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes for the list view | +| **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **striped** | `never` | optional | [REMOVED] `view.striped` 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, so authoring it was a parse-clean no-op. There is no authorable striped-rows switch; delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **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. | +| **userFilters** | `{ element?: Enum<'dropdown' \| 'toggle'>; fields?: object[] }` | optional | | + +### Nested Shape: `ObjectListView.columns[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name (snake_case) | +| **label** | `string \| Record` | optional | Display label override | +| **width** | `number` | optional | Column width in pixels | +| **align** | `Enum<'left' \| 'center' \| 'right'>` | optional | Text alignment | +| **hidden** | `boolean` | optional | Hide column by default | +| **sortable** | `boolean` | optional | Allow sorting by this column | +| **resizable** | `boolean` | optional | Allow resizing this column | +| **wrap** | `boolean` | optional | Allow text wrapping | +| **type** | `string` | optional | Renderer type override (e.g., "currency", "date") | +| **pinned** | `Enum<'left' \| 'right'>` | optional | Pin/freeze column to left or right side | +| **summary** | `Enum<'none' \| 'count' \| 'count_empty' \| 'count_filled' \| 'count_unique' \| …> \| { type: Enum<'none' \| 'count' \| 'count_empty' \| 'count_filled' \| 'count_unique' \| …>; field?: string }` | optional | Footer aggregation for this column — the function alone, or `{ type, field }` to aggregate another field | +| **prefix** | `{ field: string; type?: Enum<'badge' \| 'text'> }` | optional | Field rendered inline before this cell value | +| **link** | `boolean` | optional | Functions as the primary navigation link (triggers View navigation) | +| **action** | `string` | optional | Registered Action ID to execute when clicked | + +### Nested Shape: `ObjectListView.filter[number]` + +View filter rule + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name to filter on | +| **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>` | optional | Filter operator | +| **value** | `string \| number \| boolean \| null \| (string \| number)[]` | optional | Filter value. The accepted SHAPE depends on the operator: `in` / `not_in` take an array (any length, including []), `between` takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. | + +### Nested Shape: `ObjectListView.selection` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'none' \| 'single' \| 'multiple'>` | optional (default: `"none"`) | Selection mode | + +### Nested Shape: `ObjectListView.navigation` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **mode** | `Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>` | optional (default: `"page"`) | | +| **view** | `string` | optional | Name of the form view to use for details (e.g. "summary_view", "edit_form") | +| **preventNavigation** | `boolean` | optional (default: `false`) | Disable standard navigation entirely | +| **openNewTab** | `boolean` | optional (default: `false`) | Force open in new tab (applies to page mode) | +| **size** | `Enum<'auto' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'>` | optional (default: `"auto"`) | [#2578] Overlay size bucket for drawer/modal detail: 'auto' (default — renderer derives from field count + viewport; AI writes nothing) or a coarse override sm/md/lg/xl/full. Prefer this over the pixel `width`; page mode ignores it. | +| **width** | `string \| number` | optional | [DEPRECATED → size] Pixel/percent width of the drawer/modal (e.g. "600px"). A pixel width cannot be chosen at authoring time without knowing the client viewport — use the `size` bucket. | + +### Nested Shape: `ObjectListView.pagination` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **pageSize** | `integer` | optional (default: `25`) | Number of records per page | +| **pageSizeOptions** | `integer[]` | optional | Available page size options | + +### Nested Shape: `ObjectListView.kanban` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **groupByField** | `string` | ✅ | Field to group columns by (usually status/select) | +| **summarizeField** | `string` | optional | Field to sum at top of column (e.g. amount) | +| **columns** | `string[]` | ✅ | Fields to show on cards | + +### Nested Shape: `ObjectListView.calendar` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **startDateField** | `string` | ✅ | Field providing the event start date/time | +| **endDateField** | `string` | optional | Field providing the event end date/time (defaults to a single-day event) | +| **titleField** | `string` | ✅ | Field displayed as the event title | +| **colorField** | `string` | optional | Field whose value determines the event color | + +### Nested Shape: `ObjectListView.gantt` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **startDateField** | `string` | ✅ | Field providing the task start date | +| **endDateField** | `string` | ✅ | Field providing the task end date | +| **titleField** | `string` | ✅ | Field displayed as the task title | +| **progressField** | `string` | optional | Field providing the task completion percentage | +| **dependenciesField** | `string` | optional | Field listing the task's predecessor (dependency) record ids | +| **colorField** | `string` | optional | Field that drives the bar color | +| **parentField** | `string` | optional | Field holding the parent task id (builds the summary → step tree) | +| **typeField** | `string` | optional | Field whose value maps to task/summary/milestone | +| **baselineStartField** | `string` | optional | Baseline (planned) start field | +| **baselineEndField** | `string` | optional | Baseline (planned) end field | +| **groupByField** | `string` | optional | Field to group leaf tasks by (synthesized summary rows) | +| **resourceView** | `boolean` | optional | Render a per-resource workload histogram instead of the timeline | +| **assigneeField** | `string` | optional | Resource field to bucket load by (resource view) | +| **effortField** | `string` | optional | Per-task load units (resource view; default 1) | +| **capacity** | `number` | optional | Per-resource capacity ceiling; loads above this flag overload | +| **tooltipFields** | `(string \| { field: string; label?: string })[]` | optional | Fields to surface in the hover tooltip, in display order | +| **quickFilters** | `{ field: string; label?: string; options?: (string \| object)[] }[]` | optional | Multi-select filter dropdowns rendered above the chart | +| **autoZoomToFilter** | `boolean` | optional | When true (default), filtering zooms the range to the filtered tasks | +| **viewMode** | `Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>` | optional | Timeline granularity — one column per day/week/month/quarter/year (also the resource-view column granularity; renderer default 'day') | + +### Nested Shape: `ObjectListView.gallery` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **coverField** | `string` | optional | Attachment/image field to display as card cover | +| **coverFit** | `Enum<'cover' \| 'contain'>` | optional (default: `"cover"`) | Image fit mode for card cover | +| **cardSize** | `Enum<'small' \| 'medium' \| 'large'>` | optional (default: `"medium"`) | Card size in gallery view | +| **titleField** | `string` | optional | Field to display as card title | +| **visibleFields** | `string[]` | optional | Fields to display on card body | + +### Nested Shape: `ObjectListView.timeline` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **startDateField** | `string` | ✅ | Field for timeline item start date | +| **endDateField** | `string` | optional | Field for timeline item end date | +| **titleField** | `string` | ✅ | Field to display as timeline item title | +| **groupByField** | `string` | optional | Field to group timeline rows | +| **colorField** | `string` | optional | Field to determine item color | +| **scale** | `Enum<'hour' \| 'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>` | optional (default: `"week"`) | Default timeline scale | + +### Nested Shape: `ObjectListView.chart` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **chartType** | `Enum<'bar' \| 'line' \| 'pie' \| 'area' \| 'scatter'>` | optional (default: `"bar"`) | Chart visualisation type | +| **dataset** | `string` | ✅ | Dataset name to bind (ADR-0021) | +| **dimensions** | `string[]` | optional | Dimension names — X/group/split | +| **values** | `string[]` | ✅ | Measure names — Y (at least one) | + +### Nested Shape: `ObjectListView.map` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **latitudeField** | `string` | optional | Field providing the marker latitude (used with longitudeField) | +| **longitudeField** | `string` | optional | Field providing the marker longitude (used with latitudeField) | +| **locationField** | `string` | optional | Field providing a combined location — a "lat,lng" string or a `{ lat, lng }` object — as the alternative to the latitudeField/longitudeField pair | +| **titleField** | `string` | optional | Field displayed as the marker title (popup heading, mobile record card, and what the map search box matches on) | +| **descriptionField** | `string` | optional | Field displayed as the marker description | +| **zoom** | `number` | optional | Initial zoom level (1-20). Omit to let the renderer fit the camera to the queried records | +| **center** | `any[]` | optional | Initial camera center as [latitude, longitude]. Omit to let the renderer fit the camera to the queried records | + +### Nested Shape: `ObjectListView.tree` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **parentField** | `string` | optional | Single-parent pointer field (auto-detected from the object schema when omitted) | +| **labelField** | `string` | optional | Field rendered indented in the first column (defaults to "name") | +| **fields** | `string[]` | optional | Additional fields rendered as flat columns alongside the label | +| **defaultExpandedDepth** | `integer` | optional | Initial expansion depth (0 = roots only; omit = expand all) | + +### Nested Shape: `ObjectListView.sharing` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'personal' \| 'collaborative'>` | optional (default: `"collaborative"`) | View ownership type | +| **lockedBy** | `string` | optional | User who locked the view configuration | + +### Nested Shape: `ObjectListView.grouping` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **fields** | `{ field: string; order?: Enum<'asc' \| 'desc'>; collapsed?: boolean }[]` | ✅ | Fields to group by, in nesting order — the first entry is the outermost group and each later entry nests one level deeper (at least one field) | + +### Nested Shape: `ObjectListView.rowColor` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field to derive color from (typically a select/status field) | +| **colors** | `Record` | optional | Map of field value to color (hex/token) | + +### Nested Shape: `ObjectListView.bulkActionDefs[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Stable identifier — the audit-log action key, and (for an aggregate def) the name of the object action to dispatch. | +| **label** | `string` | optional | Button + dialog-header text. Plain string: an authored def is not i18n-resolved (declare a real action and name it in `bulkActions` to get localization). | +| **icon** | `string` | optional | Lucide icon name (e.g. "user-check", "trash-2"). | +| **variant** | `Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'outline'>` | optional | Visual treatment of the button. | +| **operation** | `Enum<'update' \| 'delete' \| 'custom'>` | ✅ | What the executor does: 'update'/'delete' are data-plane mass mutations; 'custom' dispatches an object action (see `execution`). | +| **execution** | `Enum<'perRecord' \| 'aggregate'>` | optional | For `operation: 'custom'` — 'aggregate' dispatches the named action ONCE for the whole selection, carrying every id in `params._selectedIds` (objectui#3139). Required on a custom def: the per-record form is declared as `bulkActions: ['']` instead. | +| **patch** | `Record` | optional | For `operation: 'update'` — static field values applied to every selected record, merged UNDER the user-supplied params so a fixed value can be declared without exposing it in the dialog. | +| **params** | `({ name: string; label?: string; help?: string; type: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; … } & Record)[]` | optional | Inputs collected once before the run. Omit to skip the params step and go straight to confirm. | +| **confirmText** | `string` | optional | Confirmation text shown above the affected-record summary. | +| **confirmLabel** | `string` | optional | Custom Confirm button label (default: "Run"). | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Eligibility predicate (CEL) — a string or a `{dialect, source}` envelope, i.e. `action.visible` without its boolean-literal arm (#5970): a per-record predicate has nothing to say as a constant. Evaluated once PER SELECTED RECORD with that record bound: the button is offered when at least one passes, the run covers only those, and the rest are reported as skipped. A record-free predicate (`features.x`, `current_user.y`) therefore behaves as a plain button-level gate. Fail-closed — a predicate that faults excludes the record. | +| **requiredPermissions** | `string[]` | optional | [ADR-0066 D4] Capability gate on the button, `action.requiredPermissions` semantics verbatim: absent or empty always passes, several are AND-ed, and a client that cannot resolve the caller's capabilities fails OPEN (the server stays the authority). This key exists for INLINE defs — notably the `update`/`delete` data-plane forms, which dispatch no action and so have nothing to inherit a gate from; a def promoted from `bulkActions: ['']` (or an aggregate def naming a declared action) inherits the action's own declaration instead. On a data-plane def the gate governs visibility only — the write itself is still authorized by the data API's object permissions and server hooks. | +| **maxRecords** | `integer` | optional | Selection size above which the run is blocked. Set it on defs whose server work is expensive — an aggregate def carries every selected id in one request. | +| **batchSize** | `integer` | optional | Records per executor batch (default 200). Data-plane operations only — an aggregate run is a single call by definition. | + +### Nested Shape: `ObjectListView.conditionalFormatting[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL) to evaluate. | +| **style** | `Record` | ✅ | CSS styles to apply when condition is true | + +### Nested Shape: `ObjectListView.exportOptions` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **formats** | `Enum<'csv' \| 'xlsx' \| 'json'>[]` | optional | Formats offered in the export menu (default: ['csv', 'json']). XLSX is delivered by the server stream only. | +| **maxRecords** | `integer` | optional | Maximum number of records to export; 0 or absent = unlimited | +| **includeHeaders** | `boolean` | optional | Include column headers in the exported file (default true) | +| **fileNamePrefix** | `string` | optional | Download file name prefix — replaces the object label and suppresses the view label in the generated file name | +| **streaming** | `boolean` | optional | Set false to force the client-side export path (csv/json only) instead of the server stream | + +### Nested Shape: `ObjectListView.userActions` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **sort** | `boolean` | optional (default: `true`) | Allow users to sort records | +| **search** | `boolean` | optional (default: `true`) | Allow users to search records | +| **filter** | `boolean` | optional (default: `true`) | Allow users to filter records | +| **refresh** | `boolean` | optional (default: `true`) | Allow users to reload the view data from the backend without a full page reload | +| **rowHeight** | `boolean` | optional (default: `true`) | Allow users to toggle row height/density | +| **group** | `boolean` | optional (default: `true`) | Allow users to change record grouping from the toolbar. Toggle only — the grouping itself is configured in the view-level `grouping` block. | +| **addRecordForm** | `boolean` | optional (default: `false`) | Add records through a form instead of inline | +| **editInline** | `boolean` | optional (default: `false`) | Allow users to edit records inline — click a cell to edit it with the field's type-aware widget (the same control the form uses). Off by default: the list is read-only unless the author opts in. | +| **hideFields** | `boolean` | optional (default: `false`) | Allow users to hide/show fields from the toolbar (the affordance behind the view-level `hiddenFields` list). Boolean toggle — distinct from the record-details component's `hideFields`, which is an array of field names to omit. Off by default: column hiding is opt-in. | +| **rowColor** | `boolean` | optional (default: `false`) | Allow users to configure row colouring from the toolbar. Boolean toggle — the colour rules themselves live in the view-level `rowColor` block. Off by default: row colouring is opt-in. | +| **buttons** | `string[]` | optional | Custom action button IDs to show in the toolbar | + +### Nested Shape: `ObjectListView.appearance` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **showDescription** | `boolean` | optional (default: `true`) | Show the view description text | +| **allowedVisualizations** | `Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>[]` | optional | Whitelist of visualization types users can switch between (e.g. ["grid", "gallery", "kanban"]) | + +### Nested Shape: `ObjectListView.tabs[number]` + +Tab configuration for multi-tab view interface + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Tab identifier (snake_case) | +| **label** | `string \| Record` | optional | Display label | +| **icon** | `string` | optional | Tab icon name | +| **view** | `string` | optional | Referenced list view name from listViews | +| **filter** | `{ field: string; operator?: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Tab-specific filter criteria | +| **order** | `integer` | optional | Tab display order | +| **pinned** | `boolean` | optional (default: `false`) | Pin tab (cannot be removed by users) | +| **isDefault** | `boolean` | optional (default: `false`) | Set as the default active tab | +| **visible** | `boolean` | optional (default: `true`) | Tab visibility | + +### Nested Shape: `ObjectListView.addRecord` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional (default: `true`) | Show the add record entry point | +| **position** | `Enum<'top' \| 'bottom' \| 'both'>` | optional (default: `"bottom"`) | Position of the add record button | +| **mode** | `Enum<'inline' \| 'form' \| 'modal'>` | optional (default: `"inline"`) | How to add a new record | +| **formView** | `string` | optional | Named form view to use when mode is "form" or "modal" | + +### Nested Shape: `ObjectListView.emptyState` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **title** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **message** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **icon** | `string` | optional | | + +### Nested Shape: `ObjectListView.aria` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **ariaLabel** | `string \| Record` | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. | +| **ariaDescribedBy** | `string` | optional | ID of element providing additional description (WAI-ARIA aria-describedby) | +| **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | + +### Nested Shape: `ObjectListView.userFilters` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **element** | `Enum<'dropdown' \| 'toggle'>` | optional (default: `"dropdown"`) | Filter control style on object views: "dropdown" (per-field value chips). "toggle" is deprecated. "tabs" is page-only — use `listViews` for named presets. | +| **fields** | `{ field: string; label?: string \| Record; type?: Enum<'select' \| 'multi-select' \| 'boolean' \| 'date-range' \| 'text'>; options?: object[]; … }[]` | optional | Fields exposed as quick filters (dropdown/toggle elements) | + + +--- + +## ObjectUserFilters + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **element** | `Enum<'dropdown' \| 'toggle'>` | optional (default: `"dropdown"`) | Filter control style on object views: "dropdown" (per-field value chips). "toggle" is deprecated. "tabs" is page-only — use `listViews` for named presets. | +| **fields** | `{ field: string; label?: string \| Record; type?: Enum<'select' \| 'multi-select' \| 'boolean' \| 'date-range' \| 'text'>; options?: object[]; … }[]` | optional | Fields exposed as quick filters (dropdown/toggle elements) | + +### Nested Shape: `ObjectUserFilters.fields[number]` + +Quick-filter field configuration + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name on the source object (must exist — checked by reference diagnostics) | +| **label** | `string \| Record` | optional | Display label override (defaults to the field label) | +| **type** | `Enum<'select' \| 'multi-select' \| 'boolean' \| 'date-range' \| 'text'>` | optional | Filter control type. Omit to infer from the field definition | +| **options** | `{ value: string \| number \| boolean; label: string \| Record; color?: string }[]` | optional | Static options. Omit to derive from the field definition (select options / lookup records) | +| **showCount** | `boolean` | optional | Show per-option record counts | +| **defaultValues** | `(string \| number \| boolean)[]` | optional | Pre-selected values when the view loads | + + +--- + +## PaginationConfig + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **pageSize** | `integer` | optional (default: `25`) | Number of records per page | +| **pageSizeOptions** | `integer[]` | optional | Available page size options | + + +--- + +## RowColorConfig + +Row color configuration based on field values + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field to derive color from (typically a select/status field) | +| **colors** | `Record` | optional | Map of field value to color (hex/token) | + + +--- ## RowHeight @@ -761,6 +1490,14 @@ Quick-filter field configuration | **showCount** | `boolean` | optional | Show per-option record counts | | **defaultValues** | `(string \| number \| boolean)[]` | optional | Pre-selected values when the view loads | +### Nested Shape: `UserFilterField.options[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **value** | `string \| number \| boolean` | ✅ | Option value | +| **label** | `string \| Record` | ✅ | Option label | +| **color** | `string` | optional | Option color token/hex | + --- @@ -778,30 +1515,235 @@ End-user quick-filter configuration (Airtable "User filters" parity) | **showAllRecords** | `boolean` | optional | Show an "All records" tab before the presets (tabs element) | | **allowAddTab** | `boolean` | optional | Let end users add their own tab after the presets (tabs element): the affordance asks for a name and snapshots the filters currently applied as a new tab. SESSION-SCOPED — an added tab lives only for the current mount, is never written back as metadata (ADR-0047), and carries a remove control the authored presets do not. Page lists only — object views use `listViews` for named presets | +### Nested Shape: `UserFilters.fields[number]` + +Quick-filter field configuration + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name on the source object (must exist — checked by reference diagnostics) | +| **label** | `string \| Record` | optional | Display label override (defaults to the field label) | +| **type** | `Enum<'select' \| 'multi-select' \| 'boolean' \| 'date-range' \| 'text'>` | optional | Filter control type. Omit to infer from the field definition | +| **options** | `{ value: string \| number \| boolean; label: string \| Record; color?: string }[]` | optional | Static options. Omit to derive from the field definition (select options / lookup records) | +| **showCount** | `boolean` | optional | Show per-option record counts | +| **defaultValues** | `(string \| number \| boolean)[]` | optional | Pre-selected values when the view loads | + +### Nested Shape: `UserFilters.tabs[number]` + +Tab configuration for multi-tab view interface + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Tab identifier (snake_case) | +| **label** | `string \| Record` | optional | Display label | +| **icon** | `string` | optional | Tab icon name | +| **view** | `string` | optional | Referenced list view name from listViews | +| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Tab-specific filter criteria | +| **order** | `integer` | optional | Tab display order | +| **pinned** | `boolean` | optional (default: `false`) | Pin tab (cannot be removed by users) | +| **isDefault** | `boolean` | optional (default: `false`) | Set as the default active tab | +| **visible** | `boolean` | optional (default: `true`) | Tab visibility | + --- ## View -### Properties +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional | Item name — supplied by the metadata door; for an object-scoped container it is the object name. | +| **label** | `string \| Record` | optional | Human-readable label shown in metadata lists. | +| **object** | `string` | optional | Object this container binds to — how a stack-level `views: [...]` entry says which object its views belong to; read by `getViewsByObject()` / `GET /meta/view?object=`. | +| **list** | `{ name?: string; label?: string \| Record; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: object \| … +3 more; … }` | optional | | +| **form** | `{ type?: Enum<'simple' \| 'tabbed' \| 'wizard' \| 'split' \| 'drawer' \| 'modal'>; layout?: Enum<'vertical' \| 'horizontal' \| 'inline' \| 'grid'>; columns?: integer; title?: string; … }` | optional | | +| **listViews** | `Record; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: object \| … +3 more; … }>` | optional | Additional named list views (views mode — dropdown userFilters allowed, no tabs; ADR-0047) | +| **formViews** | `Record; layout?: Enum<'vertical' \| 'horizontal' \| 'inline' \| 'grid'>; columns?: integer; title?: string; … }>` | optional | Additional named form views | +| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this view. | +| **_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. | + +### Nested Shape: `View.list` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional | Internal view name (lowercase snake_case) | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **type** | `Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>` | optional (default: `"grid"`) | | +| **data** | `{ provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source configuration (defaults to "object" provider) | +| **columns** | `string[] \| { field: string; label?: string \| Record; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | ✅ | Fields to display as columns | +| **filter** | `{ field: string; operator?: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Filter criteria (JSON Rules) | +| **sort** | `string \| { field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | | +| **searchableFields** | `string[]` | optional | Fields enabled for search | +| **filterableFields** | `string[]` | optional | Legacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters | +| **resizable** | `boolean` | optional | Enable column resizing | +| **compactToolbar** | `boolean` | optional | Collapse Group/Color/Density/Hide-fields into a single View settings popover | +| **selection** | `{ type?: Enum<'none' \| 'single' \| 'multiple'> }` | optional | Row selection configuration | +| **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; view?: string; preventNavigation?: boolean; openNewTab?: boolean; … }` | optional | Configuration for item click navigation (page, drawer, modal, etc.) | +| **pagination** | `{ pageSize?: integer; pageSizeOptions?: integer[] }` | optional | Pagination configuration | +| **kanban** | `{ groupByField: string; summarizeField?: string; columns: string[] }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout | +| **calendar** | `{ startDateField: string; endDateField?: string; titleField: string; colorField?: string }` | optional | Calendar configuration — applies when the view renders as a calendar layout | +| **gantt** | `{ startDateField: string; endDateField: string; titleField: string; progressField?: string; … } & Record` | optional | Gantt-timeline configuration — applies when the view renders as a gantt layout | +| **gallery** | `{ coverField?: string; coverFit?: Enum<'cover' \| 'contain'>; cardSize?: Enum<'small' \| 'medium' \| 'large'>; titleField?: string; … }` | optional | Gallery/card view configuration | +| **timeline** | `{ startDateField: string; endDateField?: string; titleField: string; groupByField?: string; … }` | optional | Timeline view configuration | +| **chart** | `{ chartType?: Enum<'bar' \| 'line' \| 'pie' \| 'area' \| 'scatter'>; dataset: string; dimensions?: string[]; values: string[] }` | optional | List chart view configuration | +| **map** | `{ latitudeField?: string; longitudeField?: string; locationField?: string; titleField?: string; … }` | optional | Map configuration — applies when the view renders as a map layout | +| **tree** | `{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer } & Record` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout | +| **description** | `string \| Record` | optional | View description for documentation/tooltips | +| **sharing** | `{ type?: Enum<'personal' \| 'collaborative'>; lockedBy?: string }` | optional | View sharing and access configuration | +| **rowHeight** | `Enum<'compact' \| 'short' \| 'medium' \| 'tall' \| 'extra_tall'>` | optional | Row height / density setting | +| **grouping** | `{ fields: object[] }` | optional | Group records by one or more fields | +| **rowColor** | `{ field: string; colors?: Record }` | optional | Color rows based on field value | +| **hiddenFields** | `string[]` | optional | Fields to hide in this specific view | +| **fieldOrder** | `string[]` | optional | Explicit field display order for this view | +| **rowActions** | `string[]` | optional | Actions available for individual row items | +| **bulkActions** | `string[]` | optional | Actions available when multiple rows are selected | +| **bulkActionDefs** | `{ name: string; label?: string; icon?: string; variant?: Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'outline'>; … }[]` | optional | Rich bulk action definitions (schema-driven, executed via BulkActionDialog). Use a def for a mass data-plane mutation ('update' with a `patch` / 'delete') that no action expresses, or for an `operation: 'custom'` + `execution: 'aggregate'` entry (objectui#3139) that dispatches the action it NAMES once for the whole selection — the renderer injects `params._selectedIds: string[]` (read that on the server, not `recordId`) so a single call can produce one aggregate artifact (zip of QR codes, merged PDF, batch print). Aggregate results are all-or-nothing: a handler that cannot cover the whole selection must reject, and per-row retry is replaced by re-running the action. `batchSize` does not apply (the call is never chunked); set `maxRecords` on defs whose server work is expensive. For the PER-RECORD dispatch use `bulkActions: ['']` instead — the bare-string form, promoted with the action's own label, params and `visible`; a 'custom' def without `execution: 'aggregate'` has no dispatcher and is refused at parse time (#4457). Toolbar url/api actions can also interpolate the current selection via `${ctx.selection.ids}` / `${ctx.selection.count}`. | +| **conditionalFormatting** | `{ condition: string \| object; style: Record }[]` | optional | Conditional formatting rules for list rows | +| **inlineEdit** | `boolean` | optional | Allow inline editing of records directly in the list view | +| **exportOptions** | `Enum<'csv' \| 'xlsx' \| 'json'>[] \| { formats?: Enum<'csv' \| 'xlsx' \| 'json'>[]; maxRecords?: integer; includeHeaders?: boolean; fileNamePrefix?: string; … }` | optional | Export configuration for the list toolbar export menu: `{ formats?, maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }`. A bare format array is the legacy spelling and lifts to `{ formats: [...] }` at parse. | +| **userActions** | `{ sort?: boolean; search?: boolean; filter?: boolean; refresh?: boolean; … }` | optional | User action toggles for the view toolbar | +| **appearance** | `{ showDescription?: boolean; allowedVisualizations?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>[] }` | optional | Appearance and visualization configuration | +| **tabs** | `{ name: string; label?: string \| Record; icon?: string; view?: string; … }[]` | optional | Tab definitions for multi-tab view interface | +| **addRecord** | `{ enabled?: boolean; position?: Enum<'top' \| 'bottom' \| 'both'>; mode?: Enum<'inline' \| 'form' \| 'modal'>; formView?: string }` | optional | Add record entry point configuration | +| **showRecordCount** | `boolean` | optional | Show record count at the bottom of the list | +| **allowPrinting** | `boolean` | optional | Allow users to print the view | +| **emptyState** | `{ title?: string \| Record; message?: string \| Record; icon?: string }` | optional | Empty state configuration when no records found | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes for the list view | +| **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **striped** | `never` | optional | [REMOVED] `view.striped` 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, so authoring it was a parse-clean no-op. There is no authorable striped-rows switch; delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **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. | +| **userFilters** | `{ element?: Enum<'dropdown' \| 'toggle'>; fields?: object[] }` | optional | | + +### Nested Shape: `View.form` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'simple' \| 'tabbed' \| 'wizard' \| 'split' \| 'drawer' \| 'modal'>` | optional (default: `"simple"`) | | +| **layout** | `Enum<'vertical' \| 'horizontal' \| 'inline' \| 'grid'>` | optional | Field layout direction | +| **columns** | `integer` | optional | Number of columns for the form body | +| **title** | `string` | optional | Form title | +| **description** | `string` | optional | Form description | +| **defaultTab** | `string` | optional | Initially active tab (tabbed forms) | +| **tabPosition** | `Enum<'top' \| 'bottom' \| 'left' \| 'right'>` | optional | Tab strip position (tabbed forms) | +| **allowSkip** | `boolean` | optional | Allow skipping steps (wizard forms) | +| **showStepIndicator** | `boolean` | optional | Show the step indicator (wizard forms) | +| **splitDirection** | `Enum<'horizontal' \| 'vertical'>` | optional | Split orientation (split forms) | +| **splitSize** | `number` | optional | Primary split panel size, % (split forms) | +| **splitResizable** | `boolean` | optional | Whether the split is resizable (split forms) | +| **drawerSide** | `Enum<'top' \| 'bottom' \| 'left' \| 'right'>` | optional | Drawer side (drawer forms) | +| **drawerWidth** | `string` | optional | [DEPRECATED → size buckets] Drawer width, e.g. "480px". A pixel width cannot be chosen without knowing the client viewport — the renderer derives it. | +| **modalSize** | `Enum<'sm' \| 'default' \| 'lg' \| 'xl' \| 'full'>` | optional | Modal size (modal forms) | +| **data** | `{ provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source configuration (defaults to "object" provider) | +| **sections** | `{ name?: string; label?: string \| Record; description?: string; collapsible?: boolean; … }[]` | optional | | +| **groups** | `{ name?: string; label?: string \| Record; description?: string; collapsible?: boolean; … }[]` | optional | [LEGACY ALIAS → `sections`] Accepted for back-compat and folded onto `sections` at parse; `sections` wins when both are present. Prefer `sections`. | +| **subforms** | `{ childObject: string; relationshipField?: string; columns?: any[]; amountField?: string; … }[]` | optional | Inline master-detail child collections | +| **defaultSort** | `never` | optional | [REMOVED] `form.defaultSort` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — nothing read it: a related list inside a form sorts by its own list view's `sort`. Delete the key and set the sort on the related list view instead. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **sharing** | `{ enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … }` | optional | Public sharing configuration for this form | +| **submitBehavior** | `{ kind: 'thank-you'; title?: string; message?: string } \| { kind: 'redirect'; url: string; delayMs?: integer } \| { kind: 'continue' } \| { kind: 'next-record' }` | optional | Post-submit behavior. On the `redirect` arm, `url` is relative-only and interpolates only declared record fields as `{{record.field_name}}`, URL-escaped (ruled 2026-08-11, #7496). | +| **buttons** | `{ submit?: object; cancel?: object; reset?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm (framework#1894 / #2998). | +| **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: `View.listViews[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional | Internal view name (lowercase snake_case) | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **type** | `Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>` | optional (default: `"grid"`) | | +| **data** | `{ provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source configuration (defaults to "object" provider) | +| **columns** | `string[] \| { field: string; label?: string \| Record; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | ✅ | Fields to display as columns | +| **filter** | `{ field: string; operator?: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Filter criteria (JSON Rules) | +| **sort** | `string \| { field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | | +| **searchableFields** | `string[]` | optional | Fields enabled for search | +| **filterableFields** | `string[]` | optional | Legacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters | +| **resizable** | `boolean` | optional | Enable column resizing | +| **compactToolbar** | `boolean` | optional | Collapse Group/Color/Density/Hide-fields into a single View settings popover | +| **selection** | `{ type?: Enum<'none' \| 'single' \| 'multiple'> }` | optional | Row selection configuration | +| **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; view?: string; preventNavigation?: boolean; openNewTab?: boolean; … }` | optional | Configuration for item click navigation (page, drawer, modal, etc.) | +| **pagination** | `{ pageSize?: integer; pageSizeOptions?: integer[] }` | optional | Pagination configuration | +| **kanban** | `{ groupByField: string; summarizeField?: string; columns: string[] }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout | +| **calendar** | `{ startDateField: string; endDateField?: string; titleField: string; colorField?: string }` | optional | Calendar configuration — applies when the view renders as a calendar layout | +| **gantt** | `{ startDateField: string; endDateField: string; titleField: string; progressField?: string; … } & Record` | optional | Gantt-timeline configuration — applies when the view renders as a gantt layout | +| **gallery** | `{ coverField?: string; coverFit?: Enum<'cover' \| 'contain'>; cardSize?: Enum<'small' \| 'medium' \| 'large'>; titleField?: string; … }` | optional | Gallery/card view configuration | +| **timeline** | `{ startDateField: string; endDateField?: string; titleField: string; groupByField?: string; … }` | optional | Timeline view configuration | +| **chart** | `{ chartType?: Enum<'bar' \| 'line' \| 'pie' \| 'area' \| 'scatter'>; dataset: string; dimensions?: string[]; values: string[] }` | optional | List chart view configuration | +| **map** | `{ latitudeField?: string; longitudeField?: string; locationField?: string; titleField?: string; … }` | optional | Map configuration — applies when the view renders as a map layout | +| **tree** | `{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer } & Record` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout | +| **description** | `string \| Record` | optional | View description for documentation/tooltips | +| **sharing** | `{ type?: Enum<'personal' \| 'collaborative'>; lockedBy?: string }` | optional | View sharing and access configuration | +| **rowHeight** | `Enum<'compact' \| 'short' \| 'medium' \| 'tall' \| 'extra_tall'>` | optional | Row height / density setting | +| **grouping** | `{ fields: object[] }` | optional | Group records by one or more fields | +| **rowColor** | `{ field: string; colors?: Record }` | optional | Color rows based on field value | +| **hiddenFields** | `string[]` | optional | Fields to hide in this specific view | +| **fieldOrder** | `string[]` | optional | Explicit field display order for this view | +| **rowActions** | `string[]` | optional | Actions available for individual row items | +| **bulkActions** | `string[]` | optional | Actions available when multiple rows are selected | +| **bulkActionDefs** | `{ name: string; label?: string; icon?: string; variant?: Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'outline'>; … }[]` | optional | Rich bulk action definitions (schema-driven, executed via BulkActionDialog). Use a def for a mass data-plane mutation ('update' with a `patch` / 'delete') that no action expresses, or for an `operation: 'custom'` + `execution: 'aggregate'` entry (objectui#3139) that dispatches the action it NAMES once for the whole selection — the renderer injects `params._selectedIds: string[]` (read that on the server, not `recordId`) so a single call can produce one aggregate artifact (zip of QR codes, merged PDF, batch print). Aggregate results are all-or-nothing: a handler that cannot cover the whole selection must reject, and per-row retry is replaced by re-running the action. `batchSize` does not apply (the call is never chunked); set `maxRecords` on defs whose server work is expensive. For the PER-RECORD dispatch use `bulkActions: ['']` instead — the bare-string form, promoted with the action's own label, params and `visible`; a 'custom' def without `execution: 'aggregate'` has no dispatcher and is refused at parse time (#4457). Toolbar url/api actions can also interpolate the current selection via `${ctx.selection.ids}` / `${ctx.selection.count}`. | +| **conditionalFormatting** | `{ condition: string \| object; style: Record }[]` | optional | Conditional formatting rules for list rows | +| **inlineEdit** | `boolean` | optional | Allow inline editing of records directly in the list view | +| **exportOptions** | `Enum<'csv' \| 'xlsx' \| 'json'>[] \| { formats?: Enum<'csv' \| 'xlsx' \| 'json'>[]; maxRecords?: integer; includeHeaders?: boolean; fileNamePrefix?: string; … }` | optional | Export configuration for the list toolbar export menu: `{ formats?, maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }`. A bare format array is the legacy spelling and lifts to `{ formats: [...] }` at parse. | +| **userActions** | `{ sort?: boolean; search?: boolean; filter?: boolean; refresh?: boolean; … }` | optional | User action toggles for the view toolbar | +| **appearance** | `{ showDescription?: boolean; allowedVisualizations?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>[] }` | optional | Appearance and visualization configuration | +| **tabs** | `{ name: string; label?: string \| Record; icon?: string; view?: string; … }[]` | optional | Tab definitions for multi-tab view interface | +| **addRecord** | `{ enabled?: boolean; position?: Enum<'top' \| 'bottom' \| 'both'>; mode?: Enum<'inline' \| 'form' \| 'modal'>; formView?: string }` | optional | Add record entry point configuration | +| **showRecordCount** | `boolean` | optional | Show record count at the bottom of the list | +| **allowPrinting** | `boolean` | optional | Allow users to print the view | +| **emptyState** | `{ title?: string \| Record; message?: string \| Record; icon?: string }` | optional | Empty state configuration when no records found | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes for the list view | +| **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **striped** | `never` | optional | [REMOVED] `view.striped` 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, so authoring it was a parse-clean no-op. There is no authorable striped-rows switch; delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **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. | +| **userFilters** | `{ element?: Enum<'dropdown' \| 'toggle'>; fields?: object[] }` | optional | | + +### Nested Shape: `View.formViews[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'simple' \| 'tabbed' \| 'wizard' \| 'split' \| 'drawer' \| 'modal'>` | optional (default: `"simple"`) | | +| **layout** | `Enum<'vertical' \| 'horizontal' \| 'inline' \| 'grid'>` | optional | Field layout direction | +| **columns** | `integer` | optional | Number of columns for the form body | +| **title** | `string` | optional | Form title | +| **description** | `string` | optional | Form description | +| **defaultTab** | `string` | optional | Initially active tab (tabbed forms) | +| **tabPosition** | `Enum<'top' \| 'bottom' \| 'left' \| 'right'>` | optional | Tab strip position (tabbed forms) | +| **allowSkip** | `boolean` | optional | Allow skipping steps (wizard forms) | +| **showStepIndicator** | `boolean` | optional | Show the step indicator (wizard forms) | +| **splitDirection** | `Enum<'horizontal' \| 'vertical'>` | optional | Split orientation (split forms) | +| **splitSize** | `number` | optional | Primary split panel size, % (split forms) | +| **splitResizable** | `boolean` | optional | Whether the split is resizable (split forms) | +| **drawerSide** | `Enum<'top' \| 'bottom' \| 'left' \| 'right'>` | optional | Drawer side (drawer forms) | +| **drawerWidth** | `string` | optional | [DEPRECATED → size buckets] Drawer width, e.g. "480px". A pixel width cannot be chosen without knowing the client viewport — the renderer derives it. | +| **modalSize** | `Enum<'sm' \| 'default' \| 'lg' \| 'xl' \| 'full'>` | optional | Modal size (modal forms) | +| **data** | `{ provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source configuration (defaults to "object" provider) | +| **sections** | `{ name?: string; label?: string \| Record; description?: string; collapsible?: boolean; … }[]` | optional | | +| **groups** | `{ name?: string; label?: string \| Record; description?: string; collapsible?: boolean; … }[]` | optional | [LEGACY ALIAS → `sections`] Accepted for back-compat and folded onto `sections` at parse; `sections` wins when both are present. Prefer `sections`. | +| **subforms** | `{ childObject: string; relationshipField?: string; columns?: any[]; amountField?: string; … }[]` | optional | Inline master-detail child collections | +| **defaultSort** | `never` | optional | [REMOVED] `form.defaultSort` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — nothing read it: a related list inside a form sorts by its own list view's `sort`. Delete the key and set the sort on the related list view instead. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **sharing** | `{ enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … }` | optional | Public sharing configuration for this form | +| **submitBehavior** | `{ kind: 'thank-you'; title?: string; message?: string } \| { kind: 'redirect'; url: string; delayMs?: integer } \| { kind: 'continue' } \| { kind: 'next-record' }` | optional | Post-submit behavior. On the `redirect` arm, `url` is relative-only and interpolates only declared record fields as `{{record.field_name}}`, URL-escaped (ruled 2026-08-11, #7496). | +| **buttons** | `{ submit?: object; cancel?: object; reset?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm (framework#1894 / #2998). | +| **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: `View.protection` | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **name** | `string` | optional | Item name — supplied by the metadata door; for an object-scoped container it is the object name. | -| **label** | `string \| Record` | optional | Human-readable label shown in metadata lists. | -| **object** | `string` | optional | Object this container binds to — how a stack-level `views: [...]` entry says which object its views belong to; read by `getViewsByObject()` / `GET /meta/view?object=`. | -| **list** | `{ name?: string; label?: string \| Record; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: object \| … +3 more; … }` | optional | | -| **form** | `{ type?: Enum<'simple' \| 'tabbed' \| 'wizard' \| 'split' \| 'drawer' \| 'modal'>; layout?: Enum<'vertical' \| 'horizontal' \| 'inline' \| 'grid'>; columns?: integer; title?: string; … }` | optional | | -| **listViews** | `Record; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: object \| … +3 more; … }>` | optional | Additional named list views (views mode — dropdown userFilters allowed, no tabs; ADR-0047) | -| **formViews** | `Record; layout?: Enum<'vertical' \| 'horizontal' \| 'inline' \| 'grid'>; columns?: integer; title?: string; … }>` | optional | Additional named form views | -| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this view. | -| **_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. | +| **lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | ✅ | Lock policy — none \| no-overlay \| no-delete \| full. | +| **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | +| **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | --- @@ -833,6 +1775,26 @@ This schema accepts one of the following structures: | **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: `ViewData.read` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **url** | `string` | ✅ | API endpoint URL | +| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'>` | optional (default: `"GET"`) | HTTP method | +| **headers** | `Record` | optional | Custom HTTP headers | +| **params** | `Record` | optional | Query parameters | +| **body** | `any` | optional | Request body for POST/PUT/PATCH | + +### Nested Shape: `ViewData.write` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **url** | `string` | ✅ | API endpoint URL | +| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'>` | optional (default: `"GET"`) | HTTP method | +| **headers** | `Record` | optional | Custom HTTP headers | +| **params** | `Record` | optional | Query parameters | +| **body** | `any` | optional | Request body for POST/PUT/PATCH | + --- #### Option 3 @@ -930,6 +1892,68 @@ This schema accepts one of the following structures: | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `ViewItem.config` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional | Internal view name (lowercase snake_case) | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **type** | `Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>` | optional (default: `"grid"`) | | +| **data** | `{ provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source configuration (defaults to "object" provider) | +| **columns** | `string[] \| { field: string; label?: string \| Record; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | ✅ | Fields to display as columns | +| **filter** | `{ field: string; operator?: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Filter criteria (JSON Rules) | +| **sort** | `string \| { field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | | +| **searchableFields** | `string[]` | optional | Fields enabled for search | +| **filterableFields** | `string[]` | optional | Legacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters | +| **userFilters** | `{ element?: Enum<'dropdown' \| 'tabs' \| 'toggle'>; fields?: object[]; tabs?: object[]; showAllRecords?: boolean; … }` | optional | End-user quick-filter bar: dropdown/toggle fields or tab presets. Omit to let the renderer derive filters from select/boolean fields | +| **resizable** | `boolean` | optional | Enable column resizing | +| **compactToolbar** | `boolean` | optional | Collapse Group/Color/Density/Hide-fields into a single View settings popover | +| **selection** | `{ type?: Enum<'none' \| 'single' \| 'multiple'> }` | optional | Row selection configuration | +| **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; view?: string; preventNavigation?: boolean; openNewTab?: boolean; … }` | optional | Configuration for item click navigation (page, drawer, modal, etc.) | +| **pagination** | `{ pageSize?: integer; pageSizeOptions?: integer[] }` | optional | Pagination configuration | +| **kanban** | `{ groupByField: string; summarizeField?: string; columns: string[] }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout | +| **calendar** | `{ startDateField: string; endDateField?: string; titleField: string; colorField?: string }` | optional | Calendar configuration — applies when the view renders as a calendar layout | +| **gantt** | `{ startDateField: string; endDateField: string; titleField: string; progressField?: string; … } & Record` | optional | Gantt-timeline configuration — applies when the view renders as a gantt layout | +| **gallery** | `{ coverField?: string; coverFit?: Enum<'cover' \| 'contain'>; cardSize?: Enum<'small' \| 'medium' \| 'large'>; titleField?: string; … }` | optional | Gallery/card view configuration | +| **timeline** | `{ startDateField: string; endDateField?: string; titleField: string; groupByField?: string; … }` | optional | Timeline view configuration | +| **chart** | `{ chartType?: Enum<'bar' \| 'line' \| 'pie' \| 'area' \| 'scatter'>; dataset: string; dimensions?: string[]; values: string[] }` | optional | List chart view configuration | +| **map** | `{ latitudeField?: string; longitudeField?: string; locationField?: string; titleField?: string; … }` | optional | Map configuration — applies when the view renders as a map layout | +| **tree** | `{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer } & Record` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout | +| **description** | `string \| Record` | optional | View description for documentation/tooltips | +| **sharing** | `{ type?: Enum<'personal' \| 'collaborative'>; lockedBy?: string }` | optional | View sharing and access configuration | +| **rowHeight** | `Enum<'compact' \| 'short' \| 'medium' \| 'tall' \| 'extra_tall'>` | optional | Row height / density setting | +| **grouping** | `{ fields: object[] }` | optional | Group records by one or more fields | +| **rowColor** | `{ field: string; colors?: Record }` | optional | Color rows based on field value | +| **hiddenFields** | `string[]` | optional | Fields to hide in this specific view | +| **fieldOrder** | `string[]` | optional | Explicit field display order for this view | +| **rowActions** | `string[]` | optional | Actions available for individual row items | +| **bulkActions** | `string[]` | optional | Actions available when multiple rows are selected | +| **bulkActionDefs** | `{ name: string; label?: string; icon?: string; variant?: Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'outline'>; … }[]` | optional | Rich bulk action definitions (schema-driven, executed via BulkActionDialog). Use a def for a mass data-plane mutation ('update' with a `patch` / 'delete') that no action expresses, or for an `operation: 'custom'` + `execution: 'aggregate'` entry (objectui#3139) that dispatches the action it NAMES once for the whole selection — the renderer injects `params._selectedIds: string[]` (read that on the server, not `recordId`) so a single call can produce one aggregate artifact (zip of QR codes, merged PDF, batch print). Aggregate results are all-or-nothing: a handler that cannot cover the whole selection must reject, and per-row retry is replaced by re-running the action. `batchSize` does not apply (the call is never chunked); set `maxRecords` on defs whose server work is expensive. For the PER-RECORD dispatch use `bulkActions: ['']` instead — the bare-string form, promoted with the action's own label, params and `visible`; a 'custom' def without `execution: 'aggregate'` has no dispatcher and is refused at parse time (#4457). Toolbar url/api actions can also interpolate the current selection via `${ctx.selection.ids}` / `${ctx.selection.count}`. | +| **conditionalFormatting** | `{ condition: string \| object; style: Record }[]` | optional | Conditional formatting rules for list rows | +| **inlineEdit** | `boolean` | optional | Allow inline editing of records directly in the list view | +| **exportOptions** | `Enum<'csv' \| 'xlsx' \| 'json'>[] \| { formats?: Enum<'csv' \| 'xlsx' \| 'json'>[]; maxRecords?: integer; includeHeaders?: boolean; fileNamePrefix?: string; … }` | optional | Export configuration for the list toolbar export menu: `{ formats?, maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }`. A bare format array is the legacy spelling and lifts to `{ formats: [...] }` at parse. | +| **userActions** | `{ sort?: boolean; search?: boolean; filter?: boolean; refresh?: boolean; … }` | optional | User action toggles for the view toolbar | +| **appearance** | `{ showDescription?: boolean; allowedVisualizations?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>[] }` | optional | Appearance and visualization configuration | +| **tabs** | `{ name: string; label?: string \| Record; icon?: string; view?: string; … }[]` | optional | Tab definitions for multi-tab view interface | +| **addRecord** | `{ enabled?: boolean; position?: Enum<'top' \| 'bottom' \| 'both'>; mode?: Enum<'inline' \| 'form' \| 'modal'>; formView?: string }` | optional | Add record entry point configuration | +| **showRecordCount** | `boolean` | optional | Show record count at the bottom of the list | +| **allowPrinting** | `boolean` | optional | Allow users to print the view | +| **emptyState** | `{ title?: string \| Record; message?: string \| Record; icon?: string }` | optional | Empty state configuration when no records found | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes for the list view | +| **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **striped** | `never` | optional | [REMOVED] `view.striped` 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, so authoring it was a parse-clean no-op. There is no authorable striped-rows switch; delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **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: `ViewItem.protection` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | ✅ | Lock policy — none \| no-overlay \| no-delete \| full. | +| **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | +| **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | + --- #### Option 2 @@ -957,6 +1981,44 @@ This schema accepts one of the following structures: | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Nested Shape: `ViewItem.config` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'simple' \| 'tabbed' \| 'wizard' \| 'split' \| 'drawer' \| 'modal'>` | optional (default: `"simple"`) | | +| **layout** | `Enum<'vertical' \| 'horizontal' \| 'inline' \| 'grid'>` | optional | Field layout direction | +| **columns** | `integer` | optional | Number of columns for the form body | +| **title** | `string` | optional | Form title | +| **description** | `string` | optional | Form description | +| **defaultTab** | `string` | optional | Initially active tab (tabbed forms) | +| **tabPosition** | `Enum<'top' \| 'bottom' \| 'left' \| 'right'>` | optional | Tab strip position (tabbed forms) | +| **allowSkip** | `boolean` | optional | Allow skipping steps (wizard forms) | +| **showStepIndicator** | `boolean` | optional | Show the step indicator (wizard forms) | +| **splitDirection** | `Enum<'horizontal' \| 'vertical'>` | optional | Split orientation (split forms) | +| **splitSize** | `number` | optional | Primary split panel size, % (split forms) | +| **splitResizable** | `boolean` | optional | Whether the split is resizable (split forms) | +| **drawerSide** | `Enum<'top' \| 'bottom' \| 'left' \| 'right'>` | optional | Drawer side (drawer forms) | +| **drawerWidth** | `string` | optional | [DEPRECATED → size buckets] Drawer width, e.g. "480px". A pixel width cannot be chosen without knowing the client viewport — the renderer derives it. | +| **modalSize** | `Enum<'sm' \| 'default' \| 'lg' \| 'xl' \| 'full'>` | optional | Modal size (modal forms) | +| **data** | `{ provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source configuration (defaults to "object" provider) | +| **sections** | `{ name?: string; label?: string \| Record; description?: string; collapsible?: boolean; … }[]` | optional | | +| **groups** | `{ name?: string; label?: string \| Record; description?: string; collapsible?: boolean; … }[]` | optional | [LEGACY ALIAS → `sections`] Accepted for back-compat and folded onto `sections` at parse; `sections` wins when both are present. Prefer `sections`. | +| **subforms** | `{ childObject: string; relationshipField?: string; columns?: any[]; amountField?: string; … }[]` | optional | Inline master-detail child collections | +| **defaultSort** | `never` | optional | [REMOVED] `form.defaultSort` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — nothing read it: a related list inside a form sorts by its own list view's `sort`. Delete the key and set the sort on the related list view instead. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **sharing** | `{ enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … }` | optional | Public sharing configuration for this form | +| **submitBehavior** | `{ kind: 'thank-you'; title?: string; message?: string } \| { kind: 'redirect'; url: string; delayMs?: integer } \| { kind: 'continue' } \| { kind: 'next-record' }` | optional | Post-submit behavior. On the `redirect` arm, `url` is relative-only and interpolates only declared record fields as `{{record.field_name}}`, URL-escaped (ruled 2026-08-11, #7496). | +| **buttons** | `{ submit?: object; cancel?: object; reset?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm (framework#1894 / #2998). | +| **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: `ViewItem.protection` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | ✅ | Lock policy — none \| no-overlay \| no-delete \| full. | +| **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | +| **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | + --- @@ -1005,6 +2067,75 @@ This schema accepts one of the following structures: | **sortOrder** | `integer` | optional | Studio round-trip: position within the switcher (per-user state, written by the console — not authored). | | **columnState** | `{ order?: string[]; widths?: Record }` | optional | Studio round-trip: per-user column order/widths (runtime-only state, written by the console grid — not authored). #9933 | +### Nested Shape: `ViewItemWire.config` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional | Internal view name (lowercase snake_case) | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **type** | `Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>` | optional (default: `"grid"`) | | +| **data** | `{ provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source configuration (defaults to "object" provider) | +| **columns** | `string[] \| { field: string; label?: string \| Record; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | ✅ | Fields to display as columns | +| **filter** | `{ field: string; operator?: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Filter criteria (JSON Rules) | +| **sort** | `string \| { field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | | +| **searchableFields** | `string[]` | optional | Fields enabled for search | +| **filterableFields** | `string[]` | optional | Legacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters | +| **userFilters** | `{ element?: Enum<'dropdown' \| 'tabs' \| 'toggle'>; fields?: object[]; tabs?: object[]; showAllRecords?: boolean; … }` | optional | End-user quick-filter bar: dropdown/toggle fields or tab presets. Omit to let the renderer derive filters from select/boolean fields | +| **resizable** | `boolean` | optional | Enable column resizing | +| **compactToolbar** | `boolean` | optional | Collapse Group/Color/Density/Hide-fields into a single View settings popover | +| **selection** | `{ type?: Enum<'none' \| 'single' \| 'multiple'> }` | optional | Row selection configuration | +| **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; view?: string; preventNavigation?: boolean; openNewTab?: boolean; … }` | optional | Configuration for item click navigation (page, drawer, modal, etc.) | +| **pagination** | `{ pageSize?: integer; pageSizeOptions?: integer[] }` | optional | Pagination configuration | +| **kanban** | `{ groupByField: string; summarizeField?: string; columns: string[] }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout | +| **calendar** | `{ startDateField: string; endDateField?: string; titleField: string; colorField?: string }` | optional | Calendar configuration — applies when the view renders as a calendar layout | +| **gantt** | `{ startDateField: string; endDateField: string; titleField: string; progressField?: string; … } & Record` | optional | Gantt-timeline configuration — applies when the view renders as a gantt layout | +| **gallery** | `{ coverField?: string; coverFit?: Enum<'cover' \| 'contain'>; cardSize?: Enum<'small' \| 'medium' \| 'large'>; titleField?: string; … }` | optional | Gallery/card view configuration | +| **timeline** | `{ startDateField: string; endDateField?: string; titleField: string; groupByField?: string; … }` | optional | Timeline view configuration | +| **chart** | `{ chartType?: Enum<'bar' \| 'line' \| 'pie' \| 'area' \| 'scatter'>; dataset: string; dimensions?: string[]; values: string[] }` | optional | List chart view configuration | +| **map** | `{ latitudeField?: string; longitudeField?: string; locationField?: string; titleField?: string; … }` | optional | Map configuration — applies when the view renders as a map layout | +| **tree** | `{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer } & Record` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout | +| **description** | `string \| Record` | optional | View description for documentation/tooltips | +| **sharing** | `{ type?: Enum<'personal' \| 'collaborative'>; lockedBy?: string }` | optional | View sharing and access configuration | +| **rowHeight** | `Enum<'compact' \| 'short' \| 'medium' \| 'tall' \| 'extra_tall'>` | optional | Row height / density setting | +| **grouping** | `{ fields: object[] }` | optional | Group records by one or more fields | +| **rowColor** | `{ field: string; colors?: Record }` | optional | Color rows based on field value | +| **hiddenFields** | `string[]` | optional | Fields to hide in this specific view | +| **fieldOrder** | `string[]` | optional | Explicit field display order for this view | +| **rowActions** | `string[]` | optional | Actions available for individual row items | +| **bulkActions** | `string[]` | optional | Actions available when multiple rows are selected | +| **bulkActionDefs** | `{ name: string; label?: string; icon?: string; variant?: Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'outline'>; … }[]` | optional | Rich bulk action definitions (schema-driven, executed via BulkActionDialog). Use a def for a mass data-plane mutation ('update' with a `patch` / 'delete') that no action expresses, or for an `operation: 'custom'` + `execution: 'aggregate'` entry (objectui#3139) that dispatches the action it NAMES once for the whole selection — the renderer injects `params._selectedIds: string[]` (read that on the server, not `recordId`) so a single call can produce one aggregate artifact (zip of QR codes, merged PDF, batch print). Aggregate results are all-or-nothing: a handler that cannot cover the whole selection must reject, and per-row retry is replaced by re-running the action. `batchSize` does not apply (the call is never chunked); set `maxRecords` on defs whose server work is expensive. For the PER-RECORD dispatch use `bulkActions: ['']` instead — the bare-string form, promoted with the action's own label, params and `visible`; a 'custom' def without `execution: 'aggregate'` has no dispatcher and is refused at parse time (#4457). Toolbar url/api actions can also interpolate the current selection via `${ctx.selection.ids}` / `${ctx.selection.count}`. | +| **conditionalFormatting** | `{ condition: string \| object; style: Record }[]` | optional | Conditional formatting rules for list rows | +| **inlineEdit** | `boolean` | optional | Allow inline editing of records directly in the list view | +| **exportOptions** | `Enum<'csv' \| 'xlsx' \| 'json'>[] \| { formats?: Enum<'csv' \| 'xlsx' \| 'json'>[]; maxRecords?: integer; includeHeaders?: boolean; fileNamePrefix?: string; … }` | optional | Export configuration for the list toolbar export menu: `{ formats?, maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }`. A bare format array is the legacy spelling and lifts to `{ formats: [...] }` at parse. | +| **userActions** | `{ sort?: boolean; search?: boolean; filter?: boolean; refresh?: boolean; … }` | optional | User action toggles for the view toolbar | +| **appearance** | `{ showDescription?: boolean; allowedVisualizations?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>[] }` | optional | Appearance and visualization configuration | +| **tabs** | `{ name: string; label?: string \| Record; icon?: string; view?: string; … }[]` | optional | Tab definitions for multi-tab view interface | +| **addRecord** | `{ enabled?: boolean; position?: Enum<'top' \| 'bottom' \| 'both'>; mode?: Enum<'inline' \| 'form' \| 'modal'>; formView?: string }` | optional | Add record entry point configuration | +| **showRecordCount** | `boolean` | optional | Show record count at the bottom of the list | +| **allowPrinting** | `boolean` | optional | Allow users to print the view | +| **emptyState** | `{ title?: string \| Record; message?: string \| Record; icon?: string }` | optional | Empty state configuration when no records found | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes for the list view | +| **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **striped** | `never` | optional | [REMOVED] `view.striped` 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, so authoring it was a parse-clean no-op. There is no authorable striped-rows switch; delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **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: `ViewItemWire.protection` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | ✅ | Lock policy — none \| no-overlay \| no-delete \| full. | +| **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | +| **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | + +### Nested Shape: `ViewItemWire.columnState` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **order** | `string[]` | optional | Column order as field names, leftmost first (runtime-only per-user state — written by the console grid, never authored). | +| **widths** | `Record` | optional | Column widths in pixels, keyed by field name (runtime-only per-user state — written by the console grid, never authored). | + --- #### Option 2 @@ -1035,6 +2166,51 @@ This schema accepts one of the following structures: | **sortOrder** | `integer` | optional | Studio round-trip: position within the switcher (per-user state, written by the console — not authored). | | **columnState** | `{ order?: string[]; widths?: Record }` | optional | Studio round-trip: per-user column order/widths (runtime-only state, written by the console grid — not authored). #9933 | +### Nested Shape: `ViewItemWire.config` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'simple' \| 'tabbed' \| 'wizard' \| 'split' \| 'drawer' \| 'modal'>` | optional (default: `"simple"`) | | +| **layout** | `Enum<'vertical' \| 'horizontal' \| 'inline' \| 'grid'>` | optional | Field layout direction | +| **columns** | `integer` | optional | Number of columns for the form body | +| **title** | `string` | optional | Form title | +| **description** | `string` | optional | Form description | +| **defaultTab** | `string` | optional | Initially active tab (tabbed forms) | +| **tabPosition** | `Enum<'top' \| 'bottom' \| 'left' \| 'right'>` | optional | Tab strip position (tabbed forms) | +| **allowSkip** | `boolean` | optional | Allow skipping steps (wizard forms) | +| **showStepIndicator** | `boolean` | optional | Show the step indicator (wizard forms) | +| **splitDirection** | `Enum<'horizontal' \| 'vertical'>` | optional | Split orientation (split forms) | +| **splitSize** | `number` | optional | Primary split panel size, % (split forms) | +| **splitResizable** | `boolean` | optional | Whether the split is resizable (split forms) | +| **drawerSide** | `Enum<'top' \| 'bottom' \| 'left' \| 'right'>` | optional | Drawer side (drawer forms) | +| **drawerWidth** | `string` | optional | [DEPRECATED → size buckets] Drawer width, e.g. "480px". A pixel width cannot be chosen without knowing the client viewport — the renderer derives it. | +| **modalSize** | `Enum<'sm' \| 'default' \| 'lg' \| 'xl' \| 'full'>` | optional | Modal size (modal forms) | +| **data** | `{ provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source configuration (defaults to "object" provider) | +| **sections** | `{ name?: string; label?: string \| Record; description?: string; collapsible?: boolean; … }[]` | optional | | +| **groups** | `{ name?: string; label?: string \| Record; description?: string; collapsible?: boolean; … }[]` | optional | [LEGACY ALIAS → `sections`] Accepted for back-compat and folded onto `sections` at parse; `sections` wins when both are present. Prefer `sections`. | +| **subforms** | `{ childObject: string; relationshipField?: string; columns?: any[]; amountField?: string; … }[]` | optional | Inline master-detail child collections | +| **defaultSort** | `never` | optional | [REMOVED] `form.defaultSort` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — nothing read it: a related list inside a form sorts by its own list view's `sort`. Delete the key and set the sort on the related list view instead. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **sharing** | `{ enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … }` | optional | Public sharing configuration for this form | +| **submitBehavior** | `{ kind: 'thank-you'; title?: string; message?: string } \| { kind: 'redirect'; url: string; delayMs?: integer } \| { kind: 'continue' } \| { kind: 'next-record' }` | optional | Post-submit behavior. On the `redirect` arm, `url` is relative-only and interpolates only declared record fields as `{{record.field_name}}`, URL-escaped (ruled 2026-08-11, #7496). | +| **buttons** | `{ submit?: object; cancel?: object; reset?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm (framework#1894 / #2998). | +| **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: `ViewItemWire.protection` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | ✅ | Lock policy — none \| no-overlay \| no-delete \| full. | +| **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | +| **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | + +### Nested Shape: `ViewItemWire.columnState` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **order** | `string[]` | optional | Column order as field names, leftmost first (runtime-only per-user state — written by the console grid, never authored). | +| **widths** | `Record` | optional | Column widths in pixels, keyed by field name (runtime-only per-user state — written by the console grid, never authored). | + --- @@ -1097,6 +2273,16 @@ Tab configuration for multi-tab view interface | **isDefault** | `boolean` | optional (default: `false`) | Set as the default active tab | | **visible** | `boolean` | optional (default: `true`) | Tab visibility | +### Nested Shape: `ViewTab.filter[number]` + +View filter rule + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name to filter on | +| **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>` | ✅ | Filter operator | +| **value** | `string \| number \| boolean \| null \| (string \| number)[]` | optional | Filter value. The accepted SHAPE depends on the operator: `in` / `not_in` take an array (any length, including []), `between` takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. | + --- diff --git a/packages/spec/scripts/lib/format-type.ts b/packages/spec/scripts/lib/format-type.ts index 0d52d07275..499dd2aa18 100644 --- a/packages/spec/scripts/lib/format-type.ts +++ b/packages/spec/scripts/lib/format-type.ts @@ -558,6 +558,135 @@ export function formatPropertyType(prop: any, ctx?: TypeContext): RenderedProper return { cell: formatType(prop, ctx), allowedValues: null }; } +/** + * The single `{ … }` shape a property's cell opens, and the TypeScript indexed + * accessor that selects it out of the property's type. + * + * `node` is the JSON Schema object node itself, so a caller can render its keys + * with the same table code the enclosing section already uses. + */ +export interface NestedShape { + /** The object node whose keys the cell summarizes into `{ … }`. */ + node: any; + /** + * `''` for a property whose own type IS the shape, `'[number]'` for an array + * element, `'[string]'` for a `Record` value — composed left to right for a + * wrapper stack (`'[string][number]'`). + * + * These are real TypeScript indexed-access spellings, not a notation invented + * 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`. + */ + accessor: string; + /** Live (non-tombstone) key names, in declaration order. */ + keys: string[]; +} + +/** + * The ONE shape a property opens at depth 0 — or `null` when it opens none, or + * more than one (#11601). + * + * ## What this is for + * + * `renderType` spends `SHAPE_DEPTH_LIMIT` on the first object it meets and + * prints `{ label: string; icon?: string; … }` — a summary with **no + * description column**. Every `.describe()` an author wrote on a key of that + * shape is therefore unreachable from the page: not truncated, not marked, just + * absent, and `check:docs` is green because generated and committed output + * agree about text neither contains. `page:tabs`'s item-level `visibleWhen` + * carries a 600-character contract note — including a ⚠️ that its evaluation + * environment is NOT the page-component `visibleWhen` of the same name — and + * `content/docs/references/ui/component.mdx` rendered it as an empty cell. + * + * This function names the node a caller can put under its own table, so the + * description column exists exactly once for those keys. + * + * ## Why it lives beside `renderType` rather than in the section renderer + * + * The two must agree, key for key, about what "one shape level" means: the + * table below a cell has to be the shape that cell summarized, or the page + * documents something it did not print. Wrappers do not spend the budget and + * shapes do — the accounting in the object branch above — so an array of + * objects, a `Record` of objects and a `string | { … }` union all open exactly + * 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` + * + * `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. + * + * 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 + * and their descriptions are already published in full. (Zod inlines + * everything today, so every ref in the emitted tree is anonymous or self — + * this is the branch that keeps the rule true if that changes.) + */ +export function nestedShapeOf(prop: any, ctx?: TypeContext): NestedShape | null { + const found: NestedShape[] = []; + + 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; + // A tombstone accepts nothing; there is no shape under it. + if (isNeverNode(node)) return; + + if (node.$ref) { + // Both spellings name a schema documented in its own section. + if (node.$ref === '#') return; + const name = refName(node.$ref); + if (!isAnonymousRef(name)) return; + if (expanding.has(name)) return; + const target = ctx?.defs?.[name]; + if (!target) return; + const next = new Set(expanding); + next.add(name); + walk({ ...target, $ref: undefined }, next, accessor, guard + 1); + return; + } + + // 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.type === 'array') { + walk(node.items, expanding, `${accessor}[number]`, guard + 1); + return; + } + + 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; + } + + 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 (node.additionalProperties && typeof node.additionalProperties === 'object') { + walk(node.additionalProperties, expanding, `${accessor}[string]`, guard + 1); + } + } + }; + + walk(prop, new Set(ctx?.expanding ?? []), '', 0); + return found.length === 1 ? found[0] : null; +} + /** * `depth` is the count of `{ … }` shape levels already OPEN above this node, * and it is a parameter rather than a `TypeContext` field on purpose: a caller diff --git a/packages/spec/scripts/lib/schema-section.ts b/packages/spec/scripts/lib/schema-section.ts index f65aca0f2b..a7872299cf 100644 --- a/packages/spec/scripts/lib/schema-section.ts +++ b/packages/spec/scripts/lib/schema-section.ts @@ -13,7 +13,13 @@ */ import { escapeMdxDescription } from './escape-mdx'; -import { formatPropertyType, formatType, type TypeContext } from './format-type'; +import { + formatPropertyType, + formatType, + nestedShapeOf, + type NestedShape, + type TypeContext, +} from './format-type'; /** What a section needs from the generator that a unit test can supply. */ export interface SectionContext { @@ -197,6 +203,30 @@ export function renderRequiredCell(prop: any, required: boolean): string { return `optional (default: \`${value}\`)`; } +/** + * Does this nested shape carry `.describe()` text a table could publish and a + * `{ … }` cell cannot (#11601)? + * + * The test is on the shape's OWN keys, one level, matching what the table under + * the heading will contain — never a deep walk. A deep walk would answer "yes" + * for a shape whose own keys are all undescribed and whose grandchildren carry + * prose, and then emit a table that publishes none of it: a section that exists + * because of text it does not contain. + * + * Tombstoned keys count. `retiredKey()` puts the whole `[REMOVED]` migration + * prescription in `description`, and `format-type.ts` drops tombstones from the + * `{ … }` summary precisely because a summary has no column to carry it — so a + * shape whose only described key is a tombstone is a shape whose only + * documentation is currently unreachable, which is this fix's case exactly. + */ +function carriesDescription(shape: NestedShape): boolean { + const props = shape.node?.properties; + if (!props || typeof props !== 'object') return false; + return Object.values(props).some( + (child: any) => typeof child?.description === 'string' && child.description.trim() !== '', + ); +} + /** * Render one schema's section, heading included. * @@ -224,19 +254,76 @@ export function renderSchemaSection(schemaName: string, schema: any, ctx: Sectio const typeCtx: TypeContext = { defs, currentSchema: schemaName, schemaHref: ctx.schemaHref }; - const renderProperties = (props: any, required: Set = new Set()) => { + const renderProperties = ( + props: any, + required: Set = new Set(), + heading = '### Properties', + // A nested-shape table does not open tables of its own. ONE level, matched + // to the ONE shape level `SHAPE_DEPTH_LIMIT` lets a cell open: the table + // documents exactly what the cell above it summarized, and a page's depth + // stays a fact about the renderer rather than about how deeply an author + // happened to nest a schema. + // + // It also turns the cells into SUMMARY cells — `INLINE_ENUM_WIDTH_LIMIT` + // with the unquantified `…`, and no `### Allowed Values` relocation. That + // is the same flag `format-type.ts` sets below a `{ … }`, and it must be + // set here for the same reason: this table is a SECOND position for those + // keys, and #6225's relocation budget is only spendable where the + // vocabulary's authoritative copy lives. Measured by regenerating without + // it: the 288-member `ApiError.code` vocabulary relocated into a bullet + // list under every nested `error` shape — **20,260 bullet lines across the + // tree**, `api/metadata.mdx` alone +6097 — for a vocabulary already + // published in full on `api/errors.mdx` and in `json-schema/`. #9182 took + // the COUNT out of this position for a smaller version of the same cost; + // taking the whole list out of it is the same decision. + expandNested = true, + ) => { // Vocabularies too wide for their own table cell. Collected while the // table is built and printed as `### Allowed Values` bullets right after // it, so the complete list never leaves the page the cell sits on // (#6225) — the same rendering a hoisted `type: 'string'` + `enum` schema // has always got, now reachable from a property position too. const relocated: Array<{ key: string; members: string[] }> = []; - let t = `### Properties\n\n`; + // Shapes whose keys' `.describe()` text the cell above cannot carry at + // all — there is no description column inside `{ … }` (#11601). + const nested: Array<{ path: string; ownDescription: string; shape: NestedShape }> = []; + // Empty for a nested-shape table: its `### Nested Shape: \`path\`` heading + // IS its heading, and a `#### Properties` under every one of them would + // put ~1200 identically-titled headings into the tree for no reader. + let t = heading ? `${heading}\n\n` : ''; t += `| Property | Type | Required | Description |\n`; t += `| :--- | :--- | :--- | :--- |\n`; for (const [key, prop] of Object.entries(props) as [string, any][]) { - const { cell, allowedValues } = formatPropertyType(prop, typeCtx); + const { cell, allowedValues } = expandNested + ? formatPropertyType(prop, typeCtx) + : { 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)) { + const own = typeof shape.node.description === 'string' ? shape.node.description : ''; + nested.push({ + // Qualified by schema AND property, for the reason the + // `### Allowed Values` headings below are: one page carries + // many schemas, and a heading naming only the property would + // 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. + 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. + ownDescription: own && own !== prop.description ? own : '', + shape, + }); + } + } // Backslashes first, then pipes — same order as `desc` below, and for // the same reason: escaping pipes first lets a literal backslash in // the input pair with the escape and free the pipe again. @@ -262,6 +349,28 @@ export function renderSchemaSection(schemaName: string, schema: any, ctx: Sectio t += members.map(m => `* \`${m}\``).join('\n'); t += `\n\n`; } + // The relocations above, for shapes. Same position (immediately under the + // table whose cells they complete), same addressing (`Schema.key…`), same + // heading level — one page grammar, not a second one. The heading names + // the shape with a TypeScript indexed accessor (`items[number]`), so it + // states WHICH shape without inventing a sigil for the table. + for (const { path, ownDescription, shape } of nested) { + t += `### Nested Shape: \`${path}\`\n\n`; + if (ownDescription) { + t += `${escapeMdxDescription(ownDescription.replace(/\n/g, ' '))}\n\n`; + } + // Tombstoned keys are rendered here, unlike in the cell above: the + // `[REMOVED]` prescription needs a description column, and a summary + // has none — which is exactly why `format-type.ts` drops them from + // `{ … }` and says a named schema's own row is where they survive. + // This IS that row, for a shape that never had one. + t += renderProperties( + shape.node.properties, + new Set(shape.node.required || []), + '', + false, + ); + } return t; }; diff --git a/packages/spec/scripts/nested-shape.test.ts b/packages/spec/scripts/nested-shape.test.ts new file mode 100644 index 0000000000..809decf110 --- /dev/null +++ b/packages/spec/scripts/nested-shape.test.ts @@ -0,0 +1,353 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Pin for NESTED SHAPE rendering — the `### Nested Shape: \`Schema.key…\`` table + * that carries an item-level key's `.describe()` text to the reference page + * (#11601). + * + * THE DEFECT THIS PINS. `renderType` spends `SHAPE_DEPTH_LIMIT` on the first + * object it meets and prints a signature — `{ label: string; icon?: string; + * visibleWhen?: string | object; value?: string; … }[]` — into a table cell + * that has **no description column**. Every `.describe()` on a key of that + * shape was therefore unreachable from the page. Not truncated, not marked: + * absent. `page:tabs`'s item-level `visibleWhen` carries a ~600-character + * contract note whose whole point is that its evaluation environment is NOT the + * page-component `visibleWhen` of the same name, and + * `content/docs/references/ui/component.mdx:459` rendered the row with an empty + * Description cell. + * + * WHY A UNIT PIN AND NOT A GREP OVER THE EMITTED TREE. The defect's output is + * ABSENT TEXT, and `check:docs` compares generated output with committed + * output — so it is green forever on prose that neither side contains, and a + * grep for what is missing has nothing to match. Measured on the tree at the + * time of the fix: adding a `.describe()` to a nested item key produced a **zero + * line** `gen:docs` diff. The same reason `schema-section.test.ts` and + * `format-type.test.ts` exist, and the same shape as the #11482 / #11260 pins — + * assert the extracted pure function, not the artifact. + * + * MEASURED (reverse verification): reverting `nestedShapeOf` to `() => null` + * — i.e. restoring the old behaviour with every other line of the fix in place + * — 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. + */ + +import { describe, expect, it } from 'vitest'; + +import { formatType, nestedShapeOf, type TypeContext } from './lib/format-type'; +import { renderSchemaSection } from './lib/schema-section'; + +/** + * The card's own specimen, reduced: `page:tabs` items as `gen:schema` emits + * them. `visibleWhen` keeps its real describe text — this is the string the + * acceptance is stated on. + */ +const VISIBLE_WHEN_DESCRIBE = + 'Visibility predicate (CEL) — the whole tab (header + panel) is omitted when FALSE. ' + + 'NOT the same environment as page-component visibleWhen.'; + +const PAGE_TABS_PROPS = { + type: 'object', + properties: { + tabStyle: { type: 'string', enum: ['line', 'card', 'pill'], description: 'Tab-strip visual style' }, + items: { + type: 'array', + items: { + type: 'object', + properties: { + label: { type: 'string', description: 'Display label' }, + icon: { type: 'string', description: 'Lucide icon name' }, + visibleWhen: { type: 'string', description: VISIBLE_WHEN_DESCRIBE }, + value: { type: 'string', description: 'Stable `?tab=` URL token' }, + count: { type: 'integer', description: 'Badge count' }, + children: { type: 'array', items: {}, description: 'Child components' }, + }, + required: ['label', 'children'], + additionalProperties: false, + }, + }, + }, + required: ['items'], +}; + +describe('nestedShapeOf — which shape a cell opens, and how it is addressed', () => { + it('names an array element with the TypeScript index accessor', () => { + const shape = nestedShapeOf(PAGE_TABS_PROPS.properties.items); + + expect(shape).not.toBeNull(); + expect(shape!.accessor).toBe('[number]'); + expect(shape!.keys).toEqual(['label', 'icon', 'visibleWhen', 'value', 'count', 'children']); + }); + + it('names a property whose own type IS the shape with an empty accessor', () => { + const shape = nestedShapeOf({ + type: 'object', + properties: { role: { type: 'string', description: 'WAI-ARIA role' } }, + }); + + expect(shape!.accessor).toBe(''); + expect(shape!.keys).toEqual(['role']); + }); + + it('names a Record value with the string index accessor', () => { + const shape = nestedShapeOf({ + type: 'object', + additionalProperties: { + type: 'object', + properties: { type: { type: 'string', description: 'Field type' } }, + }, + }); + + expect(shape!.accessor).toBe('[string]'); + expect(shape!.keys).toEqual(['type']); + }); + + it('composes accessors left to right through a wrapper stack', () => { + const shape = nestedShapeOf({ + type: 'array', + items: { + type: 'object', + additionalProperties: { + type: 'object', + properties: { a: { type: 'string', description: 'a' } }, + }, + }, + }); + + expect(shape!.accessor).toBe('[number][string]'); + }); + + it('reaches the ONE shape of a `string | { … }` union', () => { + const shape = nestedShapeOf({ + anyOf: [ + { type: 'string' }, + { type: 'object', properties: { dialect: { type: 'string', description: 'Expression dialect' } } }, + ], + }); + + expect(shape!.accessor).toBe(''); + expect(shape!.keys).toEqual(['dialect']); + }); + + it('refuses a union of TWO object shapes — there is no single shape to name', () => { + const shape = nestedShapeOf({ + anyOf: [ + { type: 'object', properties: { id: { type: 'string', description: 'Item id' } } }, + { type: 'object', properties: { type: { type: 'string', const: 'separator' } } }, + ], + }); + + expect(shape).toBeNull(); + }); + + it('refuses a leaf — scalar, vocabulary, literal, array of scalars', () => { + expect(nestedShapeOf({ type: 'string' })).toBeNull(); + expect(nestedShapeOf({ type: 'string', enum: ['a', 'b'] })).toBeNull(); + expect(nestedShapeOf({ type: 'string', const: 'grid' })).toBeNull(); + expect(nestedShapeOf({ type: 'array', items: { type: 'string' } })).toBeNull(); + }); + + it('refuses a `retiredKey()` tombstone — nothing validates against it', () => { + expect(nestedShapeOf({ not: {} })).toBeNull(); + }); + + it('refuses a shape whose every declared key is a tombstone, and falls through to its Record tail', () => { + // No LIVE key: `format-type.ts` renders this as `Record<…>`, not a shape, + // so the accessor must record the descent rather than stop here. + const shape = nestedShapeOf({ + type: 'object', + properties: { removed: { not: {} } }, + additionalProperties: { type: 'object', properties: { live: { type: 'string', description: 'live' } } }, + }); + + expect(shape!.accessor).toBe('[string]'); + expect(shape!.keys).toEqual(['live']); + }); + + it('expands an anonymous `$defs` ref, the way the type renderer does', () => { + const ctx: TypeContext = { + defs: { + __schema7: { type: 'object', properties: { source: { type: 'string', description: 'CEL source' } } }, + }, + currentSchema: 'Anything', + }; + + const shape = nestedShapeOf({ type: 'array', items: { $ref: '#/$defs/__schema7' } }, ctx); + + expect(shape!.accessor).toBe('[number]'); + expect(shape!.keys).toEqual(['source']); + }); + + it('refuses a NAMED ref and a self ref — both are documented in their own section', () => { + const ctx: TypeContext = { defs: {}, currentSchema: 'View' }; + + expect(nestedShapeOf({ $ref: '#/$defs/Field' }, ctx)).toBeNull(); + expect(nestedShapeOf({ $ref: '#' }, ctx)).toBeNull(); + }); + + it('refuses a cycle rather than recursing — schemas are self-referential', () => { + const ctx: TypeContext = { + defs: { __schema1: { type: 'array', items: { $ref: '#/$defs/__schema1' } } }, + currentSchema: 'Node', + }; + + expect(nestedShapeOf({ $ref: '#/$defs/__schema1' }, ctx)).toBeNull(); + }); +}); + +describe('renderSchemaSection — the item-level describe reaches the page (#11601)', () => { + const md = renderSchemaSection('PageTabsProps', PAGE_TABS_PROPS); + + it('THE ACCEPTANCE: the measured `visibleWhen` describe is on the page', () => { + expect(md).toContain(VISIBLE_WHEN_DESCRIBE); + }); + + it('carries it under a heading naming the shape by its accessor path', () => { + expect(md).toContain('### Nested Shape: `PageTabsProps.items[number]`'); + }); + + it('leaves the collapsed signature cell exactly where it was', () => { + // The summary is not replaced by the table — a reader scanning the property + // list still sees the shape in one line, and the table completes it. + expect(md).toContain('| **items** | `{ label: string; icon?: string; visibleWhen?: string; value?: string; … }[]` | ✅ | |'); + }); + + it('gives every nested key a row, elided keys included', () => { + // The cell shows four keys and a `…`; the table shows all six. That gap is + // `INLINE_KEY_LIMIT`, and the table is where it stops costing the reader. + for (const key of ['label', 'icon', 'visibleWhen', 'value', 'count', 'children']) { + expect(md).toContain(`| **${key}** |`); + } + }); + + it('does not open a table for a shape whose keys carry no describe text', () => { + // Nothing to relocate: the cell already states the keys and their types, so + // a table would restate it in more space. + const undescribed = renderSchemaSection('Bare', { + type: 'object', + properties: { + point: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'number' } } }, + }, + }); + + expect(undescribed).toContain('| **point** |'); + expect(undescribed).not.toContain('### Nested Shape:'); + }); + + it('opens a table when the ONLY described key is a tombstone', () => { + // `retiredKey()` puts the whole `[REMOVED]` migration prescription in + // `description`, and `format-type.ts` drops tombstones from `{ … }` + // precisely because a summary has no column to carry it. This table is the + // row that shape never had. + const retired = renderSchemaSection('WithTombstone', { + type: 'object', + properties: { + legacy: { + type: 'array', + items: { + type: 'object', + properties: { + live: { type: 'string' }, + gone: { not: {}, description: '[REMOVED] renamed to `live` in 17.0.0' }, + }, + }, + }, + }, + }); + + expect(retired).toContain('### Nested Shape: `WithTombstone.legacy[number]`'); + expect(retired).toContain('[REMOVED] renamed to `live` in 17.0.0'); + }); + + it('does not relocate a vocabulary out of a nested table', () => { + // A nested table is a SECOND position for those keys, so it elides the way + // a `{ … }` summary does — bare `…`, no `### Allowed Values` bullets. + // Regenerating without this rule put 20,260 bullet lines into the tree. + const wide = Array.from({ length: 60 }, (_, i) => `code_${i}`); + const withVocabulary = renderSchemaSection('Envelope', { + type: 'object', + properties: { + error: { + type: 'object', + properties: { + code: { type: 'string', enum: wide, description: 'Machine-readable error code' }, + }, + }, + }, + }); + + expect(withVocabulary).toContain('### Nested Shape: `Envelope.error`'); + expect(withVocabulary).toContain('Machine-readable error code'); + expect(withVocabulary).not.toContain('### Allowed Values:'); + expect(withVocabulary).toContain('…'); + }); + + it('stops at ONE level — a nested table opens no table of its own', () => { + const deep = renderSchemaSection('Deep', { + type: 'object', + properties: { + outer: { + type: 'array', + items: { + type: 'object', + properties: { + inner: { + type: 'object', + description: 'one level down — this one reaches the page', + properties: { leaf: { type: 'string', description: 'two levels down' } }, + }, + }, + }, + }, + }, + }); + + // The one level that exists, and no second one. `leaf`'s describe stays + // unreachable, exactly as `SHAPE_DEPTH_LIMIT` leaves it unreachable in a + // cell — the budget is the renderer's, not the author's nesting depth. + expect(deep.match(/^### Nested Shape: /gm) ?? []).toHaveLength(1); + expect(deep).toContain('### Nested Shape: `Deep.outer[number]`'); + expect(deep).not.toContain('### Nested Shape: `Deep.outer[number].inner`'); + expect(deep).not.toContain('two levels down'); + }); + + it('is purely ADDITIVE — every line the old renderer emitted is still emitted', () => { + // The property table, its cells and the `## heading` are untouched; the + // section only grows. Measured on the whole tree at the time of the fix: + // regenerating moved 143 files, +14195 / -118 lines, and every one of the + // 38177 pre-existing lines is still present (the 118 are re-ordering, not + // removal). + for (const line of [ + '## PageTabsProps', + '### Properties', + '| Property | Type | Required | Description |', + "| **tabStyle** | `Enum<'line' \\| 'card' \\| 'pill'>` | optional | Tab-strip visual style |", + ]) { + expect(md).toContain(line); + } + }); +}); + +describe('nestedShapeOf agrees with the cell it completes', () => { + it('opens a shape exactly when the cell prints one', () => { + // The contract between the two functions: a table under a cell must be the + // shape THAT cell summarized. Where `formatType` prints `{ … }` at the top + // level, `nestedShapeOf` must have a shape — and where it prints a leaf, + // there must be none. + const opensAShape = [ + PAGE_TABS_PROPS.properties.items, + { type: 'object', properties: { a: { type: 'string' } } }, + { type: 'object', additionalProperties: { type: 'object', properties: { a: { type: 'string' } } } }, + ]; + for (const prop of opensAShape) { + expect(formatType(prop)).toContain('{ '); + expect(nestedShapeOf(prop)).not.toBeNull(); + } + + const opensNoShape = [{ type: 'string' }, { type: 'number' }, { not: {} }, { type: 'array', items: { type: 'string' } }]; + for (const prop of opensNoShape) { + expect(formatType(prop)).not.toContain('{ '); + expect(nestedShapeOf(prop)).toBeNull(); + } + }); +});