diff --git a/.changeset/9591-retirement-sentence-write.md b/.changeset/9591-retirement-sentence-write.md new file mode 100644 index 00000000000..549cabff448 --- /dev/null +++ b/.changeset/9591-retirement-sentence-write.md @@ -0,0 +1,17 @@ +--- +'@objectstack/spec': patch +'@objectstack/lint': patch +'@objectstack/driver-turso': patch +--- + +Retirement prescriptions name `os migrate meta --write`: "… to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand." + +Clause-②: no + +Wording only: no schema, key, type, export or error code changes, and every input that parsed or was refused before gets the same verdict at the same path. Only the closing sentence of the message changes. + +- A prescription covered by an ADR-0087 conversion used to close with "Run `os migrate meta --from N` to list the mechanical edits for existing sources; apply them by hand." It now closes with "Run `os migrate meta --from N` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand." The default run still only lists. `os migrate meta --write` rewrites in place each edit it can trace to one literal in one project file and lists every other edit with the reason it was not written. The sentence never says the tool rewrites your sources automatically, because `--write` does not write every edit. Text up to and including "for existing sources;" is unchanged, so a caller matching that prefix still matches. +- The three prescriptions for conversions that cover only part of a value (dashboard `compareTo.offset`, the script flow node's `config.actionType` and the package manifest `permissions` case) carry the same `--write` clause before their second clause. +- The `record:chatter` / `record:discussion` `position` value prescriptions (`'sidebar'`, `'inline'`, `'drawer'`) used to tell the author to run a bare `os migrate meta`, which the command refuses because `--from` is missing. They now name `os migrate meta --from 17` and close with the same sentence. +- `@objectstack/lint`: the script node's retired dispatch-key diagnostic and the dataset widget's `chartConfig.xAxis.field` hint close with the new sentence. +- `@objectstack/driver-turso`: the `timeout`, `localPath` and `wasm` config tombstones close with the new sentence. diff --git a/.claude/skills/spec-property-retirement/SKILL.md b/.claude/skills/spec-property-retirement/SKILL.md index f27c043adae..7766292e004 100644 --- a/.claude/skills/spec-property-retirement/SKILL.md +++ b/.claude/skills/spec-property-retirement/SKILL.md @@ -151,12 +151,12 @@ ratchet(#2978)会先开火,要求你**有意删除**对应的 manifest key;删 3. 一个破折号从句讲**它为何惰性或错误** —— "it never had an effect"、"no renderer ever read it"。 4. 祈使句修复:改名写 "use ``" + "Rename the key; the value (…) is unchanged.";删除写 "Delete the key." + **真正生效的机制是什么**。 -5. ``Run `os migrate meta --from ` to list the mechanical edits for existing sources; apply them by hand.`` - —— 命令重放链、打印机械修改清单,从不写 source 文件(#9591 的 in-place codemod 落地前恒 - 真)。 +5. ``Run `os migrate meta --from ` to list the mechanical edits for existing sources; + `--write` applies the ones it can prove, and you apply the rest by hand.`` 消息不点名 conversion id;conversion 由 CLI 命令引用。唯一允许的变体(按形状、不按站点): conversion 只覆盖值的一部分时,两从句形点名覆盖的部分 —— ``Run `os migrate meta --from ` - to list the mechanical edits for the case; .`` + to list the mechanical edits for the case; `--write` applies the ones it can prove, and + .`` (样板:`ui/dashboard.zod.ts` `compareTo.offset`)。守这两个形状的 pin 人群含本文件: `packages/spec/src/shared/retired-key-migrate-sentence.test.ts`。 diff --git a/content/docs/references/ai/agent.mdx b/content/docs/references/ai/agent.mdx index 7c12cc50f36..bea57ece8b3 100644 --- a/content/docs/references/ai/agent.mdx +++ b/content/docs/references/ai/agent.mdx @@ -49,11 +49,11 @@ const result = AIModelConfigSchema.parse(data); | **role** | `string` | ✅ | The persona/role (e.g. "Senior Support Engineer") | | **instructions** | `string` | ✅ | System Prompt / Prime Directives | | **model** | `{ provider?: Enum<'openai' \| 'azure_openai' \| 'anthropic' \| 'local'>; model: string; temperature?: number; maxTokens?: number; … }` | optional | | -| **lifecycle** | `never` | optional | [REMOVED] `agent.lifecycle` was removed in @objectstack/spec 17.7.0 (ADR-0049 enforce-or-remove) — no runtime ever read it: no agent moved through a declared state and no transition was ever refused. Delete the key. A phase of a conversation is a skill with its own `instructions` and `tools`, selected by its `triggerConditions` (ADR-0064); multi-step process orchestration is a Flow (ADR-0019); a record's status transitions are a `state_machine` validation rule on the object (ADR-0020). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **lifecycle** | `never` | optional | [REMOVED] `agent.lifecycle` was removed in @objectstack/spec 17.7.0 (ADR-0049 enforce-or-remove) — no runtime ever read it: no agent moved through a declared state and no transition was ever refused. Delete the key. A phase of a conversation is a skill with its own `instructions` and `tools`, selected by its `triggerConditions` (ADR-0064); multi-step process orchestration is a Flow (ADR-0019); a record's status transitions are a `state_machine` validation rule on the object (ADR-0020). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **surface** | `Enum<'ask' \| 'build'>` | optional (default: `"ask"`) | Product surface this agent binds ('ask' \| 'build') — ADR-0063 §1 | | **skills** | `string[]` | optional | Skill names to attach (Agent→Skill→Tool architecture) | -| **tools** | `never` | optional | [REMOVED] `agent.tools` was removed in @objectstack/spec 17 — use `skills`. An agent reaches exactly the tools its surface-compatible skills declare (ADR-0064), so move each reference into a skill: a platform tool by its registered name, or `action_` for one of your own AI-exposed Actions. This is NOT a rename — there is no key the value moves to: the migration DELETES the key and emits a notice naming each tool that was listed, and you re-declare each one in a skill by hand. ADR-0064 itself still reads `Proposed` and is cloud-owned — that scopes its RUNTIME half (tool resolution, which lives in cloud `service-ai`), not this rejection: the authoring invariant binds you here, and ADR-0109 (Accepted — implemented) is the in-repo record that carries it. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | -| **knowledge** | `never` | optional | [REMOVED] `agent.knowledge` was removed in @objectstack/spec 17.0.0 (audit close-out) — declaring knowledge sources/indexes on an agent never scoped retrieval: the `search_knowledge` tool takes `sourceIds` from the LLM's tool-call arguments, not from the agent record. Delete the block. Restrict retrieval at the knowledge-service / source level (per-source permissions), and describe intended grounding in `instructions` so the model asks for the right sources. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **tools** | `never` | optional | [REMOVED] `agent.tools` was removed in @objectstack/spec 17 — use `skills`. An agent reaches exactly the tools its surface-compatible skills declare (ADR-0064), so move each reference into a skill: a platform tool by its registered name, or `action_` for one of your own AI-exposed Actions. This is NOT a rename — there is no key the value moves to: the migration DELETES the key and emits a notice naming each tool that was listed, and you re-declare each one in a skill by hand. ADR-0064 itself still reads `Proposed` and is cloud-owned — that scopes its RUNTIME half (tool resolution, which lives in cloud `service-ai`), not this rejection: the authoring invariant binds you here, and ADR-0109 (Accepted — implemented) is the in-repo record that carries it. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **knowledge** | `never` | optional | [REMOVED] `agent.knowledge` was removed in @objectstack/spec 17.0.0 (audit close-out) — declaring knowledge sources/indexes on an agent never scoped retrieval: the `search_knowledge` tool takes `sourceIds` from the LLM's tool-call arguments, not from the agent record. Delete the block. Restrict retrieval at the knowledge-service / source level (per-source permissions), and describe intended grounding in `instructions` so the model asks for the right sources. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **active** | `boolean` | optional (default: `true`) | | | **access** | `string[]` | optional | Who can chat with this agent | | **permissions** | `string[]` | optional | Required permission-set capabilities | diff --git a/content/docs/references/ai/skill.mdx b/content/docs/references/ai/skill.mdx index d2b0757a8d1..978815485c1 100644 --- a/content/docs/references/ai/skill.mdx +++ b/content/docs/references/ai/skill.mdx @@ -39,7 +39,7 @@ const result = SkillSchema.parse(data); | **surface** | `Enum<'ask' \| 'build' \| 'both'>` | optional (default: `"ask"`) | Agent surface this skill binds to ('ask' \| 'build' \| 'both') — ADR-0063 §3; read by the cloud agent runtime only | | **instructions** | `string` | optional | LLM instructions when skill is active — also served as an MCP prompt | | **tools** | `string[]` | ✅ | Tool names belonging to this skill (supports trailing wildcard, e.g. `action_*`) — bound by the cloud agent runtime only | -| **triggerPhrases** | `never` | optional | [REMOVED] `skill.triggerPhrases` was removed in @objectstack/spec 17.0.0 (audit close-out) — phrases were never matched against the user's message; skill activation is `triggerConditions` (AND of context field/operator/value) intersected with the agent's `skills[]`, plus explicit /skill-name pinning. Delete the key. Put routing intent in `triggerConditions`; describe intent in `description`/`instructions` for the LLM. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **triggerPhrases** | `never` | optional | [REMOVED] `skill.triggerPhrases` was removed in @objectstack/spec 17.0.0 (audit close-out) — phrases were never matched against the user's message; skill activation is `triggerConditions` (AND of context field/operator/value) intersected with the agent's `skills[]`, plus explicit /skill-name pinning. Delete the key. Put routing intent in `triggerConditions`; describe intent in `description`/`instructions` for the LLM. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **triggerConditions** | `{ field: string; operator: Enum<'eq' \| 'neq' \| 'in' \| 'not_in' \| 'contains'>; value: string \| string[] }[]` | optional | Programmatic activation conditions — evaluated by the cloud agent runtime only | | **active** | `boolean` | optional (default: `true`) | Whether the skill is enabled | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this skill. | diff --git a/content/docs/references/api/automation-api.mdx b/content/docs/references/api/automation-api.mdx index 201706f90e5..4189c815a87 100644 --- a/content/docs/references/api/automation-api.mdx +++ b/content/docs/references/api/automation-api.mdx @@ -114,12 +114,12 @@ const result = AutomationApiErrorCode.parse(data); | **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 (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. | +| **template** | `never` | optional | [REMOVED] `flow.template` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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 (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. | +| **active** | `never` | optional | [REMOVED] `flow.active` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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. | @@ -153,7 +153,7 @@ const result = AutomationApiErrorCode.parse(data); | **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 (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. | +| **outputSchema** | `never` | optional | [REMOVED] `flow.nodes[].outputSchema` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | | **waitEventConfig** | `{ eventType: Enum<'timer' \| 'signal' \| 'webhook' \| 'manual' \| 'condition'>; timerDuration?: string; signalName?: string }` | optional | Configuration for wait node event resumption. REQUIRED on a `type: 'wait'` node — the block is the whole contract, and a wait node without one parses into a run that parks forever reporting success. Under `eventType: 'timer'` a `timerDuration` is required too. | | **boundaryConfig** | `{ attachedToNodeId: string; eventType: Enum<'error' \| 'timer' \| 'signal' \| 'cancel'>; interrupting?: boolean; errorCode?: string; … }` | optional | Configuration for boundary events attached to host nodes. REQUIRED on a `type: 'boundary_event'` node — without it the node names neither the activity it watches nor what fires it. | @@ -179,8 +179,8 @@ const result = AutomationApiErrorCode.parse(data); | **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 — 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 (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. | +| **retryDelayMs** | `never` | optional | [REMOVED] `retryDelayMs` was removed in @objectstack/spec 17.0.0 — 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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **fallbackNodeId** | `never` | optional | [REMOVED] `flow.errorHandling.fallbackNodeId` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | ### Nested Shape: `CreateFlowRequest.protection` @@ -238,12 +238,12 @@ const result = AutomationApiErrorCode.parse(data); | **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 (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. | +| **template** | `never` | optional | [REMOVED] `flow.template` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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 (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. | +| **active** | `never` | optional | [REMOVED] `flow.active` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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. | @@ -369,12 +369,12 @@ const result = AutomationApiErrorCode.parse(data); | **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 (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. | +| **template** | `never` | optional | [REMOVED] `flow.template` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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 (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. | +| **active** | `never` | optional | [REMOVED] `flow.active` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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. | @@ -677,12 +677,12 @@ const result = AutomationApiErrorCode.parse(data); | **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 (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. | +| **template** | `never` | optional | [REMOVED] `flow.template` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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 (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. | +| **active** | `never` | optional | [REMOVED] `flow.active` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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. | @@ -742,12 +742,12 @@ const result = AutomationApiErrorCode.parse(data); | **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 (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. | +| **template** | `never` | optional | [REMOVED] `flow.template` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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 (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. | +| **active** | `never` | optional | [REMOVED] `flow.active` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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. | diff --git a/content/docs/references/api/endpoint.mdx b/content/docs/references/api/endpoint.mdx index cd2826a75a1..48dd14dd2d6 100644 --- a/content/docs/references/api/endpoint.mdx +++ b/content/docs/references/api/endpoint.mdx @@ -41,7 +41,7 @@ const result = ApiEndpointSchema.parse(data); | **authRequired** | `boolean` | optional (default: `true`) | Require authentication | | **rateLimit** | `{ enabled?: boolean; windowMs?: integer; maxRequests?: integer }` | optional | Rate limiting policy | | **cacheTtlSeconds** | `number` | optional | Response cache TTL in seconds | -| **cacheTtl** | `never` | optional | [REMOVED] `ApiEndpoint.cacheTtl` was renamed to `cacheTtlSeconds` in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Rename the key to `cacheTtlSeconds`; the value (seconds) is unchanged, and it stays GET-only. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **cacheTtl** | `never` | optional | [REMOVED] `ApiEndpoint.cacheTtl` was renamed to `cacheTtlSeconds` in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Rename the key to `cacheTtlSeconds`; the value (seconds) is unchanged, and it stays GET-only. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **_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). | diff --git a/content/docs/references/api/metadata.mdx b/content/docs/references/api/metadata.mdx index 6aae27d2c2a..a8e855d1901 100644 --- a/content/docs/references/api/metadata.mdx +++ b/content/docs/references/api/metadata.mdx @@ -85,7 +85,7 @@ const result = AppDefinitionResponseSchema.parse(data); | :--- | :--- | :--- | :--- | | **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. | +| **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; `--write` applies the ones it can prove, and you apply the rest 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 | @@ -96,15 +96,15 @@ const result = AppDefinitionResponseSchema.parse(data); | **navigation** | `({ id: string; label?: string \| Record; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| … +8 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 (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. | +| **homePageId** | `never` | optional | [REMOVED] `app.homePageId` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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. Before the executor landed 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 — removed in @objectstack/spec 17: nothing ever read it, and authoring it is now rejected with its own prescription). 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. | +| **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; `--write` applies the ones it can prove, and you apply the rest 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. Before the executor landed 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 — removed in @objectstack/spec 17: nothing ever read it, and authoring it is now rejected with its own prescription). Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest 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; `--write` applies the ones it can prove, and you apply the rest 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; `--write` applies the ones it can prove, and you apply the rest 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; `--write` applies the ones it can prove, and you apply the rest 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. | +| **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; `--write` applies the ones it can prove, and you apply the rest 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. | diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index a90904c3f07..00eceea06a3 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -1110,8 +1110,8 @@ Enable package response | **allowDelete** | `boolean` | optional (default: `false`) | Delete permission | | **allowExport** | `boolean` | optional | 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 via the insert/update owner_id guard] Change record ownership (assign/reassign/disown owner_id) | -| **allowRestore** | `never` | optional | [REMOVED] `objects..allowRestore` was removed in @objectstack/spec 17 (ADR-0049) — the `restore` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing. Delete the key — a dispatched `restore` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **allowPurge** | `never` | optional | [REMOVED] `objects..allowPurge` was removed in @objectstack/spec 17 (ADR-0049) — the `purge` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing (a compliance/GDPR erase the author believed was permission-locked was not — the operation itself does not exist). Delete the key — a dispatched `purge` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **allowRestore** | `never` | optional | [REMOVED] `objects..allowRestore` was removed in @objectstack/spec 17 (ADR-0049) — the `restore` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing. Delete the key — a dispatched `restore` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **allowPurge** | `never` | optional | [REMOVED] `objects..allowPurge` was removed in @objectstack/spec 17 (ADR-0049) — the `purge` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing (a compliance/GDPR erase the author believed was permission-locked was not — the operation itself does not exist). Delete the key — a dispatched `purge` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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. | | **readScope** | `Enum<'own' \| 'own_and_reports' \| 'unit' \| 'unit_and_below' \| 'org'>` | optional | [ADR-0057 D1] Read depth: own\|unit\|unit_and_below\|org | @@ -1493,8 +1493,8 @@ Enable package response | **allowDelete** | `boolean` | optional (default: `false`) | Delete permission | | **allowExport** | `boolean` | optional | 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 via the insert/update owner_id guard] Change record ownership (assign/reassign/disown owner_id) | -| **allowRestore** | `never` | optional | [REMOVED] `objects..allowRestore` was removed in @objectstack/spec 17 (ADR-0049) — the `restore` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing. Delete the key — a dispatched `restore` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **allowPurge** | `never` | optional | [REMOVED] `objects..allowPurge` was removed in @objectstack/spec 17 (ADR-0049) — the `purge` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing (a compliance/GDPR erase the author believed was permission-locked was not — the operation itself does not exist). Delete the key — a dispatched `purge` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **allowRestore** | `never` | optional | [REMOVED] `objects..allowRestore` was removed in @objectstack/spec 17 (ADR-0049) — the `restore` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing. Delete the key — a dispatched `restore` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **allowPurge** | `never` | optional | [REMOVED] `objects..allowPurge` was removed in @objectstack/spec 17 (ADR-0049) — the `purge` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing (a compliance/GDPR erase the author believed was permission-locked was not — the operation itself does not exist). Delete the key — a dispatched `purge` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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. | | **readScope** | `Enum<'own' \| 'own_and_reports' \| 'unit' \| 'unit_and_below' \| 'org'>` | optional | [ADR-0057 D1] Read depth: own\|unit\|unit_and_below\|org | @@ -1692,7 +1692,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own | **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 }` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout | -| **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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 | @@ -1708,17 +1708,17 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own | **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** | `never` | optional | [REMOVED] `view.list.tabs` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's `listViews` instead: the tab's `name` becomes the entry's key, its `label` the entry's `label`, and its `filter` rules join the view's own `filter` on that entry (copy the view's `columns` too); a tab whose `view` already named a list view needs nothing more. Every `listViews` entry renders as a tab in the switcher. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **tabs** | `never` | optional | [REMOVED] `view.list.tabs` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's `listViews` instead: the tab's `name` becomes the entry's key, its `label` the entry's `label`, and its `filter` rules join the view's own `filter` on that entry (copy the view's `columns` too); a tab whose `view` already named a list view needs nothing more. Every `listViews` entry renders as a tab in the switcher. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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 (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 (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 (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 (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 (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. | +| **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **striped** | `never` | optional | [REMOVED] `view.striped` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **bordered** | `never` | optional | [REMOVED] `view.bordered` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **virtualScroll** | `never` | optional | [REMOVED] `view.virtualScroll` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | | **userFilters** | `{ element?: Enum<'dropdown' \| 'toggle'>; fields?: object[] }` | optional | | ### Nested Shape: `GetUiViewResponse.form` @@ -1744,12 +1744,12 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own | **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?: object[]; amountField?: string; … }[]` | optional | Inline master-detail child collections | -| **defaultSort** | `never` | optional | [REMOVED] `form.defaultSort` was removed in @objectstack/spec 17.0.0 (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. | +| **defaultSort** | `never` | optional | [REMOVED] `form.defaultSort` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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). | | **buttons** | `{ submit?: object; cancel?: object; reset?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm. | | **defaults** | `Record` | optional | Initial field values for create-mode forms (folded into ObjectUI ObjectForm initial values;). | -| **aria** | `never` | optional | [REMOVED] `form.aria` was removed in @objectstack/spec 17.0.0 (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. | +| **aria** | `never` | optional | [REMOVED] `form.aria` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | ### Nested Shape: `GetUiViewResponse.listViews[string]` @@ -1777,7 +1777,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own | **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 }` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout | -| **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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 | @@ -1793,17 +1793,17 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own | **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** | `never` | optional | [REMOVED] `view.list.tabs` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's `listViews` instead: the tab's `name` becomes the entry's key, its `label` the entry's `label`, and its `filter` rules join the view's own `filter` on that entry (copy the view's `columns` too); a tab whose `view` already named a list view needs nothing more. Every `listViews` entry renders as a tab in the switcher. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **tabs** | `never` | optional | [REMOVED] `view.list.tabs` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's `listViews` instead: the tab's `name` becomes the entry's key, its `label` the entry's `label`, and its `filter` rules join the view's own `filter` on that entry (copy the view's `columns` too); a tab whose `view` already named a list view needs nothing more. Every `listViews` entry renders as a tab in the switcher. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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 (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 (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 (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 (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 (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. | +| **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **striped** | `never` | optional | [REMOVED] `view.striped` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **bordered** | `never` | optional | [REMOVED] `view.bordered` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **virtualScroll** | `never` | optional | [REMOVED] `view.virtualScroll` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | | **userFilters** | `{ element?: Enum<'dropdown' \| 'toggle'>; fields?: object[] }` | optional | | ### Nested Shape: `GetUiViewResponse.formViews[string]` @@ -1829,12 +1829,12 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own | **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?: object[]; amountField?: string; … }[]` | optional | Inline master-detail child collections | -| **defaultSort** | `never` | optional | [REMOVED] `form.defaultSort` was removed in @objectstack/spec 17.0.0 (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. | +| **defaultSort** | `never` | optional | [REMOVED] `form.defaultSort` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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). | | **buttons** | `{ submit?: object; cancel?: object; reset?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm. | | **defaults** | `Record` | optional | Initial field values for create-mode forms (folded into ObjectUI ObjectForm initial values;). | -| **aria** | `never` | optional | [REMOVED] `form.aria` was removed in @objectstack/spec 17.0.0 (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. | +| **aria** | `never` | optional | [REMOVED] `form.aria` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | ### Nested Shape: `GetUiViewResponse.protection` diff --git a/content/docs/references/api/rest-server.mdx b/content/docs/references/api/rest-server.mdx index e727515985e..eabedc3dc25 100644 --- a/content/docs/references/api/rest-server.mdx +++ b/content/docs/references/api/rest-server.mdx @@ -249,7 +249,7 @@ const result = BatchEndpointsConfigSchema.parse(data); | **enableSearch** | `boolean` | optional (default: `true`) | Enable structured search endpoints (server-wide search opt-out; embedder-only, not settable from `os serve`) | | **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. 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. | +| **requireAuth** | `never` | optional | [REMOVED] `api.requireAuth` was removed in @objectstack/spec 17. 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; `--write` applies the ones it can prove, and you apply the rest by hand. | | **documentation** | `{ title?: string; description?: string; termsOfService?: string; contact?: object; … }` | optional | Publisher identity of the served OpenAPI document: each member set here overlays its `info` on both /openapi.json doors, and nothing set serves the bundled `info` unchanged. `info.version` is always the protocol version | | **responseFormat** | `never` | optional | [REMOVED] `api.responseFormat` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — nothing ever read it: `envelope`, `includeMetadata` and `includePagination` were parsed, defaulted and copied into the REST server's config and never consulted, so `envelope: false` unwrapped no response. Delete the key. Response shapes are fixed, not a server-wide option: each route answers in the response schema `@objectstack/spec/api` declares for it, which is what the client SDK parses and the served /openapi.json describes, so no configuration changes them. | @@ -297,7 +297,7 @@ const result = BatchEndpointsConfigSchema.parse(data); | **enableSearch** | `boolean` | optional (default: `true`) | Enable structured search endpoints (server-wide search opt-out; embedder-only, not settable from `os serve`) | | **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. 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. | +| **requireAuth** | `never` | optional | [REMOVED] `api.requireAuth` was removed in @objectstack/spec 17. 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; `--write` applies the ones it can prove, and you apply the rest by hand. | | **documentation** | `{ title?: string; description?: string; termsOfService?: string; contact?: object; … }` | optional | Publisher identity of the served OpenAPI document: each member set here overlays its `info` on both /openapi.json doors, and nothing set serves the bundled `info` unchanged. `info.version` is always the protocol version | | **responseFormat** | `never` | optional | [REMOVED] `api.responseFormat` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — nothing ever read it: `envelope`, `includeMetadata` and `includePagination` were parsed, defaulted and copied into the REST server's config and never consulted, so `envelope: false` unwrapped no response. Delete the key. Response shapes are fixed, not a server-wide option: each route answers in the response schema `@objectstack/spec/api` declares for it, which is what the client SDK parses and the served /openapi.json describes, so no configuration changes them. | diff --git a/content/docs/references/automation/control-flow.mdx b/content/docs/references/automation/control-flow.mdx index c950862bed9..fc69f9fabcf 100644 --- a/content/docs/references/automation/control-flow.mdx +++ b/content/docs/references/automation/control-flow.mdx @@ -119,7 +119,7 @@ const result = FlowRegionSchema.parse(data); | **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 (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. | +| **outputSchema** | `never` | optional | [REMOVED] `flow.nodes[].outputSchema` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | | **waitEventConfig** | `{ eventType: Enum<'timer' \| 'signal' \| 'webhook' \| 'manual' \| 'condition'>; timerDuration?: string; signalName?: string }` | optional | Configuration for wait node event resumption. REQUIRED on a `type: 'wait'` node — the block is the whole contract, and a wait node without one parses into a run that parks forever reporting success. Under `eventType: 'timer'` a `timerDuration` is required too. | | **boundaryConfig** | `{ attachedToNodeId: string; eventType: Enum<'error' \| 'timer' \| 'signal' \| 'cancel'>; interrupting?: boolean; errorCode?: string; … }` | optional | Configuration for boundary events attached to host nodes. REQUIRED on a `type: 'boundary_event'` node — without it the node names neither the activity it watches nor what fires it. | @@ -182,7 +182,7 @@ const result = FlowRegionSchema.parse(data); | **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 (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. | +| **outputSchema** | `never` | optional | [REMOVED] `flow.nodes[].outputSchema` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | | **waitEventConfig** | `{ eventType: Enum<'timer' \| 'signal' \| 'webhook' \| 'manual' \| 'condition'>; timerDuration?: string; signalName?: string }` | optional | Configuration for wait node event resumption. REQUIRED on a `type: 'wait'` node — the block is the whole contract, and a wait node without one parses into a run that parks forever reporting success. Under `eventType: 'timer'` a `timerDuration` is required too. | | **boundaryConfig** | `{ attachedToNodeId: string; eventType: Enum<'error' \| 'timer' \| 'signal' \| 'cancel'>; interrupting?: boolean; errorCode?: string; … }` | optional | Configuration for boundary events attached to host nodes. REQUIRED on a `type: 'boundary_event'` node — without it the node names neither the activity it watches nor what fires it. | @@ -231,7 +231,7 @@ const result = FlowRegionSchema.parse(data); | **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 — 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. | +| **retryDelayMs** | `never` | optional | [REMOVED] `retryDelayMs` was removed in @objectstack/spec 17.0.0 — 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; `--write` applies the ones it can prove, and you apply the rest by hand. | --- @@ -270,7 +270,7 @@ const result = FlowRegionSchema.parse(data); | **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 — 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. | +| **retryDelayMs** | `never` | optional | [REMOVED] `retryDelayMs` was removed in @objectstack/spec 17.0.0 — 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; `--write` applies the ones it can prove, and you apply the rest by hand. | --- diff --git a/content/docs/references/automation/flow.mdx b/content/docs/references/automation/flow.mdx index 6457321bb0f..e59becd23f1 100644 --- a/content/docs/references/automation/flow.mdx +++ b/content/docs/references/automation/flow.mdx @@ -35,12 +35,12 @@ const result = FlowSchema.parse(data); | **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 (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. | +| **template** | `never` | optional | [REMOVED] `flow.template` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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 (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. | +| **active** | `never` | optional | [REMOVED] `flow.active` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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. | @@ -74,7 +74,7 @@ const result = FlowSchema.parse(data); | **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 (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. | +| **outputSchema** | `never` | optional | [REMOVED] `flow.nodes[].outputSchema` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | | **waitEventConfig** | `{ eventType: Enum<'timer' \| 'signal' \| 'webhook' \| 'manual' \| 'condition'>; timerDuration?: string; signalName?: string }` | optional | Configuration for wait node event resumption. REQUIRED on a `type: 'wait'` node — the block is the whole contract, and a wait node without one parses into a run that parks forever reporting success. Under `eventType: 'timer'` a `timerDuration` is required too. | | **boundaryConfig** | `{ attachedToNodeId: string; eventType: Enum<'error' \| 'timer' \| 'signal' \| 'cancel'>; interrupting?: boolean; errorCode?: string; … }` | optional | Configuration for boundary events attached to host nodes. REQUIRED on a `type: 'boundary_event'` node — without it the node names neither the activity it watches nor what fires it. | @@ -100,8 +100,8 @@ const result = FlowSchema.parse(data); | **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 — 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 (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. | +| **retryDelayMs** | `never` | optional | [REMOVED] `retryDelayMs` was removed in @objectstack/spec 17.0.0 — 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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **fallbackNodeId** | `never` | optional | [REMOVED] `flow.errorHandling.fallbackNodeId` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | ### Nested Shape: `Flow.protection` @@ -145,7 +145,7 @@ const result = FlowSchema.parse(data); | **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 (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. | +| **outputSchema** | `never` | optional | [REMOVED] `flow.nodes[].outputSchema` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | | **waitEventConfig** | `{ eventType: Enum<'timer' \| 'signal' \| 'webhook' \| 'manual' \| 'condition'>; timerDuration?: string; signalName?: string }` | optional | Configuration for wait node event resumption. REQUIRED on a `type: 'wait'` node — the block is the whole contract, and a wait node without one parses into a run that parks forever reporting success. Under `eventType: 'timer'` a `timerDuration` is required too. | | **boundaryConfig** | `{ attachedToNodeId: string; eventType: Enum<'error' \| 'timer' \| 'signal' \| 'cancel'>; interrupting?: boolean; errorCode?: string; … }` | optional | Configuration for boundary events attached to host nodes. REQUIRED on a `type: 'boundary_event'` node — without it the node names neither the activity it watches nor what fires it. | @@ -172,8 +172,8 @@ const result = FlowSchema.parse(data); | **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. 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. 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. | +| **timeoutMs** | `never` | optional | [REMOVED] `waitEventConfig.timeoutMs` was removed in @objectstack/spec 17. 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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **onTimeout** | `never` | optional | [REMOVED] `waitEventConfig.onTimeout` was removed in @objectstack/spec 17. 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; `--write` applies the ones it can prove, and you apply the rest by hand. | ### Nested Shape: `FlowNode.boundaryConfig` @@ -256,12 +256,12 @@ const result = FlowSchema.parse(data); | **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 (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. | +| **template** | `never` | optional | [REMOVED] `flow.template` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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 (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. | +| **active** | `never` | optional | [REMOVED] `flow.active` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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. | diff --git a/content/docs/references/automation/schemaless-node-config.mdx b/content/docs/references/automation/schemaless-node-config.mdx index 401f6b4a3f6..ee18c5a473f 100644 --- a/content/docs/references/automation/schemaless-node-config.mdx +++ b/content/docs/references/automation/schemaless-node-config.mdx @@ -165,11 +165,11 @@ const result = DecisionConditionSchema.parse(data); | **function** | `string` | ✅ | Registered function to call (defineStack(`{ functions }`)). Contractually pure — it returns a value a later declarative node persists | | **inputs** | `Record` | optional | Inputs passed to the function (values interpolate `{token}` templates) | | **outputVariable** | `string` | optional | Flow variable the function's return value is bound to | -| **actionType** | `never` | optional | [REMOVED] `script.config.actionType` was removed in @objectstack/spec 17 — none of its values did what it said. The two built-ins were logger-backed stubs that recorded the intent and delivered nothing under any configuration, and every other value was a second spelling of `config.function`. Replace it per branch: for `email` use a `notify` node (it delivers through the messaging service — the in-app inbox by default, real email once `@objectstack/plugin-email` is installed); for `slack` use a `connector_action` node with the Slack connector, or an `http` node posting to a webhook; for anything else, move the name into `config.function`. Run `os migrate meta --from 16` to list the mechanical edits for the shorthand case into `config.function`; the stub and marker values are removed. | -| **template** | `never` | optional | [REMOVED] `script.config.template` was removed in @objectstack/spec 17 — it fed only the logger-backed `email`/`slack` stubs, which never rendered or sent a message, so no template id was ever resolved. Delete the key. A `notify` node carries its own `title`/`message`, and stored templates live in the messaging service (`sys_notification_template`), not on the node. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | -| **recipients** | `never` | optional | [REMOVED] `script.config.recipients` was removed in @objectstack/spec 17 — the addresses were logged, never messaged: the `email`/`slack` branches it fed delivered nothing. Use a `notify` node, whose `recipients` (user ids, field refs or addresses) reach the messaging service for real. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | -| **variables** | `never` | optional | [REMOVED] `script.config.variables` was removed in @objectstack/spec 17 — it injected values into a template no side effect ever rendered. Delete the key. A `notify` node carries structured data in `payload`; a registered function takes it in `config.inputs`. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | -| **script** | `never` | optional | [REMOVED] `script.config.script` was removed in @objectstack/spec 17 — the built-in runtime has no server-side JS sandbox, so an inline body was recognized and never executed: the node warned and completed as a no-op. Move the logic into a registered function (`defineStack({ functions })`) and name it in `config.function`. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **actionType** | `never` | optional | [REMOVED] `script.config.actionType` was removed in @objectstack/spec 17 — none of its values did what it said. The two built-ins were logger-backed stubs that recorded the intent and delivered nothing under any configuration, and every other value was a second spelling of `config.function`. Replace it per branch: for `email` use a `notify` node (it delivers through the messaging service — the in-app inbox by default, real email once `@objectstack/plugin-email` is installed); for `slack` use a `connector_action` node with the Slack connector, or an `http` node posting to a webhook; for anything else, move the name into `config.function`. Run `os migrate meta --from 16` to list the mechanical edits for the shorthand case into `config.function`; `--write` applies the ones it can prove, and the stub and marker values are removed. | +| **template** | `never` | optional | [REMOVED] `script.config.template` was removed in @objectstack/spec 17 — it fed only the logger-backed `email`/`slack` stubs, which never rendered or sent a message, so no template id was ever resolved. Delete the key. A `notify` node carries its own `title`/`message`, and stored templates live in the messaging service (`sys_notification_template`), not on the node. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **recipients** | `never` | optional | [REMOVED] `script.config.recipients` was removed in @objectstack/spec 17 — the addresses were logged, never messaged: the `email`/`slack` branches it fed delivered nothing. Use a `notify` node, whose `recipients` (user ids, field refs or addresses) reach the messaging service for real. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **variables** | `never` | optional | [REMOVED] `script.config.variables` was removed in @objectstack/spec 17 — it injected values into a template no side effect ever rendered. Delete the key. A `notify` node carries structured data in `payload`; a registered function takes it in `config.inputs`. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **script** | `never` | optional | [REMOVED] `script.config.script` was removed in @objectstack/spec 17 — the built-in runtime has no server-side JS sandbox, so an inline body was recognized and never executed: the node warned and completed as a no-op. Move the logic into a registered function (`defineStack({ functions })`) and name it in `config.function`. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | --- diff --git a/content/docs/references/data/analytics.mdx b/content/docs/references/data/analytics.mdx index 947a36364d1..58ac7267a6c 100644 --- a/content/docs/references/data/analytics.mdx +++ b/content/docs/references/data/analytics.mdx @@ -126,7 +126,7 @@ Type: `[string, string]` | **measures** | `Record; sql: string; … }>` | ✅ | Quantitative metrics, keyed by metric name: the record key IS the metric's name, published and queried as `.`. A metric declares no inner `name`. | | **dimensions** | `Record; sql: string; … }>` | ✅ | Qualitative attributes, keyed by dimension name: the record key IS the dimension's name, published and queried as `.`. A dimension declares no inner `name`. | | **joins** | `Record` | optional | | -| **refreshKey** | `never` | optional | [REMOVED] `analytics_cube.refreshKey` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing read it: no analytics result is cached, so neither `every` nor `sql` ever refreshed anything. Delete the key; every analytics query is computed when it is asked. A refresh cadence is declared again when a result cache exists. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **refreshKey** | `never` | optional | [REMOVED] `analytics_cube.refreshKey` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing read it: no analytics result is cached, so neither `every` nor `sql` ever refreshed anything. Delete the key; every analytics query is computed when it is asked. A refresh cadence is declared again when a result cache exists. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **public** | `boolean` | optional (default: `true`) | Whether the analytics API exposes this cube. Default true (visible). false hides it from GET /analytics/meta and refuses POST /analytics/query and /analytics/sql for it (CUBE_NOT_FOUND). Visibility only: the underlying object's permissions and row-level security still govern its records on every door. | | **_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. | @@ -140,7 +140,7 @@ Type: `[string, string]` | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **name** | `never` | optional | [REMOVED] `measures..name` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it never had an effect: the record key is the metric's name. Every consumer resolves a metric by its key in `measures` (`GET /analytics/meta` publishes it as `.`, and a query names it that way), so the inner `name` was a second copy of the identity that nothing read, and one that disagreed with its key was silently ignored. Delete the key. To rename a metric, rename its key in `measures` — and every query, dashboard and report that names `.`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **name** | `never` | optional | [REMOVED] `measures..name` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it never had an effect: the record key is the metric's name. Every consumer resolves a metric by its key in `measures` (`GET /analytics/meta` publishes it as `.`, and a query names it that way), so the inner `name` was a second copy of the identity that nothing read, and one that disagreed with its key was silently ignored. Delete the key. To rename a metric, rename its key in `measures` — and every query, dashboard and report that names `.`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **label** | `string` | ✅ | Human readable label | | **description** | `string` | optional | | | **type** | `Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>` | ✅ | | @@ -151,7 +151,7 @@ Type: `[string, string]` | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **name** | `never` | optional | [REMOVED] `dimensions..name` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it never had an effect: the record key is the dimension's name. Every consumer resolves a dimension by its key in `dimensions` (`GET /analytics/meta` publishes it as `.`, and a query names it that way), so the inner `name` was a second copy of the identity that nothing read, and one that disagreed with its key was silently ignored. Delete the key. To rename a dimension, rename its key in `dimensions` — and every query, dashboard and report that names `.`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **name** | `never` | optional | [REMOVED] `dimensions..name` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it never had an effect: the record key is the dimension's name. Every consumer resolves a dimension by its key in `dimensions` (`GET /analytics/meta` publishes it as `.`, and a query names it that way), so the inner `name` was a second copy of the identity that nothing read, and one that disagreed with its key was silently ignored. Delete the key. To rename a dimension, rename its key in `dimensions` — and every query, dashboard and report that names `.`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **label** | `string` | ✅ | Human readable label | | **description** | `string` | optional | | | **type** | `Enum<'string' \| 'number' \| 'boolean' \| 'time' \| 'geo'>` | ✅ | | @@ -184,7 +184,7 @@ Type: `[string, string]` | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **name** | `never` | optional | [REMOVED] `dimensions..name` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it never had an effect: the record key is the dimension's name. Every consumer resolves a dimension by its key in `dimensions` (`GET /analytics/meta` publishes it as `.`, and a query names it that way), so the inner `name` was a second copy of the identity that nothing read, and one that disagreed with its key was silently ignored. Delete the key. To rename a dimension, rename its key in `dimensions` — and every query, dashboard and report that names `.`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **name** | `never` | optional | [REMOVED] `dimensions..name` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it never had an effect: the record key is the dimension's name. Every consumer resolves a dimension by its key in `dimensions` (`GET /analytics/meta` publishes it as `.`, and a query names it that way), so the inner `name` was a second copy of the identity that nothing read, and one that disagreed with its key was silently ignored. Delete the key. To rename a dimension, rename its key in `dimensions` — and every query, dashboard and report that names `.`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **label** | `string` | ✅ | Human readable label | | **description** | `string` | optional | | | **type** | `Enum<'string' \| 'number' \| 'boolean' \| 'time' \| 'geo'>` | ✅ | | @@ -213,7 +213,7 @@ Type: `[string, string]` | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **name** | `never` | optional | [REMOVED] `measures..name` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it never had an effect: the record key is the metric's name. Every consumer resolves a metric by its key in `measures` (`GET /analytics/meta` publishes it as `.`, and a query names it that way), so the inner `name` was a second copy of the identity that nothing read, and one that disagreed with its key was silently ignored. Delete the key. To rename a metric, rename its key in `measures` — and every query, dashboard and report that names `.`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **name** | `never` | optional | [REMOVED] `measures..name` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it never had an effect: the record key is the metric's name. Every consumer resolves a metric by its key in `measures` (`GET /analytics/meta` publishes it as `.`, and a query names it that way), so the inner `name` was a second copy of the identity that nothing read, and one that disagreed with its key was silently ignored. Delete the key. To rename a metric, rename its key in `measures` — and every query, dashboard and report that names `.`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **label** | `string` | ✅ | Human readable label | | **description** | `string` | optional | | | **type** | `Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>` | ✅ | | diff --git a/content/docs/references/data/driver-memory.mdx b/content/docs/references/data/driver-memory.mdx index c81cc69d3cb..9bf397b7845 100644 --- a/content/docs/references/data/driver-memory.mdx +++ b/content/docs/references/data/driver-memory.mdx @@ -46,7 +46,7 @@ Auto-detect persistence configuration | **type** | `'auto'` | ✅ | | | **path** | `string` | optional | File path override for Node.js environments | | **autoSaveIntervalMs** | `number` | optional | Auto-save interval override for Node.js environments, in milliseconds | -| **autoSaveInterval** | `never` | optional | [REMOVED] `AutoPersistenceConfig.autoSaveInterval` was renamed to `autoSaveIntervalMs` in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Rename the key to `autoSaveIntervalMs`; the value (milliseconds) is unchanged. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **autoSaveInterval** | `never` | optional | [REMOVED] `AutoPersistenceConfig.autoSaveInterval` was renamed to `autoSaveIntervalMs` in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Rename the key to `autoSaveIntervalMs`; the value (milliseconds) is unchanged. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **key** | `string` | optional | localStorage key override for browser environments | @@ -63,7 +63,7 @@ File-system persistence configuration | **type** | `'file'` | ✅ | | | **path** | `string` | optional | File path to persist data | | **autoSaveIntervalMs** | `number` | optional (default: `2000`) | Auto-save interval in ms | -| **autoSaveInterval** | `never` | optional | [REMOVED] `FilePersistenceConfig.autoSaveInterval` was renamed to `autoSaveIntervalMs` in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Rename the key to `autoSaveIntervalMs`; the value (milliseconds) and the 2000 default are unchanged. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **autoSaveInterval** | `never` | optional | [REMOVED] `FilePersistenceConfig.autoSaveInterval` was renamed to `autoSaveIntervalMs` in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Rename the key to `autoSaveIntervalMs`; the value (milliseconds) and the 2000 default are unchanged. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | --- diff --git a/content/docs/references/data/driver-turso.mdx b/content/docs/references/data/driver-turso.mdx index 4aef77a466e..f71192c4a48 100644 --- a/content/docs/references/data/driver-turso.mdx +++ b/content/docs/references/data/driver-turso.mdx @@ -73,7 +73,7 @@ Turso / libSQL Connection Configuration | **syncUrl** | `string` | optional | Remote sync URL for embedded-replica mode: a libsql or https Turso endpoint | | **sync** | `{ intervalSeconds?: integer; onConnect?: boolean }` | optional | Embedded-replica sync configuration (requires `syncUrl`) | | **timeoutMs** | `integer` | optional | Operation timeout in milliseconds for remote operations | -| **timeout** | `never` | optional | [REMOVED] `turso config.timeout` was renamed to `timeoutMs` in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Rename the key to `timeoutMs`; the value (milliseconds) is unchanged. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **timeout** | `never` | optional | [REMOVED] `turso config.timeout` was renamed to `timeoutMs` in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Rename the key to `timeoutMs`; the value (milliseconds) is unchanged. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **mode** | `Enum<'local' \| 'replica' \| 'remote'>` | optional | Force a transport mode instead of inferring it from `url` | ### Nested Shape: `TursoConfig.sync` diff --git a/content/docs/references/data/field.mdx b/content/docs/references/data/field.mdx index 276db1fe066..1f1acded4ba 100644 --- a/content/docs/references/data/field.mdx +++ b/content/docs/references/data/field.mdx @@ -106,7 +106,7 @@ const result = CurrencyConfigSchema.parse(data); | **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'`. Reads the bound record's OWN columns: the field level never reads a related record, so a read THROUGH a reference field (`record.account.tier`) faults on every row and refuses every update that writes the field — `objectstack validate` refuses it. Put such a check in a `validations[]` `script` rule, whose `condition` is read one hop through a reference. | | **requiredWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is required when TRUE. A TRANSITION GATE, not an invariant: the write is refused only when the merged record violates the requirement AND the pre-write record complied — so the write that flips the predicate TRUE, an INSERT born inside the gate, and a write that clears the cell are all refused, while a row that was already missing the value keeps passing unrelated edits and state moves that stay inside the gate (ADR-0113 non-regression: adding the rule to a deployed object never bricks existing rows). Need an invariant every write must satisfy instead ('X may never exceed Y') — declare a `validations[]` `script` rule, which re-checks the merged record with no exemption. Enforced by `evaluateValidationRules`. Reads the bound record's OWN columns: the field level never reads a related record, so a read THROUGH a reference field (`record.account.tier`) faults on every row and refuses every write that reaches it — `objectstack validate` refuses it; put such a check in a `validations[]` `script` rule, whose `condition` is read one hop through a reference. The only slot; the `conditionalRequired` alias was removed in protocol 17. | -| **conditionalRequired** | `never` | optional | [REMOVED] `conditionalRequired` was removed in @objectstack/spec 17 — 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. | +| **conditionalRequired** | `never` | optional | [REMOVED] `conditionalRequired` was removed in @objectstack/spec 17 — 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; `--write` applies the ones it can prove, and you apply the rest 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 | 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. | diff --git a/content/docs/references/data/hook.mdx b/content/docs/references/data/hook.mdx index f1b32f40c27..34145cfdcdf 100644 --- a/content/docs/references/data/hook.mdx +++ b/content/docs/references/data/hook.mdx @@ -40,7 +40,7 @@ const result = HookSchema.parse(data); | **description** | `string` | optional | Human-readable description of what this hook does | | **retryPolicy** | `{ maxRetries?: number; backoffMs?: number }` | optional | Retry policy for failed hook executions | | **timeoutMs** | `number` | optional | Maximum execution time in milliseconds before the hook is aborted | -| **timeout** | `never` | optional | [REMOVED] `hook.timeout` was removed in @objectstack/spec 17 — its unit (milliseconds) lived only in the description, beside a body-level `timeoutMs` and a `retryPolicy.backoffMs` that spell theirs, so the same number read as two conventions on one surface. Rename the key to `timeoutMs`; the value (milliseconds) is unchanged. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **timeout** | `never` | optional | [REMOVED] `hook.timeout` was removed in @objectstack/spec 17 — its unit (milliseconds) lived only in the description, beside a body-level `timeoutMs` and a `retryPolicy.backoffMs` that spell theirs, so the same number read as two conventions on one surface. Rename the key to `timeoutMs`; the value (milliseconds) is unchanged. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **onError** | `Enum<'abort' \| 'log'>` | optional (default: `"abort"`) | Error handling strategy | | **runAs** | `Enum<'system' \| 'user' \| 'inherit'>` | optional (default: `"inherit"`) | Execution identity for the hook's ctx.api data operations: system = elevated (bypasses RLS), user = the triggering user (RLS-respecting), inherit = the context of the write that fired the hook (the pre-runAs behaviour; the default). A hook with no trigger user has no identity to scope to, so under user its ctx.api data operations are REFUSED — declare system to make the elevation explicit. This covers any hook fired by a write that carried no user (an isSystem plugin/service write; a system-elevated flow node). Scope: ctx.api only — condition evaluation, the readonly strip on ctx.input, ctx.session and async are unchanged. | | **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | diff --git a/content/docs/references/data/object.mdx b/content/docs/references/data/object.mdx index 6f1c57a5a35..aa3ff23f327 100644 --- a/content/docs/references/data/object.mdx +++ b/content/docs/references/data/object.mdx @@ -67,8 +67,8 @@ const result = ApiMethod.parse(data); | **name** | `string` | optional | Index name (auto-generated if not provided) | | **fields** | `string[]` | ✅ | Fields included in the index | | **unique** | `false \| '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; false/omitted = not unique. Bare true is refused (retired at protocol 18 — it was the positional spelling of 'global'): 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 (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 (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. | +| **type** | `never` | optional | [REMOVED] `indexes[].type` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **partial** | `never` | optional | [REMOVED] `indexes[].partial` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | --- @@ -270,7 +270,7 @@ const result = ApiMethod.parse(data); | **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'`. Reads the bound record's OWN columns: the field level never reads a related record, so a read THROUGH a reference field (`record.account.tier`) faults on every row and refuses every update that writes the field — `objectstack validate` refuses it. Put such a check in a `validations[]` `script` rule, whose `condition` is read one hop through a reference. | | **requiredWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is required when TRUE. A TRANSITION GATE, not an invariant: the write is refused only when the merged record violates the requirement AND the pre-write record complied — so the write that flips the predicate TRUE, an INSERT born inside the gate, and a write that clears the cell are all refused, while a row that was already missing the value keeps passing unrelated edits and state moves that stay inside the gate (ADR-0113 non-regression: adding the rule to a deployed object never bricks existing rows). Need an invariant every write must satisfy instead ('X may never exceed Y') — declare a `validations[]` `script` rule, which re-checks the merged record with no exemption. Enforced by `evaluateValidationRules`. Reads the bound record's OWN columns: the field level never reads a related record, so a read THROUGH a reference field (`record.account.tier`) faults on every row and refuses every write that reaches it — `objectstack validate` refuses it; put such a check in a `validations[]` `script` rule, whose `condition` is read one hop through a reference. The only slot; the `conditionalRequired` alias was removed in protocol 17. | -| **conditionalRequired** | `never` | optional | [REMOVED] `conditionalRequired` was removed in @objectstack/spec 17 — 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. | +| **conditionalRequired** | `never` | optional | [REMOVED] `conditionalRequired` was removed in @objectstack/spec 17 — 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; `--write` applies the ones it can prove, and you apply the rest 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 | 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. | @@ -299,8 +299,8 @@ const result = ApiMethod.parse(data); | **name** | `string` | optional | Index name (auto-generated if not provided) | | **fields** | `string[]` | ✅ | Fields included in the index | | **unique** | `false \| '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; false/omitted = not unique. Bare true is refused (retired at protocol 18 — it was the positional spelling of 'global'): 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 (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 (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. | +| **type** | `never` | optional | [REMOVED] `indexes[].type` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **partial** | `never` | optional | [REMOVED] `indexes[].partial` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | ### Nested Shape: `Object.fieldGroups[number]` @@ -384,7 +384,7 @@ const result = ApiMethod.parse(data); | **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 }` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout | -| **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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 | @@ -400,17 +400,17 @@ const result = ApiMethod.parse(data); | **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** | `never` | optional | [REMOVED] `view.list.tabs` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's `listViews` instead: the tab's `name` becomes the entry's key, its `label` the entry's `label`, and its `filter` rules join the view's own `filter` on that entry (copy the view's `columns` too); a tab whose `view` already named a list view needs nothing more. Every `listViews` entry renders as a tab in the switcher. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **tabs** | `never` | optional | [REMOVED] `view.list.tabs` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's `listViews` instead: the tab's `name` becomes the entry's key, its `label` the entry's `label`, and its `filter` rules join the view's own `filter` on that entry (copy the view's `columns` too); a tab whose `view` already named a list view needs nothing more. Every `listViews` entry renders as a tab in the switcher. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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 (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 (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 (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 (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 (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. | +| **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **striped** | `never` | optional | [REMOVED] `view.striped` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **bordered** | `never` | optional | [REMOVED] `view.bordered` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **virtualScroll** | `never` | optional | [REMOVED] `view.virtualScroll` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | | **userFilters** | `{ element?: Enum<'dropdown' \| 'toggle'>; fields?: object[] }` | optional | | ### Nested Shape: `Object.enable` @@ -455,7 +455,7 @@ const result = ApiMethod.parse(data); | **operation** | `Enum<'update'>` | optional | The declarative single-record field write, mirroring a list view's `bulkActionDefs`: `'update'` applies `patch` (merged under the collected `params`) to the current record on the data plane AS THE CALLER — never system-elevated — so the caller's permissions, the object's hooks and its validations fire as for a user edit. `type` stays at its default `'script'` (the platform action route the write is performed on); `target`/`body`/`method`/`bodyExtra` are refused beside it. `'delete'` and `'custom'` have no row-level form. | | **patch** | `Record` | optional | For `operation: 'update'` — static field values written to the current record, merged UNDER the user-supplied `params` so a fixed value can be declared without exposing it in the dialog. Written on the data plane as the caller: object permissions, hooks and validations fire as for a user edit. Refused on an action without `operation: 'update'` (it would be silently dropped). | | **execution** | `Enum<'perRecord' \| 'aggregate'>` | optional | The bulk dispatch contract this action's BODY is written for, in `bulkActionDefs`' own vocabulary: 'perRecord' = one dispatch per selected row carrying that row's `recordId` (the view's `bulkActions: ['']` bare-string form); 'aggregate' = ONE dispatch for the whole selection carrying every id in `params._selectedIds` (a `bulkActionDefs` entry with `execution: 'aggregate'`). Optional with NO default — omit it only when the body genuinely serves both. A list view wiring a declared action under the other contract is refused by `@objectstack/lint` (`action-dispatch-contract-mismatch`). | -| **execute** | `never` | optional | [REMOVED] `execute` was removed in @objectstack/spec 17 — 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. | +| **execute** | `never` | optional | [REMOVED] `execute` was removed in @objectstack/spec 17 — 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; `--write` applies the ones it can prove, and you apply the rest 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. | @@ -471,8 +471,8 @@ const result = ApiMethod.parse(data); | **requiresMembershipReach** | `Enum<'invite_member' \| 'cancel_invitation' \| 'update_member_role' \| …>` | optional | Organization endpoint (a MEMBERSHIP_REACH row) whose membership-grade gate this action follows; lowered into `visible` over current_user.positions 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 (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 (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. | +| **shortcut** | `never` | optional | [REMOVED] `action.shortcut` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **bulkEnabled** | `never` | optional | [REMOVED] `action.bulkEnabled` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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". | @@ -483,7 +483,7 @@ const result = ApiMethod.parse(data); | **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. `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** | `never` | optional | [REMOVED] `action.aria` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no action surface ever applied it: the button, icon, menu, group and bar renderers, the row and bulk action menus and the record quick-actions toolbar all take the accessible name from the action's `label` and never read this block, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied is the action's required `label` — the visible button or menu-item text, and the `aria-label` of an icon-only action — so write the name you meant there. To name the region that PLACES the actions, author `ariaLabel` / `ariaDescribedBy` / `role` in the `aria` block of the placing node: `page.components[].aria` (the component that renders the actions) or the list view `aria`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **aria** | `never` | optional | [REMOVED] `action.aria` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no action surface ever applied it: the button, icon, menu, group and bar renderers, the row and bulk action menus and the record quick-actions toolbar all take the accessible name from the action's `label` and never read this block, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied is the action's required `label` — the visible button or menu-item text, and the `aria-label` of an icon-only action — so write the name you meant there. To name the region that PLACES the actions, author `ariaLabel` / `ariaDescribedBy` / `role` in the `aria` block of the placing node: `page.components[].aria` (the component that renders the actions) or the list view `aria`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **_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). | @@ -605,7 +605,7 @@ const result = ApiMethod.parse(data); | **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'`. Reads the bound record's OWN columns: the field level never reads a related record, so a read THROUGH a reference field (`record.account.tier`) faults on every row and refuses every update that writes the field — `objectstack validate` refuses it. Put such a check in a `validations[]` `script` rule, whose `condition` is read one hop through a reference. | | **requiredWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is required when TRUE. A TRANSITION GATE, not an invariant: the write is refused only when the merged record violates the requirement AND the pre-write record complied — so the write that flips the predicate TRUE, an INSERT born inside the gate, and a write that clears the cell are all refused, while a row that was already missing the value keeps passing unrelated edits and state moves that stay inside the gate (ADR-0113 non-regression: adding the rule to a deployed object never bricks existing rows). Need an invariant every write must satisfy instead ('X may never exceed Y') — declare a `validations[]` `script` rule, which re-checks the merged record with no exemption. Enforced by `evaluateValidationRules`. Reads the bound record's OWN columns: the field level never reads a related record, so a read THROUGH a reference field (`record.account.tier`) faults on every row and refuses every write that reaches it — `objectstack validate` refuses it; put such a check in a `validations[]` `script` rule, whose `condition` is read one hop through a reference. The only slot; the `conditionalRequired` alias was removed in protocol 17. | -| **conditionalRequired** | `never` | optional | [REMOVED] `conditionalRequired` was removed in @objectstack/spec 17 — 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. | +| **conditionalRequired** | `never` | optional | [REMOVED] `conditionalRequired` was removed in @objectstack/spec 17 — 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; `--write` applies the ones it can prove, and you apply the rest 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 | 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. | @@ -634,8 +634,8 @@ const result = ApiMethod.parse(data); | **name** | `string` | optional | Index name (auto-generated if not provided) | | **fields** | `string[]` | ✅ | Fields included in the index | | **unique** | `false \| '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; false/omitted = not unique. Bare true is refused (retired at protocol 18 — it was the positional spelling of 'global'): 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 (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 (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. | +| **type** | `never` | optional | [REMOVED] `indexes[].type` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **partial** | `never` | optional | [REMOVED] `indexes[].partial` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | --- diff --git a/content/docs/references/integration/connector.mdx b/content/docs/references/integration/connector.mdx index c8f93c87e0f..8566a20c5e7 100644 --- a/content/docs/references/integration/connector.mdx +++ b/content/docs/references/integration/connector.mdx @@ -190,18 +190,18 @@ const result = ConnectorSchema.parse(data); | **providerConfig** | `Record` | optional | Provider-specific config validated by the provider factory at boot (e.g. `{ spec, baseUrl }` for openapi, where spec is an inline document, a package-relative file path like './billing-openapi.json', or an http(s) URL). Requires `provider`. | | **auth** | `{ type: 'none' } \| { type: 'bearer'; credentialRef: string } \| { type: 'api-key'; credentialRef: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; credentialRef: string }` | optional | Declarative instance auth — references credentials via `credentialRef` (resolved at boot), never inline secrets. Requires `provider` (ADR-0097). | | **actions** | `{ key: string; label: string; description?: string; inputSchema?: Record; … }[]` | optional | | -| **triggers** | `never` | optional | [REMOVED] `connector.triggers` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — a connector trigger never started anything: `AutomationEngine.registerConnector` registers a connector's actions only, no polling loop read `intervalSeconds` (or the `interval` spelling it was renamed from), and no receiver was driven by a `webhook` trigger. Delete the key; the `ConnectorTrigger` shape leaves with it. To start work from an external system, write a flow that calls the connector's action in a `connector_action` node: for an external event, an `api` flow that the event's sender calls; for a scheduled pull, a `schedule` flow. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **syncConfig** | `never` | optional | [REMOVED] `connector.syncConfig` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no engine ever ran a connector-attached sync: nothing read `strategy`, `direction`, `realtimeSync`, `timestampField`, `conflictResolution`, `batchSize`, `deleteMode` or `filters`, so the `latest_wins` and `soft_delete` defaults resolved and deleted nothing. Delete the key; the `DataSyncConfig` shape leaves with it. A sync is defined on its TARGET: a `mapping` (`targetObject`, `fieldMapping`, `mode`, `upsertKey`) whose `connectorSource` names the `rest` or `openapi` connector it pulls from, the read action and an optional timestamp `watermark`, with a `job` for the cadence. Its pull runs when a `job` drives it — a `job` whose `pull: { mapping }` names that mapping; the binding alone moves no rows. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **fieldMappings** | `never` | optional | [REMOVED] `connector.fieldMappings` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no engine ever moved a value through a connector field mapping: nothing read `source`, `target`, `defaultValue`, `dataType`, `required` or `syncMode`. Delete the key; the `ConnectorFieldMapping` shape leaves with it. Map fields on the sync's TARGET instead: a `mapping`'s `fieldMapping` (`source` → `target`, with a `transform` the import path executes), which its `connectorSource` pulls through. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **webhooks** | `never` | optional | [REMOVED] `connector.webhooks` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — a webhook nested inside a connector was never registered as a `webhook` item, so it was never materialized into `sys_webhook` and never delivered, and nothing emits the connector events its `events` list could name (`sync.completed`, `auth.expired` and the rest). Delete the key; the nested shape leaves with it (`WebhookConfig`, `WebhookEvent`, `WebhookSignatureAlgorithm`). To have a webhook actually sent, declare it in the stack's top-level `webhooks:` collection, which is materialized into `sys_webhook` and delivered on record events — note that doing so STARTS deliveries this connector never made. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **rateLimitConfig** | `never` | optional | [REMOVED] `connector.rateLimitConfig` was removed in @objectstack/spec 17.0.0 (ADR-0049 D2) — the entire shape is gone, not just this key: `ConnectorRateLimitConfig` and its `RateLimitStrategy` enum were removed with it, because no outbound rate-limiting engine ever existed. The platform's only token bucket (runtime `security/rate-limit.ts`) throttles INBOUND requests to us; nothing throttled the calls a connector makes out, so every knob here was inert while reading like a configured cap. Delete the key. Do NOT substitute `shared` `RateLimitConfig` — that is the inbound limiter and would cap the wrong direction; until an outbound throttle exists, rate-limit at the connector provider or upstream gateway. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **triggers** | `never` | optional | [REMOVED] `connector.triggers` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — a connector trigger never started anything: `AutomationEngine.registerConnector` registers a connector's actions only, no polling loop read `intervalSeconds` (or the `interval` spelling it was renamed from), and no receiver was driven by a `webhook` trigger. Delete the key; the `ConnectorTrigger` shape leaves with it. To start work from an external system, write a flow that calls the connector's action in a `connector_action` node: for an external event, an `api` flow that the event's sender calls; for a scheduled pull, a `schedule` flow. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **syncConfig** | `never` | optional | [REMOVED] `connector.syncConfig` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no engine ever ran a connector-attached sync: nothing read `strategy`, `direction`, `realtimeSync`, `timestampField`, `conflictResolution`, `batchSize`, `deleteMode` or `filters`, so the `latest_wins` and `soft_delete` defaults resolved and deleted nothing. Delete the key; the `DataSyncConfig` shape leaves with it. A sync is defined on its TARGET: a `mapping` (`targetObject`, `fieldMapping`, `mode`, `upsertKey`) whose `connectorSource` names the `rest` or `openapi` connector it pulls from, the read action and an optional timestamp `watermark`, with a `job` for the cadence. Its pull runs when a `job` drives it — a `job` whose `pull: { mapping }` names that mapping; the binding alone moves no rows. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **fieldMappings** | `never` | optional | [REMOVED] `connector.fieldMappings` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no engine ever moved a value through a connector field mapping: nothing read `source`, `target`, `defaultValue`, `dataType`, `required` or `syncMode`. Delete the key; the `ConnectorFieldMapping` shape leaves with it. Map fields on the sync's TARGET instead: a `mapping`'s `fieldMapping` (`source` → `target`, with a `transform` the import path executes), which its `connectorSource` pulls through. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **webhooks** | `never` | optional | [REMOVED] `connector.webhooks` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — a webhook nested inside a connector was never registered as a `webhook` item, so it was never materialized into `sys_webhook` and never delivered, and nothing emits the connector events its `events` list could name (`sync.completed`, `auth.expired` and the rest). Delete the key; the nested shape leaves with it (`WebhookConfig`, `WebhookEvent`, `WebhookSignatureAlgorithm`). To have a webhook actually sent, declare it in the stack's top-level `webhooks:` collection, which is materialized into `sys_webhook` and delivered on record events — note that doing so STARTS deliveries this connector never made. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **rateLimitConfig** | `never` | optional | [REMOVED] `connector.rateLimitConfig` was removed in @objectstack/spec 17.0.0 (ADR-0049 D2) — the entire shape is gone, not just this key: `ConnectorRateLimitConfig` and its `RateLimitStrategy` enum were removed with it, because no outbound rate-limiting engine ever existed. The platform's only token bucket (runtime `security/rate-limit.ts`) throttles INBOUND requests to us; nothing throttled the calls a connector makes out, so every knob here was inert while reading like a configured cap. Delete the key. Do NOT substitute `shared` `RateLimitConfig` — that is the inbound limiter and would cap the wrong direction; until an outbound throttle exists, rate-limit at the connector provider or upstream gateway. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **retryConfig** | `{ strategy?: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts?: number; initialDelayMs?: number; maxDelayMs?: number; … }` | optional | Retry configuration | -| **connectionTimeoutMs** | `never` | optional | [REMOVED] `connector.connectionTimeoutMs` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — the platform never honoured it and cannot honour it where it was declared: a connector's outbound call is a WHATWG `fetch`, whose only cancellation surface is one `AbortSignal` over the whole operation, so nothing there observes the connection phase separately, and the value only ever travelled (onto the reported def and the materialization fingerprint) without ever bounding a connect. Delete the key. Use `requestTimeoutMs` for the deadline the platform does keep — it is applied as `resilientFetch`'s per-attempt timeout — and bound the connect phase at a connector provider or upstream gateway on a transport that can separate the phases. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **connectionTimeoutMs** | `never` | optional | [REMOVED] `connector.connectionTimeoutMs` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — the platform never honoured it and cannot honour it where it was declared: a connector's outbound call is a WHATWG `fetch`, whose only cancellation surface is one `AbortSignal` over the whole operation, so nothing there observes the connection phase separately, and the value only ever travelled (onto the reported def and the materialization fingerprint) without ever bounding a connect. Delete the key. Use `requestTimeoutMs` for the deadline the platform does keep — it is applied as `resilientFetch`'s per-attempt timeout — and bound the connect phase at a connector provider or upstream gateway on a transport that can separate the phases. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **requestTimeoutMs** | `number` | optional (default: `30000`) | Request timeout in ms | -| **status** | `never` | optional | [REMOVED] `connector.status` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read it: `status: 'active'` neither enabled nor advertised a connector, and `'error'` or `'configuring'` changed nothing either. Delete the key; the `ConnectorStatus` enum leaves with it. On a declarative entry, `enabled: false` is what withdraws a materialized instance or marks a catalog-only descriptor, and whether a registered connector can be dispatched is computed by the runtime and reported as `state` (`ready` or `degraded`) on `GET /api/v1/automation/connectors` — no authored value sets it. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **status** | `never` | optional | [REMOVED] `connector.status` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read it: `status: 'active'` neither enabled nor advertised a connector, and `'error'` or `'configuring'` changed nothing either. Delete the key; the `ConnectorStatus` enum leaves with it. On a declarative entry, `enabled: false` is what withdraws a materialized instance or marks a catalog-only descriptor, and whether a registered connector can be dispatched is computed by the runtime and reported as `state` (`ready` or `degraded`) on `GET /api/v1/automation/connectors` — no authored value sets it. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **enabled** | `boolean` | optional (default: `true`) | Enable connector. On declarative stack entries, false marks a deliberate catalog-only descriptor. | -| **errorMapping** | `never` | optional | [REMOVED] `connector.errorMapping` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read it: no provider, dispatcher or materializer mapped an external error through the rules, so `unmappedBehavior` configured nothing and a rule's `userMessage` was never shown to anyone (that spelling is the live API-error channel, `ApiError.userMessage`, which a thrown HTTP error declares — not connector metadata). Delete the key; the whole shape leaves with it (`ErrorMappingConfig`, `ErrorMappingRule` and the `ConnectorErrorCategory` enum). There is no replacement, because no error-mapping engine exists: a connector's failures reach callers as the provider's own errors (ADR-0097). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **health** | `never` | optional | [REMOVED] `connector.health` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no connector health probe or circuit breaker ever existed: nothing scheduled a `healthCheck` request, counted consecutive failures against a threshold, or opened, half-opened or closed a `circuitBreaker`, and no `fallbackStrategy` was ever applied, so every key in the block configured nothing. That includes `circuitBreaker.monitoringWindowMs` and the `monitoringWindow` spelling it was renamed from: the renamed key is removed with the rest. Delete the key; the whole shape leaves with it (`ConnectorHealth`, `HealthCheckConfig`, `CircuitBreakerConfig`). Whether a connector can be dispatched is computed, not authored: `GET /api/v1/automation/connectors` reports each connector's `state` (`ready` or `degraded`). Put health probes and circuit breaking in the connector provider or an upstream gateway. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **errorMapping** | `never` | optional | [REMOVED] `connector.errorMapping` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read it: no provider, dispatcher or materializer mapped an external error through the rules, so `unmappedBehavior` configured nothing and a rule's `userMessage` was never shown to anyone (that spelling is the live API-error channel, `ApiError.userMessage`, which a thrown HTTP error declares — not connector metadata). Delete the key; the whole shape leaves with it (`ErrorMappingConfig`, `ErrorMappingRule` and the `ConnectorErrorCategory` enum). There is no replacement, because no error-mapping engine exists: a connector's failures reach callers as the provider's own errors (ADR-0097). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **health** | `never` | optional | [REMOVED] `connector.health` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no connector health probe or circuit breaker ever existed: nothing scheduled a `healthCheck` request, counted consecutive failures against a threshold, or opened, half-opened or closed a `circuitBreaker`, and no `fallbackStrategy` was ever applied, so every key in the block configured nothing. That includes `circuitBreaker.monitoringWindowMs` and the `monitoringWindow` spelling it was renamed from: the renamed key is removed with the rest. Delete the key; the whole shape leaves with it (`ConnectorHealth`, `HealthCheckConfig`, `CircuitBreakerConfig`). Whether a connector can be dispatched is computed, not authored: `GET /api/v1/automation/connectors` reports each connector's `state` (`ready` or `degraded`). Put health probes and circuit breaking in the connector provider or an upstream gateway. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **metadata** | `Record` | optional | Custom connector metadata | | **_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. | @@ -487,18 +487,18 @@ Connector type | **providerConfig** | `Record` | optional | Provider-specific config validated by the provider factory at boot (e.g. `{ spec, baseUrl }` for openapi, where spec is an inline document, a package-relative file path like './billing-openapi.json', or an http(s) URL). Requires `provider`. | | **auth** | `{ type: 'none' } \| { type: 'bearer'; credentialRef: string } \| { type: 'api-key'; credentialRef: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; credentialRef: string }` | optional | Declarative instance auth — references credentials via `credentialRef` (resolved at boot), never inline secrets. Requires `provider` (ADR-0097). | | **actions** | `{ key: string; label: string; description?: string; inputSchema?: Record; … }[]` | optional | | -| **triggers** | `never` | optional | [REMOVED] `connector.triggers` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — a connector trigger never started anything: `AutomationEngine.registerConnector` registers a connector's actions only, no polling loop read `intervalSeconds` (or the `interval` spelling it was renamed from), and no receiver was driven by a `webhook` trigger. Delete the key; the `ConnectorTrigger` shape leaves with it. To start work from an external system, write a flow that calls the connector's action in a `connector_action` node: for an external event, an `api` flow that the event's sender calls; for a scheduled pull, a `schedule` flow. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **syncConfig** | `never` | optional | [REMOVED] `connector.syncConfig` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no engine ever ran a connector-attached sync: nothing read `strategy`, `direction`, `realtimeSync`, `timestampField`, `conflictResolution`, `batchSize`, `deleteMode` or `filters`, so the `latest_wins` and `soft_delete` defaults resolved and deleted nothing. Delete the key; the `DataSyncConfig` shape leaves with it. A sync is defined on its TARGET: a `mapping` (`targetObject`, `fieldMapping`, `mode`, `upsertKey`) whose `connectorSource` names the `rest` or `openapi` connector it pulls from, the read action and an optional timestamp `watermark`, with a `job` for the cadence. Its pull runs when a `job` drives it — a `job` whose `pull: { mapping }` names that mapping; the binding alone moves no rows. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **fieldMappings** | `never` | optional | [REMOVED] `connector.fieldMappings` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no engine ever moved a value through a connector field mapping: nothing read `source`, `target`, `defaultValue`, `dataType`, `required` or `syncMode`. Delete the key; the `ConnectorFieldMapping` shape leaves with it. Map fields on the sync's TARGET instead: a `mapping`'s `fieldMapping` (`source` → `target`, with a `transform` the import path executes), which its `connectorSource` pulls through. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **webhooks** | `never` | optional | [REMOVED] `connector.webhooks` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — a webhook nested inside a connector was never registered as a `webhook` item, so it was never materialized into `sys_webhook` and never delivered, and nothing emits the connector events its `events` list could name (`sync.completed`, `auth.expired` and the rest). Delete the key; the nested shape leaves with it (`WebhookConfig`, `WebhookEvent`, `WebhookSignatureAlgorithm`). To have a webhook actually sent, declare it in the stack's top-level `webhooks:` collection, which is materialized into `sys_webhook` and delivered on record events — note that doing so STARTS deliveries this connector never made. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **rateLimitConfig** | `never` | optional | [REMOVED] `connector.rateLimitConfig` was removed in @objectstack/spec 17.0.0 (ADR-0049 D2) — the entire shape is gone, not just this key: `ConnectorRateLimitConfig` and its `RateLimitStrategy` enum were removed with it, because no outbound rate-limiting engine ever existed. The platform's only token bucket (runtime `security/rate-limit.ts`) throttles INBOUND requests to us; nothing throttled the calls a connector makes out, so every knob here was inert while reading like a configured cap. Delete the key. Do NOT substitute `shared` `RateLimitConfig` — that is the inbound limiter and would cap the wrong direction; until an outbound throttle exists, rate-limit at the connector provider or upstream gateway. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **triggers** | `never` | optional | [REMOVED] `connector.triggers` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — a connector trigger never started anything: `AutomationEngine.registerConnector` registers a connector's actions only, no polling loop read `intervalSeconds` (or the `interval` spelling it was renamed from), and no receiver was driven by a `webhook` trigger. Delete the key; the `ConnectorTrigger` shape leaves with it. To start work from an external system, write a flow that calls the connector's action in a `connector_action` node: for an external event, an `api` flow that the event's sender calls; for a scheduled pull, a `schedule` flow. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **syncConfig** | `never` | optional | [REMOVED] `connector.syncConfig` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no engine ever ran a connector-attached sync: nothing read `strategy`, `direction`, `realtimeSync`, `timestampField`, `conflictResolution`, `batchSize`, `deleteMode` or `filters`, so the `latest_wins` and `soft_delete` defaults resolved and deleted nothing. Delete the key; the `DataSyncConfig` shape leaves with it. A sync is defined on its TARGET: a `mapping` (`targetObject`, `fieldMapping`, `mode`, `upsertKey`) whose `connectorSource` names the `rest` or `openapi` connector it pulls from, the read action and an optional timestamp `watermark`, with a `job` for the cadence. Its pull runs when a `job` drives it — a `job` whose `pull: { mapping }` names that mapping; the binding alone moves no rows. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **fieldMappings** | `never` | optional | [REMOVED] `connector.fieldMappings` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no engine ever moved a value through a connector field mapping: nothing read `source`, `target`, `defaultValue`, `dataType`, `required` or `syncMode`. Delete the key; the `ConnectorFieldMapping` shape leaves with it. Map fields on the sync's TARGET instead: a `mapping`'s `fieldMapping` (`source` → `target`, with a `transform` the import path executes), which its `connectorSource` pulls through. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **webhooks** | `never` | optional | [REMOVED] `connector.webhooks` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — a webhook nested inside a connector was never registered as a `webhook` item, so it was never materialized into `sys_webhook` and never delivered, and nothing emits the connector events its `events` list could name (`sync.completed`, `auth.expired` and the rest). Delete the key; the nested shape leaves with it (`WebhookConfig`, `WebhookEvent`, `WebhookSignatureAlgorithm`). To have a webhook actually sent, declare it in the stack's top-level `webhooks:` collection, which is materialized into `sys_webhook` and delivered on record events — note that doing so STARTS deliveries this connector never made. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **rateLimitConfig** | `never` | optional | [REMOVED] `connector.rateLimitConfig` was removed in @objectstack/spec 17.0.0 (ADR-0049 D2) — the entire shape is gone, not just this key: `ConnectorRateLimitConfig` and its `RateLimitStrategy` enum were removed with it, because no outbound rate-limiting engine ever existed. The platform's only token bucket (runtime `security/rate-limit.ts`) throttles INBOUND requests to us; nothing throttled the calls a connector makes out, so every knob here was inert while reading like a configured cap. Delete the key. Do NOT substitute `shared` `RateLimitConfig` — that is the inbound limiter and would cap the wrong direction; until an outbound throttle exists, rate-limit at the connector provider or upstream gateway. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **retryConfig** | `{ strategy?: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts?: number; initialDelayMs?: number; maxDelayMs?: number; … }` | optional | Retry configuration | -| **connectionTimeoutMs** | `never` | optional | [REMOVED] `connector.connectionTimeoutMs` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — the platform never honoured it and cannot honour it where it was declared: a connector's outbound call is a WHATWG `fetch`, whose only cancellation surface is one `AbortSignal` over the whole operation, so nothing there observes the connection phase separately, and the value only ever travelled (onto the reported def and the materialization fingerprint) without ever bounding a connect. Delete the key. Use `requestTimeoutMs` for the deadline the platform does keep — it is applied as `resilientFetch`'s per-attempt timeout — and bound the connect phase at a connector provider or upstream gateway on a transport that can separate the phases. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **connectionTimeoutMs** | `never` | optional | [REMOVED] `connector.connectionTimeoutMs` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — the platform never honoured it and cannot honour it where it was declared: a connector's outbound call is a WHATWG `fetch`, whose only cancellation surface is one `AbortSignal` over the whole operation, so nothing there observes the connection phase separately, and the value only ever travelled (onto the reported def and the materialization fingerprint) without ever bounding a connect. Delete the key. Use `requestTimeoutMs` for the deadline the platform does keep — it is applied as `resilientFetch`'s per-attempt timeout — and bound the connect phase at a connector provider or upstream gateway on a transport that can separate the phases. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **requestTimeoutMs** | `number` | optional (default: `30000`) | Request timeout in ms | -| **status** | `never` | optional | [REMOVED] `connector.status` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read it: `status: 'active'` neither enabled nor advertised a connector, and `'error'` or `'configuring'` changed nothing either. Delete the key; the `ConnectorStatus` enum leaves with it. On a declarative entry, `enabled: false` is what withdraws a materialized instance or marks a catalog-only descriptor, and whether a registered connector can be dispatched is computed by the runtime and reported as `state` (`ready` or `degraded`) on `GET /api/v1/automation/connectors` — no authored value sets it. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **status** | `never` | optional | [REMOVED] `connector.status` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read it: `status: 'active'` neither enabled nor advertised a connector, and `'error'` or `'configuring'` changed nothing either. Delete the key; the `ConnectorStatus` enum leaves with it. On a declarative entry, `enabled: false` is what withdraws a materialized instance or marks a catalog-only descriptor, and whether a registered connector can be dispatched is computed by the runtime and reported as `state` (`ready` or `degraded`) on `GET /api/v1/automation/connectors` — no authored value sets it. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **enabled** | `boolean` | optional (default: `true`) | Enable connector. On declarative stack entries, false marks a deliberate catalog-only descriptor. | -| **errorMapping** | `never` | optional | [REMOVED] `connector.errorMapping` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read it: no provider, dispatcher or materializer mapped an external error through the rules, so `unmappedBehavior` configured nothing and a rule's `userMessage` was never shown to anyone (that spelling is the live API-error channel, `ApiError.userMessage`, which a thrown HTTP error declares — not connector metadata). Delete the key; the whole shape leaves with it (`ErrorMappingConfig`, `ErrorMappingRule` and the `ConnectorErrorCategory` enum). There is no replacement, because no error-mapping engine exists: a connector's failures reach callers as the provider's own errors (ADR-0097). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **health** | `never` | optional | [REMOVED] `connector.health` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no connector health probe or circuit breaker ever existed: nothing scheduled a `healthCheck` request, counted consecutive failures against a threshold, or opened, half-opened or closed a `circuitBreaker`, and no `fallbackStrategy` was ever applied, so every key in the block configured nothing. That includes `circuitBreaker.monitoringWindowMs` and the `monitoringWindow` spelling it was renamed from: the renamed key is removed with the rest. Delete the key; the whole shape leaves with it (`ConnectorHealth`, `HealthCheckConfig`, `CircuitBreakerConfig`). Whether a connector can be dispatched is computed, not authored: `GET /api/v1/automation/connectors` reports each connector's `state` (`ready` or `degraded`). Put health probes and circuit breaking in the connector provider or an upstream gateway. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **errorMapping** | `never` | optional | [REMOVED] `connector.errorMapping` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read it: no provider, dispatcher or materializer mapped an external error through the rules, so `unmappedBehavior` configured nothing and a rule's `userMessage` was never shown to anyone (that spelling is the live API-error channel, `ApiError.userMessage`, which a thrown HTTP error declares — not connector metadata). Delete the key; the whole shape leaves with it (`ErrorMappingConfig`, `ErrorMappingRule` and the `ConnectorErrorCategory` enum). There is no replacement, because no error-mapping engine exists: a connector's failures reach callers as the provider's own errors (ADR-0097). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **health** | `never` | optional | [REMOVED] `connector.health` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no connector health probe or circuit breaker ever existed: nothing scheduled a `healthCheck` request, counted consecutive failures against a threshold, or opened, half-opened or closed a `circuitBreaker`, and no `fallbackStrategy` was ever applied, so every key in the block configured nothing. That includes `circuitBreaker.monitoringWindowMs` and the `monitoringWindow` spelling it was renamed from: the renamed key is removed with the rest. Delete the key; the whole shape leaves with it (`ConnectorHealth`, `HealthCheckConfig`, `CircuitBreakerConfig`). Whether a connector can be dispatched is computed, not authored: `GET /api/v1/automation/connectors` reports each connector's `state` (`ready` or `degraded`). Put health probes and circuit breaking in the connector provider or an upstream gateway. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **metadata** | `Record` | optional | Custom connector metadata | | **_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. | diff --git a/content/docs/references/kernel/metadata-plugin.mdx b/content/docs/references/kernel/metadata-plugin.mdx index 23bd44626a0..34817087bee 100644 --- a/content/docs/references/kernel/metadata-plugin.mdx +++ b/content/docs/references/kernel/metadata-plugin.mdx @@ -325,7 +325,7 @@ const result = MetadataBulkResultSchema.parse(data); | **operation** | `Enum<'update'>` | optional | The declarative single-record field write, mirroring a list view's `bulkActionDefs`: `'update'` applies `patch` (merged under the collected `params`) to the current record on the data plane AS THE CALLER — never system-elevated — so the caller's permissions, the object's hooks and its validations fire as for a user edit. `type` stays at its default `'script'` (the platform action route the write is performed on); `target`/`body`/`method`/`bodyExtra` are refused beside it. `'delete'` and `'custom'` have no row-level form. | | **patch** | `Record` | optional | For `operation: 'update'` — static field values written to the current record, merged UNDER the user-supplied `params` so a fixed value can be declared without exposing it in the dialog. Written on the data plane as the caller: object permissions, hooks and validations fire as for a user edit. Refused on an action without `operation: 'update'` (it would be silently dropped). | | **execution** | `Enum<'perRecord' \| 'aggregate'>` | optional | The bulk dispatch contract this action's BODY is written for, in `bulkActionDefs`' own vocabulary: 'perRecord' = one dispatch per selected row carrying that row's `recordId` (the view's `bulkActions: ['']` bare-string form); 'aggregate' = ONE dispatch for the whole selection carrying every id in `params._selectedIds` (a `bulkActionDefs` entry with `execution: 'aggregate'`). Optional with NO default — omit it only when the body genuinely serves both. A list view wiring a declared action under the other contract is refused by `@objectstack/lint` (`action-dispatch-contract-mismatch`). | -| **execute** | `never` | optional | [REMOVED] `execute` was removed in @objectstack/spec 17 — 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. | +| **execute** | `never` | optional | [REMOVED] `execute` was removed in @objectstack/spec 17 — 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; `--write` applies the ones it can prove, and you apply the rest 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. | @@ -341,8 +341,8 @@ const result = MetadataBulkResultSchema.parse(data); | **requiresMembershipReach** | `Enum<'invite_member' \| 'cancel_invitation' \| 'update_member_role' \| …>` | optional | Organization endpoint (a MEMBERSHIP_REACH row) whose membership-grade gate this action follows; lowered into `visible` over current_user.positions 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 (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 (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. | +| **shortcut** | `never` | optional | [REMOVED] `action.shortcut` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **bulkEnabled** | `never` | optional | [REMOVED] `action.bulkEnabled` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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". | @@ -353,7 +353,7 @@ const result = MetadataBulkResultSchema.parse(data); | **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. `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** | `never` | optional | [REMOVED] `action.aria` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no action surface ever applied it: the button, icon, menu, group and bar renderers, the row and bulk action menus and the record quick-actions toolbar all take the accessible name from the action's `label` and never read this block, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied is the action's required `label` — the visible button or menu-item text, and the `aria-label` of an icon-only action — so write the name you meant there. To name the region that PLACES the actions, author `ariaLabel` / `ariaDescribedBy` / `role` in the `aria` block of the placing node: `page.components[].aria` (the component that renders the actions) or the list view `aria`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **aria** | `never` | optional | [REMOVED] `action.aria` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no action surface ever applied it: the button, icon, menu, group and bar renderers, the row and bulk action menus and the record quick-actions toolbar all take the accessible name from the action's `label` and never read this block, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied is the action's required `label` — the visible button or menu-item text, and the `aria-label` of an icon-only action — so write the name you meant there. To name the region that PLACES the actions, author `ariaLabel` / `ariaDescribedBy` / `role` in the `aria` block of the placing node: `page.components[].aria` (the component that renders the actions) or the list view `aria`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **_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). | diff --git a/content/docs/references/security/permission.mdx b/content/docs/references/security/permission.mdx index ed59116daaf..224659ec51f 100644 --- a/content/docs/references/security/permission.mdx +++ b/content/docs/references/security/permission.mdx @@ -50,8 +50,8 @@ const result = AdminScopeSchema.parse(data); | **allowDelete** | `boolean` | optional (default: `false`) | Delete permission | | **allowExport** | `boolean` | optional | 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 via the insert/update owner_id guard] Change record ownership (assign/reassign/disown owner_id) | -| **allowRestore** | `never` | optional | [REMOVED] `objects..allowRestore` was removed in @objectstack/spec 17 (ADR-0049) — the `restore` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing. Delete the key — a dispatched `restore` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **allowPurge** | `never` | optional | [REMOVED] `objects..allowPurge` was removed in @objectstack/spec 17 (ADR-0049) — the `purge` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing (a compliance/GDPR erase the author believed was permission-locked was not — the operation itself does not exist). Delete the key — a dispatched `purge` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **allowRestore** | `never` | optional | [REMOVED] `objects..allowRestore` was removed in @objectstack/spec 17 (ADR-0049) — the `restore` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing. Delete the key — a dispatched `restore` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **allowPurge** | `never` | optional | [REMOVED] `objects..allowPurge` was removed in @objectstack/spec 17 (ADR-0049) — the `purge` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing (a compliance/GDPR erase the author believed was permission-locked was not — the operation itself does not exist). Delete the key — a dispatched `purge` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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. | | **readScope** | `Enum<'own' \| 'own_and_reports' \| 'unit' \| 'unit_and_below' \| 'org'>` | optional | [ADR-0057 D1] Read depth: own\|unit\|unit_and_below\|org | @@ -98,8 +98,8 @@ const result = AdminScopeSchema.parse(data); | **allowDelete** | `boolean` | optional (default: `false`) | Delete permission | | **allowExport** | `boolean` | optional | 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 via the insert/update owner_id guard] Change record ownership (assign/reassign/disown owner_id) | -| **allowRestore** | `never` | optional | [REMOVED] `objects..allowRestore` was removed in @objectstack/spec 17 (ADR-0049) — the `restore` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing. Delete the key — a dispatched `restore` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **allowPurge** | `never` | optional | [REMOVED] `objects..allowPurge` was removed in @objectstack/spec 17 (ADR-0049) — the `purge` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing (a compliance/GDPR erase the author believed was permission-locked was not — the operation itself does not exist). Delete the key — a dispatched `purge` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **allowRestore** | `never` | optional | [REMOVED] `objects..allowRestore` was removed in @objectstack/spec 17 (ADR-0049) — the `restore` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing. Delete the key — a dispatched `restore` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **allowPurge** | `never` | optional | [REMOVED] `objects..allowPurge` was removed in @objectstack/spec 17 (ADR-0049) — the `purge` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing (a compliance/GDPR erase the author believed was permission-locked was not — the operation itself does not exist). Delete the key — a dispatched `purge` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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. | | **readScope** | `Enum<'own' \| 'own_and_reports' \| 'unit' \| 'unit_and_below' \| 'org'>` | optional | [ADR-0057 D1] Read depth: own\|unit\|unit_and_below\|org | @@ -145,8 +145,8 @@ const result = AdminScopeSchema.parse(data); | **allowDelete** | `boolean` | optional (default: `false`) | Delete permission | | **allowExport** | `boolean` | optional | 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 via the insert/update owner_id guard] Change record ownership (assign/reassign/disown owner_id) | -| **allowRestore** | `never` | optional | [REMOVED] `objects..allowRestore` was removed in @objectstack/spec 17 (ADR-0049) — the `restore` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing. Delete the key — a dispatched `restore` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **allowPurge** | `never` | optional | [REMOVED] `objects..allowPurge` was removed in @objectstack/spec 17 (ADR-0049) — the `purge` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing (a compliance/GDPR erase the author believed was permission-locked was not — the operation itself does not exist). Delete the key — a dispatched `purge` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **allowRestore** | `never` | optional | [REMOVED] `objects..allowRestore` was removed in @objectstack/spec 17 (ADR-0049) — the `restore` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing. Delete the key — a dispatched `restore` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **allowPurge** | `never` | optional | [REMOVED] `objects..allowPurge` was removed in @objectstack/spec 17 (ADR-0049) — the `purge` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing (a compliance/GDPR erase the author believed was permission-locked was not — the operation itself does not exist). Delete the key — a dispatched `purge` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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. | | **readScope** | `Enum<'own' \| 'own_and_reports' \| 'unit' \| 'unit_and_below' \| 'org'>` | optional | [ADR-0057 D1] Read depth: own\|unit\|unit_and_below\|org | @@ -172,8 +172,8 @@ const result = AdminScopeSchema.parse(data); | **check** | `string` | optional | Validation condition judged on every row an insert or an update writes (enforced at application level): each row of an insert, an array insert included, as its `beforeInsert` hooks leave it, and each row an update changes, by id or `multi: true`, as the prior row merged with the final payload after its `beforeUpdate` hooks. One failing row refuses the whole write. A by-id update is also judged, before its hooks, on the prior row merged with the change set as sent. The default to `using` is decided per operation across the applicable policies, not per policy: when any applicable policy for that operation declares `check`, only the declared checks decide (OR-combined) and a USING-only sibling adds nothing; only when none declares `check` does each applicable policy's `using` stand in as its check (OR-combined). Applicable = not `enabled: false`, `object` matches or is '*', `operation` matches or is 'all', and the caller holds one of its `positions` when it lists any. Refused on a policy whose `operation` is `select` or `delete`, which write no new row to check: limit the rows such a policy admits with `using`, and declare the `check` on an `insert`, `update` or `all` policy. | | **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. It never had an effect. 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** | `never` | optional | [REMOVED] `rowLevelSecurity[].tags` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — nothing ever read it: the RLS compiler never consulted a policy's tags and nothing else acted on them, so a tag scoped, restricted and reported nothing. Delete the key. A tag never limited whom a policy applies to; to do that, list the positions in `positions`. A policy is identified by its `name` and its `object`; say why it exists in `description`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **priority** | `never` | optional | [REMOVED] `rowLevelSecurity[].priority` was removed in @objectstack/spec 17.0.0. It never had an effect. Delete the key — policy outcomes are unchanged. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **tags** | `never` | optional | [REMOVED] `rowLevelSecurity[].tags` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — nothing ever read it: the RLS compiler never consulted a policy's tags and nothing else acted on them, so a tag scoped, restricted and reported nothing. Delete the key. A tag never limited whom a policy applies to; to do that, list the positions in `positions`. A policy is identified by its `name` and its `object`; say why it exists in `description`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | ### Nested Shape: `PermissionSet.adminScope` diff --git a/content/docs/references/security/rls.mdx b/content/docs/references/security/rls.mdx index 411389d91d1..f7e2dbaef10 100644 --- a/content/docs/references/security/rls.mdx +++ b/content/docs/references/security/rls.mdx @@ -180,8 +180,8 @@ const result = RLSEvaluationResultSchema.parse(data); | **check** | `string` | optional | Validation condition judged on every row an insert or an update writes (enforced at application level): each row of an insert, an array insert included, as its `beforeInsert` hooks leave it, and each row an update changes, by id or `multi: true`, as the prior row merged with the final payload after its `beforeUpdate` hooks. One failing row refuses the whole write. A by-id update is also judged, before its hooks, on the prior row merged with the change set as sent. The default to `using` is decided per operation across the applicable policies, not per policy: when any applicable policy for that operation declares `check`, only the declared checks decide (OR-combined) and a USING-only sibling adds nothing; only when none declares `check` does each applicable policy's `using` stand in as its check (OR-combined). Applicable = not `enabled: false`, `object` matches or is '*', `operation` matches or is 'all', and the caller holds one of its `positions` when it lists any. Refused on a policy whose `operation` is `select` or `delete`, which write no new row to check: limit the rows such a policy admits with `using`, and declare the `check` on an `insert`, `update` or `all` policy. | | **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. It never had an effect. 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** | `never` | optional | [REMOVED] `rowLevelSecurity[].tags` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — nothing ever read it: the RLS compiler never consulted a policy's tags and nothing else acted on them, so a tag scoped, restricted and reported nothing. Delete the key. A tag never limited whom a policy applies to; to do that, list the positions in `positions`. A policy is identified by its `name` and its `object`; say why it exists in `description`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **priority** | `never` | optional | [REMOVED] `rowLevelSecurity[].priority` was removed in @objectstack/spec 17.0.0. It never had an effect. Delete the key — policy outcomes are unchanged. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **tags** | `never` | optional | [REMOVED] `rowLevelSecurity[].tags` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — nothing ever read it: the RLS compiler never consulted a policy's tags and nothing else acted on them, so a tag scoped, restricted and reported nothing. Delete the key. A tag never limited whom a policy applies to; to do that, list the positions in `positions`. A policy is identified by its `name` and its `object`; say why it exists in `description`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | --- diff --git a/content/docs/references/shared/mapping.mdx b/content/docs/references/shared/mapping.mdx index 73f2928447e..ca355d829f9 100644 --- a/content/docs/references/shared/mapping.mdx +++ b/content/docs/references/shared/mapping.mdx @@ -76,7 +76,7 @@ const result = FieldMappingSchema.parse(data); | :--- | :--- | :--- | :--- | | **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 (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. 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. | +| **transform** | `never` | optional | [REMOVED] `FieldMapping.transform` — authored as `connector.fieldMappings[].transform` and `externalLookup.fieldMappings[].transform` — was removed in @objectstack/spec 17.0.0 (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. 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; `--write` applies the ones it can prove, and you apply the rest by hand. | | **defaultValue** | `any` | optional | Default if source is null/undefined | diff --git a/content/docs/references/system/book.mdx b/content/docs/references/system/book.mdx index 66d4f7065ed..fd613241dd3 100644 --- a/content/docs/references/system/book.mdx +++ b/content/docs/references/system/book.mdx @@ -68,7 +68,7 @@ const result = BookSchema.parse(data); | :--- | :--- | :--- | :--- | | **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 (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. | +| **translations** | `never` | optional | [REMOVED] Inline `translations` on a book (and on a book group) was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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) | @@ -116,7 +116,7 @@ Type: `'public'` | :--- | :--- | :--- | :--- | | **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 (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. | +| **translations** | `never` | optional | [REMOVED] Inline `translations` on a book (and on a book group) was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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) | diff --git a/content/docs/references/system/job.mdx b/content/docs/references/system/job.mdx index 509dd269d80..b14ac2f4a08 100644 --- a/content/docs/references/system/job.mdx +++ b/content/docs/references/system/job.mdx @@ -63,7 +63,7 @@ const result = CronScheduleSchema.parse(data); | **organization** | `string` | optional | Organization id (sys_organization.id) this job runs as — its `body`'s `ctx.api`, the execution context its `handler` is handed, and its `pull`'s reads and writes alike, as a system run carrying that organization. A scheduled run has no session to inherit one from. Judged at bind by the posture rule scheduled flows use: required under the isolated tenancy posture (a job that declares none is not scheduled); optional under group (undeclared, the run carries no organization and a tenant-scoped write it makes is refused); not required under single (the install's one organization is resolved beneath each write). Where declared, it is the organization the run acts as on every posture. | | **retryPolicy** | `{ maxRetries?: integer; backoffMs?: integer; backoffMultiplier?: number; maxRetryDelayMs?: integer; … }` | optional | Retry policy: failed runs (including timeouts) are retried with exponential backoff (delay = min(backoffMs * backoffMultiplier^(retry-1), maxRetryDelayMs), optionally jittered) up to maxRetries retries after the initial attempt. Omit the block for a single attempt; declaring it without `maxRetries` also means no retry since 17.0.0 — state a count to opt in. | | **timeoutMs** | `integer` | optional | Per-attempt time limit in milliseconds; an over-limit run is recorded with execution status "timeout". A `handler` run is abandoned, not forcibly cancelled. For a job with a `body` this is the ONE time limit: one attempt is one sandbox invocation, the runtime bounds that invocation by this value, and the body shape's own `timeoutMs` (capped at 30000 for hooks and actions) is refused on a job — so this key, which has no such cap, is where long-running work states how long it needs. Omit for no per-attempt limit; a `body` run is then still bounded by the sandbox's own default invocation limits. | -| **timeout** | `never` | optional | [REMOVED] `job.timeout` was removed in @objectstack/spec 17 — its unit (milliseconds) lived only in the description while the sibling `retryPolicy.backoffMs` spells its own, so the same number read as two conventions on one surface. Rename the key to `timeoutMs`; the value (milliseconds) is unchanged. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **timeout** | `never` | optional | [REMOVED] `job.timeout` was removed in @objectstack/spec 17 — its unit (milliseconds) lived only in the description while the sibling `retryPolicy.backoffMs` spells its own, so the same number read as two conventions on one surface. Rename the key to `timeoutMs`; the value (milliseconds) is unchanged. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **enabled** | `boolean` | optional (default: `true`) | Whether the job is enabled | | **_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. | @@ -120,7 +120,7 @@ const result = CronScheduleSchema.parse(data); | **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 — 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. | +| **retryDelayMs** | `never` | optional | [REMOVED] `retryDelayMs` was removed in @objectstack/spec 17.0.0 — 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; `--write` applies the ones it can prove, and you apply the rest by hand. | --- @@ -177,7 +177,7 @@ const result = CronScheduleSchema.parse(data); | **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 — 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. | +| **retryDelayMs** | `never` | optional | [REMOVED] `retryDelayMs` was removed in @objectstack/spec 17.0.0 — 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; `--write` applies the ones it can prove, and you apply the rest by hand. | --- diff --git a/content/docs/references/system/migration.mdx b/content/docs/references/system/migration.mdx index f1c45c4e75a..1353735f25c 100644 --- a/content/docs/references/system/migration.mdx +++ b/content/docs/references/system/migration.mdx @@ -107,7 +107,7 @@ Add a new field to an existing object | **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'`. Reads the bound record's OWN columns: the field level never reads a related record, so a read THROUGH a reference field (`record.account.tier`) faults on every row and refuses every update that writes the field — `objectstack validate` refuses it. Put such a check in a `validations[]` `script` rule, whose `condition` is read one hop through a reference. | | **requiredWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is required when TRUE. A TRANSITION GATE, not an invariant: the write is refused only when the merged record violates the requirement AND the pre-write record complied — so the write that flips the predicate TRUE, an INSERT born inside the gate, and a write that clears the cell are all refused, while a row that was already missing the value keeps passing unrelated edits and state moves that stay inside the gate (ADR-0113 non-regression: adding the rule to a deployed object never bricks existing rows). Need an invariant every write must satisfy instead ('X may never exceed Y') — declare a `validations[]` `script` rule, which re-checks the merged record with no exemption. Enforced by `evaluateValidationRules`. Reads the bound record's OWN columns: the field level never reads a related record, so a read THROUGH a reference field (`record.account.tier`) faults on every row and refuses every write that reaches it — `objectstack validate` refuses it; put such a check in a `validations[]` `script` rule, whose `condition` is read one hop through a reference. The only slot; the `conditionalRequired` alias was removed in protocol 17. | -| **conditionalRequired** | `never` | optional | [REMOVED] `conditionalRequired` was removed in @objectstack/spec 17 — 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. | +| **conditionalRequired** | `never` | optional | [REMOVED] `conditionalRequired` was removed in @objectstack/spec 17 — 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; `--write` applies the ones it can prove, and you apply the rest 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 | 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. | @@ -529,7 +529,7 @@ Add a new field to an existing object | **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'`. Reads the bound record's OWN columns: the field level never reads a related record, so a read THROUGH a reference field (`record.account.tier`) faults on every row and refuses every update that writes the field — `objectstack validate` refuses it. Put such a check in a `validations[]` `script` rule, whose `condition` is read one hop through a reference. | | **requiredWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is required when TRUE. A TRANSITION GATE, not an invariant: the write is refused only when the merged record violates the requirement AND the pre-write record complied — so the write that flips the predicate TRUE, an INSERT born inside the gate, and a write that clears the cell are all refused, while a row that was already missing the value keeps passing unrelated edits and state moves that stay inside the gate (ADR-0113 non-regression: adding the rule to a deployed object never bricks existing rows). Need an invariant every write must satisfy instead ('X may never exceed Y') — declare a `validations[]` `script` rule, which re-checks the merged record with no exemption. Enforced by `evaluateValidationRules`. Reads the bound record's OWN columns: the field level never reads a related record, so a read THROUGH a reference field (`record.account.tier`) faults on every row and refuses every write that reaches it — `objectstack validate` refuses it; put such a check in a `validations[]` `script` rule, whose `condition` is read one hop through a reference. The only slot; the `conditionalRequired` alias was removed in protocol 17. | -| **conditionalRequired** | `never` | optional | [REMOVED] `conditionalRequired` was removed in @objectstack/spec 17 — 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. | +| **conditionalRequired** | `never` | optional | [REMOVED] `conditionalRequired` was removed in @objectstack/spec 17 — 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; `--write` applies the ones it can prove, and you apply the rest 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 | 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. | diff --git a/content/docs/references/ui/action.mdx b/content/docs/references/ui/action.mdx index b1d00f690e4..7d558d9bea9 100644 --- a/content/docs/references/ui/action.mdx +++ b/content/docs/references/ui/action.mdx @@ -42,7 +42,7 @@ const result = ActionSchema.parse(data); | **operation** | `Enum<'update'>` | optional | The declarative single-record field write, mirroring a list view's `bulkActionDefs`: `'update'` applies `patch` (merged under the collected `params`) to the current record on the data plane AS THE CALLER — never system-elevated — so the caller's permissions, the object's hooks and its validations fire as for a user edit. `type` stays at its default `'script'` (the platform action route the write is performed on); `target`/`body`/`method`/`bodyExtra` are refused beside it. `'delete'` and `'custom'` have no row-level form. | | **patch** | `Record` | optional | For `operation: 'update'` — static field values written to the current record, merged UNDER the user-supplied `params` so a fixed value can be declared without exposing it in the dialog. Written on the data plane as the caller: object permissions, hooks and validations fire as for a user edit. Refused on an action without `operation: 'update'` (it would be silently dropped). | | **execution** | `Enum<'perRecord' \| 'aggregate'>` | optional | The bulk dispatch contract this action's BODY is written for, in `bulkActionDefs`' own vocabulary: 'perRecord' = one dispatch per selected row carrying that row's `recordId` (the view's `bulkActions: ['']` bare-string form); 'aggregate' = ONE dispatch for the whole selection carrying every id in `params._selectedIds` (a `bulkActionDefs` entry with `execution: 'aggregate'`). Optional with NO default — omit it only when the body genuinely serves both. A list view wiring a declared action under the other contract is refused by `@objectstack/lint` (`action-dispatch-contract-mismatch`). | -| **execute** | `never` | optional | [REMOVED] `execute` was removed in @objectstack/spec 17 — 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. | +| **execute** | `never` | optional | [REMOVED] `execute` was removed in @objectstack/spec 17 — 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; `--write` applies the ones it can prove, and you apply the rest 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. | @@ -58,8 +58,8 @@ const result = ActionSchema.parse(data); | **requiresMembershipReach** | `Enum<'invite_member' \| 'cancel_invitation' \| 'update_member_role' \| 'transfer_ownership' \| 'remove_member' \| 'create_team' \| 'update_team' \| 'remove_team' \| … +2 more>` | optional | Organization endpoint (a MEMBERSHIP_REACH row) whose membership-grade gate this action follows; lowered into `visible` over current_user.positions 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 (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 (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. | +| **shortcut** | `never` | optional | [REMOVED] `action.shortcut` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **bulkEnabled** | `never` | optional | [REMOVED] `action.bulkEnabled` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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". | @@ -70,7 +70,7 @@ const result = ActionSchema.parse(data); | **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. `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** | `never` | optional | [REMOVED] `action.aria` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no action surface ever applied it: the button, icon, menu, group and bar renderers, the row and bulk action menus and the record quick-actions toolbar all take the accessible name from the action's `label` and never read this block, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied is the action's required `label` — the visible button or menu-item text, and the `aria-label` of an icon-only action — so write the name you meant there. To name the region that PLACES the actions, author `ariaLabel` / `ariaDescribedBy` / `role` in the `aria` block of the placing node: `page.components[].aria` (the component that renders the actions) or the list view `aria`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **aria** | `never` | optional | [REMOVED] `action.aria` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no action surface ever applied it: the button, icon, menu, group and bar renderers, the row and bulk action menus and the record quick-actions toolbar all take the accessible name from the action's `label` and never read this block, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied is the action's required `label` — the visible button or menu-item text, and the `aria-label` of an icon-only action — so write the name you meant there. To name the region that PLACES the actions, author `ariaLabel` / `ariaDescribedBy` / `role` in the `aria` block of the placing node: `page.components[].aria` (the component that renders the actions) or the list view `aria`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **_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). | diff --git a/content/docs/references/ui/app.mdx b/content/docs/references/ui/app.mdx index 6ec5862b041..c3ec3d874d2 100644 --- a/content/docs/references/ui/app.mdx +++ b/content/docs/references/ui/app.mdx @@ -59,7 +59,7 @@ const result = ActionNavItemSchema.parse(data); | :--- | :--- | :--- | :--- | | **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. | +| **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; `--write` applies the ones it can prove, and you apply the rest 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 | @@ -70,15 +70,15 @@ const result = ActionNavItemSchema.parse(data); | **navigation** | `({ id: string; label?: string \| Record; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| … +8 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 (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. | +| **homePageId** | `never` | optional | [REMOVED] `app.homePageId` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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. Before the executor landed 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 — removed in @objectstack/spec 17: nothing ever read it, and authoring it is now rejected with its own prescription). 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. | +| **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; `--write` applies the ones it can prove, and you apply the rest 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. Before the executor landed 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 — removed in @objectstack/spec 17: nothing ever read it, and authoring it is now rejected with its own prescription). Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest 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; `--write` applies the ones it can prove, and you apply the rest 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; `--write` applies the ones it can prove, and you apply the rest 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; `--write` applies the ones it can prove, and you apply the rest 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. | +| **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; `--write` applies the ones it can prove, and you apply the rest 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. | diff --git a/content/docs/references/ui/chart.mdx b/content/docs/references/ui/chart.mdx index ae29db3cfff..c8a04427930 100644 --- a/content/docs/references/ui/chart.mdx +++ b/content/docs/references/ui/chart.mdx @@ -118,7 +118,7 @@ Inline aggregation for an object-bound chart | **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** | `never` | optional | [REMOVED] `ChartConfig.aria` — authored as `dashboard.widgets[].chartConfig.aria`, `report.chart.aria` and `report.blocks[].chart.aria` — was removed in @objectstack/spec 17 (ADR-0049 D2). No chart renderer ever applied it: the chart implementation declares no `aria` prop, the presentation lowering names it nowhere, and the react `` block never published it, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied on this same chart config is its sibling `description`, which the chart renderer lowers onto the chart graphic as `role="img"` plus `aria-label`. The shared `AriaProps` shape is NOT gone — `ariaLabel` / `ariaDescribedBy` / `role` stay live in the `aria` block on `page.aria`, `page.components[].aria` and the list view `aria`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **aria** | `never` | optional | [REMOVED] `ChartConfig.aria` — authored as `dashboard.widgets[].chartConfig.aria`, `report.chart.aria` and `report.blocks[].chart.aria` — was removed in @objectstack/spec 17 (ADR-0049 D2). No chart renderer ever applied it: the chart implementation declares no `aria` prop, the presentation lowering names it nowhere, and the react `` block never published it, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied on this same chart config is its sibling `description`, which the chart renderer lowers onto the chart graphic as `role="img"` plus `aria-label`. The shared `AriaProps` shape is NOT gone — `ariaLabel` / `ariaDescribedBy` / `role` stay live in the `aria` block on `page.aria`, `page.components[].aria` and the list view `aria`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | ### Allowed Values: `ChartConfig.type` diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index 2160ec44059..e56b72e0d3c 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -262,12 +262,12 @@ const result = ActionButtonPropsSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **object** | `never` | optional | [REMOVED] `element:filter` property `object` was removed in @objectstack/spec 17 (ADR-0049) — the whole `element:filter` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion), so every key on this element was a capability claim nothing kept. Delete the `element:filter` component; list surfaces own their filtering — use a view's `userFilters` quick-filter bar or the list toolbar's filter builder. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **fields** | `never` | optional | [REMOVED] `element:filter` property `fields` was removed in @objectstack/spec 17 (ADR-0049) — the whole `element:filter` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion), so every key on this element was a capability claim nothing kept. Delete the `element:filter` component; list surfaces own their filtering — use a view's `userFilters` quick-filter bar or the list toolbar's filter builder. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **targetVariable** | `never` | optional | [REMOVED] `element:filter` property `targetVariable` was removed in @objectstack/spec 17 (ADR-0049) — the whole `element:filter` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion), so every key on this element was a capability claim nothing kept. Delete the `element:filter` component; list surfaces own their filtering — use a view's `userFilters` quick-filter bar or the list toolbar's filter builder. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **layout** | `never` | optional | [REMOVED] `element:filter` property `layout` was removed in @objectstack/spec 17 (ADR-0049) — the whole `element:filter` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion), so every key on this element was a capability claim nothing kept. Delete the `element:filter` component; list surfaces own their filtering — use a view's `userFilters` quick-filter bar or the list toolbar's filter builder. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **showSearch** | `never` | optional | [REMOVED] `element:filter` property `showSearch` was removed in @objectstack/spec 17 (ADR-0049) — the whole `element:filter` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion), so every key on this element was a capability claim nothing kept. Delete the `element:filter` component; list surfaces own their filtering — use a view's `userFilters` quick-filter bar or the list toolbar's filter builder. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **aria** | `never` | optional | [REMOVED] `element:filter` property `aria` was removed in @objectstack/spec 17 (ADR-0049) — the whole `element:filter` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion), so every key on this element was a capability claim nothing kept. Delete the `element:filter` component; list surfaces own their filtering — use a view's `userFilters` quick-filter bar or the list toolbar's filter builder. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **object** | `never` | optional | [REMOVED] `element:filter` property `object` was removed in @objectstack/spec 17 (ADR-0049) — the whole `element:filter` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion), so every key on this element was a capability claim nothing kept. Delete the `element:filter` component; list surfaces own their filtering — use a view's `userFilters` quick-filter bar or the list toolbar's filter builder. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **fields** | `never` | optional | [REMOVED] `element:filter` property `fields` was removed in @objectstack/spec 17 (ADR-0049) — the whole `element:filter` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion), so every key on this element was a capability claim nothing kept. Delete the `element:filter` component; list surfaces own their filtering — use a view's `userFilters` quick-filter bar or the list toolbar's filter builder. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **targetVariable** | `never` | optional | [REMOVED] `element:filter` property `targetVariable` was removed in @objectstack/spec 17 (ADR-0049) — the whole `element:filter` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion), so every key on this element was a capability claim nothing kept. Delete the `element:filter` component; list surfaces own their filtering — use a view's `userFilters` quick-filter bar or the list toolbar's filter builder. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **layout** | `never` | optional | [REMOVED] `element:filter` property `layout` was removed in @objectstack/spec 17 (ADR-0049) — the whole `element:filter` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion), so every key on this element was a capability claim nothing kept. Delete the `element:filter` component; list surfaces own their filtering — use a view's `userFilters` quick-filter bar or the list toolbar's filter builder. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **showSearch** | `never` | optional | [REMOVED] `element:filter` property `showSearch` was removed in @objectstack/spec 17 (ADR-0049) — the whole `element:filter` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion), so every key on this element was a capability claim nothing kept. Delete the `element:filter` component; list surfaces own their filtering — use a view's `userFilters` quick-filter bar or the list toolbar's filter builder. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **aria** | `never` | optional | [REMOVED] `element:filter` property `aria` was removed in @objectstack/spec 17 (ADR-0049) — the whole `element:filter` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion), so every key on this element was a capability claim nothing kept. Delete the `element:filter` component; list surfaces own their filtering — use a view's `userFilters` quick-filter bar or the list toolbar's filter builder. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | --- @@ -278,12 +278,12 @@ const result = ActionButtonPropsSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **object** | `never` | optional | [REMOVED] `element:form` property `object` was removed in @objectstack/spec 17 (ADR-0049) — the whole `element:form` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **fields** | `never` | optional | [REMOVED] `element:form` property `fields` was removed in @objectstack/spec 17 (ADR-0049) — the whole `element:form` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **mode** | `never` | optional | [REMOVED] `element:form` property `mode` was removed in @objectstack/spec 17 (ADR-0049) — the whole `element:form` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **submitLabel** | `never` | optional | [REMOVED] `element:form` property `submitLabel` was removed in @objectstack/spec 17 (ADR-0049) — the whole `element:form` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **onSubmit** | `never` | optional | [REMOVED] `element:form` property `onSubmit` was removed in @objectstack/spec 17 (ADR-0049) — the whole `element:form` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **aria** | `never` | optional | [REMOVED] `element:form` property `aria` was removed in @objectstack/spec 17 (ADR-0049) — the whole `element:form` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **object** | `never` | optional | [REMOVED] `element:form` property `object` was removed in @objectstack/spec 17 (ADR-0049) — the whole `element:form` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **fields** | `never` | optional | [REMOVED] `element:form` property `fields` was removed in @objectstack/spec 17 (ADR-0049) — the whole `element:form` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **mode** | `never` | optional | [REMOVED] `element:form` property `mode` was removed in @objectstack/spec 17 (ADR-0049) — the whole `element:form` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **submitLabel** | `never` | optional | [REMOVED] `element:form` property `submitLabel` was removed in @objectstack/spec 17 (ADR-0049) — the whole `element:form` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **onSubmit** | `never` | optional | [REMOVED] `element:form` property `onSubmit` was removed in @objectstack/spec 17 (ADR-0049) — the whole `element:form` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **aria** | `never` | optional | [REMOVED] `element:form` property `aria` was removed in @objectstack/spec 17 (ADR-0049) — the whole `element:form` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | --- @@ -384,12 +384,12 @@ View filter rule | **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Filter rules narrowing which records the picker offers — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography the array-declared `filter` doors of this map share. The MongoDB-style record form is refused — see migration `element-record-picker-filter-rule-array`. The binding-level `dataSource.filter` wins outright when both are set | | **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Row order — synonym of the component-level `dataSource.sort`, which takes precedence when both are set | | **limit** | `integer` | optional | Max records offered — synonym of the component-level `dataSource.limit`, which takes precedence when both are set (renderer default 50) | -| **targetVariable** | `never` | optional | [REMOVED] `element:record_picker` property `targetVariable` was removed in @objectstack/spec 17 (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 picked record id, declare it on the variable — `variables: [{ name: '', type: 'record_id', source: '' }]`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **targetVariable** | `never` | optional | [REMOVED] `element:record_picker` property `targetVariable` was removed in @objectstack/spec 17 (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 picked record id, declare it on the variable — `variables: [{ name: '', type: 'record_id', source: '' }]`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **placeholder** | `string \| Record` | optional | Placeholder text | | **emptyText** | `string \| Record` | optional | Text shown when the query returns no records (default "No records") | -| **displayField** | `never` | optional | [REMOVED] `element:record_picker` property `displayField` was removed in @objectstack/spec 17.0.0 (ADR-0087 D2) — it was a required declaration no renderer ever read, while the renderer honoured `labelField` for the same thing and defaulted to `name`. Rename the key to `labelField`; the value (a field name) is unchanged. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | -| **searchFields** | `never` | optional | [REMOVED] `element:record_picker` property `searchFields` was removed in @objectstack/spec 17.0.0 (ADR-0049) — the picker renders a plain single-select with no search input, so no renderer ever read it and it narrowed nothing. Delete the key. To restrict which records the picker offers, use `filter` (or the component-level `dataSource.filter`), which the query path does apply. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | -| **multiple** | `never` | optional | [REMOVED] `element:record_picker` property `multiple` was removed in @objectstack/spec 17.0.0 (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. | +| **displayField** | `never` | optional | [REMOVED] `element:record_picker` property `displayField` was removed in @objectstack/spec 17.0.0 (ADR-0087 D2) — it was a required declaration no renderer ever read, while the renderer honoured `labelField` for the same thing and defaulted to `name`. Rename the key to `labelField`; the value (a field name) is unchanged. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **searchFields** | `never` | optional | [REMOVED] `element:record_picker` property `searchFields` was removed in @objectstack/spec 17.0.0 (ADR-0049) — the picker renders a plain single-select with no search input, so no renderer ever read it and it narrowed nothing. Delete the key. To restrict which records the picker offers, use `filter` (or the component-level `dataSource.filter`), which the query path does apply. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **multiple** | `never` | optional | [REMOVED] `element:record_picker` property `multiple` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | ### Nested Shape: `ElementRecordPickerProps.filter[number]` @@ -478,7 +478,7 @@ Sort field and direction pair | **required** | `boolean` | optional (default: `false`) | Mark the field as required | | **disabled** | `boolean` | optional (default: `false`) | Disable the input | | **description** | `string \| Record` | optional | Helper text shown below the input | -| **targetVariable** | `never` | optional | [REMOVED] `element:text_input` property `targetVariable` was removed in @objectstack/spec 17 (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. | +| **targetVariable** | `never` | optional | [REMOVED] `element:text_input` property `targetVariable` was removed in @objectstack/spec 17 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | ### Nested Shape: `ElementTextInputProps.aria` @@ -847,7 +847,7 @@ Sort field and direction pair | **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Base query filter — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` door in this map shares; lowered to the wire `$filter`. THE key, singular — not the plural misspelling. The MongoDB-style record form is refused — see migration `element-data-source-and-object-block-filter-rule-array` | | **defaultFilters** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Legacy base-filter fallback, read only when `filter` is absent — the SAME ViewFilterRule array form `[{ field, operator, value }, ...]` as `filter`, lowered through the same sink. Prefer `filter`. The MongoDB-style record form, a bare string and an ObjectQL AST tuple array are refused — see migration `object-grid-default-filters-rule-array` | | **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Initial row order — the SortItem array form `[{ field, order }, ...]`, the one sort orthography every declared `sort` door on this platform shares; lowered to the wire `$orderby`. The legacy string clause (`name desc`) is refused — see migration `object-block-sort-item-array` | -| **defaultSort** | `never` | optional | [REMOVED] `object-grid` property `defaultSort` was removed in @objectstack/spec 17 (ADR-0049) — it was the legacy second spelling of `sort`: a single `{ field, order }` pair read only when `sort` was absent, so one intent had two spellings and a grid authoring both silently ignored this one. Rename the key to `sort` and wrap the value in an array (`defaultSort: { field, order }` becomes `sort: [{ field, order }]`); the pair itself is unchanged. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **defaultSort** | `never` | optional | [REMOVED] `object-grid` property `defaultSort` was removed in @objectstack/spec 17 (ADR-0049) — it was the legacy second spelling of `sort`: a single `{ field, order }` pair read only when `sort` was absent, so one intent had two spellings and a grid authoring both silently ignored this one. Rename the key to `sort` and wrap the value in an array (`defaultSort: { field, order }` becomes `sort: [{ field, order }]`); the pair itself is unchanged. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **pagination** | `{ pageSize?: integer; pageSizeOptions?: integer[] } & Record` | optional | Pagination config (`{ pageSize, pageSizeOptions, … }`); its presence enables paging. `pageSize` and every `pageSizeOptions` entry is a positive integer — the accept set the view arm's `PaginationConfigSchema` already rules; the bag stays open, so other keys pass through unvalidated | | **pageSize** | `integer` | optional | Flat page-size shorthand, a positive integer; `pagination.pageSize` wins when both are set | | **showPagination** | `boolean` | optional | Show the pager (read only when `pagination` is absent) | @@ -869,7 +869,7 @@ Sort field and direction pair | **singleClickEdit** | `boolean` | optional | Enter cell edit on single click (default true when editable) | | **keyboardNavigation** | `boolean` | optional | Arrow-key cell navigation on the WAI-ARIA grid pattern: the grid's data cells become one Tab stop that the arrow keys, Home / End and Ctrl+Home / Ctrl+End move between. Defaults to on when the grid renders editable — `editable` set and the viewer allowed to edit; a grid that renders read-only keeps every cell its own Tab stop unless this is `true`, and `false` turns it off on an editable grid | | **resizable** | `boolean` | optional | Allow column resize (the renderer default is on) | -| **resizableColumns** | `never` | optional | [REMOVED] `object-grid` property `resizableColumns` was removed in @objectstack/spec 17.7.0 (ADR-0049) — it was the legacy second spelling of `resizable`, read only when `resizable` was absent, so one switch had two spellings and a grid authoring both silently ignored this one. Use `resizable`. Rename the key; the value (a boolean) is unchanged. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **resizableColumns** | `never` | optional | [REMOVED] `object-grid` property `resizableColumns` was removed in @objectstack/spec 17.7.0 (ADR-0049) — it was the legacy second spelling of `resizable`, read only when `resizable` was absent, so one switch had two spellings and a grid authoring both silently ignored this one. Use `resizable`. Rename the key; the value (a boolean) is unchanged. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **reorderableColumns** | `boolean` | optional | Allow column drag-reorder | | **frozenColumns** | `number` | optional | How many leading columns stay frozen (default 1) | | **showColumnTypeIcons** | `boolean` | optional | Show field-type icons in column headers | @@ -1066,7 +1066,7 @@ Sort field and direction pair | **cardFields** | `string[]` | optional | Fields rendered on each card | | **swimlaneField** | `string` | optional | Field for horizontal swimlanes (in addition to columns) | | **grouping** | `{ fields: object[] }` | optional | View grouping config; its first field is the swimlane fallback | -| **quickAdd** | `never` | optional | [REMOVED] `object-kanban` property `quickAdd` was removed in @objectstack/spec 17 (ADR-0049) — the board forwarded it, but the per-column affordance is gated on both `quickAdd` and `onQuickAdd`, and `onQuickAdd` is a host-supplied function JSON cannot carry and no producer ever put on an `object-kanban` node, so authoring it was a parse-clean no-op. Delete the key; `object-kanban` offers no quick-add control. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **quickAdd** | `never` | optional | [REMOVED] `object-kanban` property `quickAdd` was removed in @objectstack/spec 17 (ADR-0049) — the board forwarded it, but the per-column affordance is gated on both `quickAdd` and `onQuickAdd`, and `onQuickAdd` is a host-supplied function JSON cannot carry and no producer ever put on an `object-kanban` node, so authoring it was a parse-clean no-op. Delete the key; `object-kanban` offers no quick-add control. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **coverImageField** | `string` | optional | Image field rendered as the card cover | | **conditionalFormatting** | `{ condition: string \| object; style: Record }[]` | optional | Card conditional formatting rules — `[{ condition, style }]`, the same rules a list view declares: the first rule whose CEL `condition` holds applies its CSS `style` map to that card | | **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'>; … }` | optional | Card-click navigation config — the same block `ListViewSchema.navigation` declares (`{ mode, size, openNewTab, preventNavigation }`). The renderer's own default is `{ mode: 'drawer' }` when the key is absent; it is documented rather than declared, so a parsed board carries the key only when the author wrote it | @@ -1257,7 +1257,7 @@ Sort field and direction pair | **formFields** | `string[]` | optional | Child field names for the per-row expand form. When omitted they are derived from the child object's fields — except on an entry that names both `relationshipField` and at least one column, which is kept as authored: nothing is derived, and the per-row form is offered only when `inlineMode` is 'form', where it draws the child object's full field list | | **inlineMode** | `Enum<'grid' \| 'form'>` | optional | Inline-edit form factor: 'grid' = editable cells; 'form' = read-only list + per-row full form. When omitted it is resolved from the relationship field's `inlineEdit`, else from the child object's shape — except on an entry that names both `relationshipField` and at least one column, which is kept as authored: nothing is resolved, the collection renders as a grid, and the per-row form is offered only when `formFields` lists more fields than `columns` | | **amountField** | `string` | optional | Numeric child column summed for the running total and the `totalField` rollup. When omitted it is picked from the grid's number and currency columns: a computed one, else one named `amount`, `total`, `subtotal`, `line_total`, `line_amount` or `net_amount`, else the last currency column, else the last numeric one — except on an entry that names `relationshipField` and at least one column and gives every column a `type`. The renderer keeps that entry exactly as authored, so nothing is picked. With no `amountField` authored or picked, the sums read a child column named `amount`, and the grid shows a running total only when `totalField` is set | -| **sortField** | `never` | optional | [REMOVED] `object-master-detail-form` property `details[].sortField` was removed in @objectstack/spec 17 (ADR-0087 D2) — the console reads no authored value: the field the line grid stamps with each line's position on drag-reorder is derived from the child object, so an authored `sortField` was accepted and dropped. Delete the key. For a drag-reorder to be saved, give the child object a field named `position` / `sort_order` / `sequence` / `line_no` / `line_number` / `sort`: the renderer stamps the child's first field with one of those names — except on an entry that names `relationshipField` and at least one column and gives every column a `type`, which the renderer keeps exactly as authored and stamps no line position on. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **sortField** | `never` | optional | [REMOVED] `object-master-detail-form` property `details[].sortField` was removed in @objectstack/spec 17 (ADR-0087 D2) — the console reads no authored value: the field the line grid stamps with each line's position on drag-reorder is derived from the child object, so an authored `sortField` was accepted and dropped. Delete the key. For a drag-reorder to be saved, give the child object a field named `position` / `sort_order` / `sequence` / `line_no` / `line_number` / `sort`: the renderer stamps the child's first field with one of those names — except on an entry that names `relationshipField` and at least one column and gives every column a `type`, which the renderer keeps exactly as authored and stamps no line position on. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **totalField** | `string` | optional | Parent field to receive the rolled-up sum | | **title** | `string` | optional | Section title | | **minRows** | `number` | optional | Minimum number of rows | @@ -1542,9 +1542,9 @@ View filter rule | :--- | :--- | :--- | :--- | | **title** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | | **bordered** | `boolean` | optional (default: `true`) | | -| **actions** | `never` | optional | [REMOVED] `page:card` property `actions` was removed in @objectstack/spec 17.0.0 (ADR-0087 D2) — no renderer ever read it: objectui's card renderer builds its `` from `title`, `bordered`, `children` and `footer` only, has no actions area, and the component registry never published it as an input, so an authored value was accepted and dropped. Delete the key and author the buttons as components in the card's `children` or `footer` (`element:button`, `record:quick_actions`), which is what actually renders. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **actions** | `never` | optional | [REMOVED] `page:card` property `actions` was removed in @objectstack/spec 17.0.0 (ADR-0087 D2) — no renderer ever read it: objectui's card renderer builds its `` from `title`, `bordered`, `children` and `footer` only, has no actions area, and the component registry never published it as an input, so an authored value was accepted and dropped. Delete the key and author the buttons as components in the card's `children` or `footer` (`element:button`, `record:quick_actions`), which is what actually renders. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **children** | `any[]` | optional | Card content components, in order (the card body slot) | -| **body** | `never` | optional | [REMOVED] `page:card` property `body` was removed in @objectstack/spec 17.0.0 (ADR-0087 D2) — it was a second spelling of the composition slot every other container calls `children`, and the renderer reads both. Rename the key to `children`; the value (an array of child components) is unchanged. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **body** | `never` | optional | [REMOVED] `page:card` property `body` was removed in @objectstack/spec 17.0.0 (ADR-0087 D2) — it was a second spelling of the composition slot every other container calls `children`, and the renderer reads both. Rename the key to `children`; the value (an array of child components) is unchanged. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **footer** | `any[]` | optional | Card footer components (slot) | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | @@ -1578,8 +1578,8 @@ View filter rule | :--- | :--- | :--- | :--- | | **title** | `string \| Record` | optional | Page title. Omit to let the renderer derive the heading from the record (the default for record pages) — set explicitly on non-record pages (dashboard, landing) with no record to derive from. | | **subtitle** | `string \| Record` | optional | Page subtitle | -| **icon** | `never` | optional | [REMOVED] `page:header` property `icon` was removed in @objectstack/spec 17.0.0 (ADR-0087 D2) — no renderer ever read it: objectui resolves `icon` only per header action (`action.icon`), never off the header's own props bag, and the component registry never published it as an input, so an authored value was accepted and dropped. Delete the key. The header's own identity is drawn by the record chrome (`recordChrome`, on by default) and each action carries its own `icon`. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | -| **breadcrumb** | `never` | optional | [REMOVED] `page:header` property `breadcrumb` was removed in @objectstack/spec 17 (ADR-0087 D2) — no renderer ever drew a trail for it: objectui drew an empty slot and nothing filled it, and the navigation trail is drawn once, by the app shell's header. Delete the key, whether it was `true` or `false`; the shell's trail is unchanged. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **icon** | `never` | optional | [REMOVED] `page:header` property `icon` was removed in @objectstack/spec 17.0.0 (ADR-0087 D2) — no renderer ever read it: objectui resolves `icon` only per header action (`action.icon`), never off the header's own props bag, and the component registry never published it as an input, so an authored value was accepted and dropped. Delete the key. The header's own identity is drawn by the record chrome (`recordChrome`, on by default) and each action carries its own `icon`. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **breadcrumb** | `never` | optional | [REMOVED] `page:header` property `breadcrumb` was removed in @objectstack/spec 17 (ADR-0087 D2) — no renderer ever drew a trail for it: objectui drew an empty slot and nothing filled it, and the navigation trail is drawn once, by the app shell's header. Delete the key, whether it was `true` or `false`; the shell's trail is unchanged. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **actions** | `string[]` | optional | Action IDs to show in header | | **recordChrome** | `boolean` | optional (default: `true`) | Render the record chrome — the title as a record chip with its follow star and copy-id button. Set false on a non-record page (dashboard, landing) to fall back to the bare heading layout. | | **showStar** | `boolean` | optional (default: `true`) | Show the follow (favourite) star beside the record title. Part of the record chrome — no effect when `recordChrome` is false. | @@ -1606,7 +1606,7 @@ View filter rule | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **tabStyle** | `Enum<'line' \| 'card' \| 'pill'>` | optional (default: `"line"`) | Tab-strip visual style: 'line' underlines the active tab, 'card' frames each tab, 'pill' renders rounded pills | -| **type** | `never` | optional | [REMOVED] `page:tabs` property `type` was removed in @objectstack/spec 17.0.0 (ADR-0087 D2) — a props key named `type` collides with the page component's own dispatch key, so it is unauthorable in the flat and JSX carriers and was never validated in them. Rename the key to `tabStyle`; the value (`line` \| `card` \| `pill`) is unchanged. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **type** | `never` | optional | [REMOVED] `page:tabs` property `type` was removed in @objectstack/spec 17.0.0 (ADR-0087 D2) — a props key named `type` collides with the page component's own dispatch key, so it is unauthorable in the flat and JSX carriers and was never validated in them. Rename the key to `tabStyle`; the value (`line` \| `card` \| `pill`) is unchanged. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **position** | `Enum<'top' \| 'left'>` | optional (default: `"top"`) | | | **alwaysShowStrip** | `boolean` | optional | Render the tab strip even when only one tab is visible (renderer default: a one-tab strip is hidden). | | **items** | `{ label: string \| Record; icon?: string; visibleWhen?: string \| object; value?: string; … }[]` | ✅ | | @@ -1751,7 +1751,7 @@ View filter rule | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **columns** | `Enum<'1' \| '2' \| '3' \| '4'>` | optional (default: `"2"`) | Number of columns for field layout (1-4) | -| **layout** | `never` | optional | [REMOVED] `record:details` property `layout` was removed in @objectstack/spec 17.0.0 (ADR-0087 D2) — its declared `auto` \| `custom` semantics were never implemented: the renderer tests `layout` only against `inline` \| `compact`, two values the schema never permitted, so both legal values took the same branch and the key selected nothing. Delete the key — the body is already chosen by what you author: `sections` renders the explicit groups (the old `custom`), and omitting it falls back to the object's `highlightFields` (the old `auto`). Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **layout** | `never` | optional | [REMOVED] `record:details` property `layout` was removed in @objectstack/spec 17.0.0 (ADR-0087 D2) — its declared `auto` \| `custom` semantics were never implemented: the renderer tests `layout` only against `inline` \| `compact`, two values the schema never permitted, so both legal values took the same branch and the key selected nothing. Delete the key — the body is already chosen by what you author: `sections` renders the explicit groups (the old `custom`), and omitting it falls back to the object's `highlightFields` (the old `auto`). Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **sections** | `{ name?: string; label?: string \| Record; columns?: integer; group?: string; … }[]` | optional | Field groups rendered as the detail body, in order. Object form: `{ name?, label?, columns?, fields, hideEmpty?, collapsible?, showBorder?, defaultCollapsed?, icon?, description?, headerColor? }` — or the group-reference form `{ group, columns?, hideEmpty?, showBorder?, headerColor? }`, which inherits members and presentation from the object's `fieldGroups` entry (ADR-0085 §5). | | **fields** | `string[]` | optional | Explicit field list to display (optional, overrides highlightFields) | | **hideFields** | `string[]` | optional | Field names to omit from the body — applied to `fields` and to every section's `fields` (used to dedupe fields already shown in `record:highlights` or as the page title) | diff --git a/content/docs/references/ui/dashboard.mdx b/content/docs/references/ui/dashboard.mdx index 2fc25e9d515..33a530f1d65 100644 --- a/content/docs/references/ui/dashboard.mdx +++ b/content/docs/references/ui/dashboard.mdx @@ -36,11 +36,11 @@ const result = DashboardSchema.parse(data); | **columns** | `integer` | optional | Number of grid columns (default 12) | | **gap** | `integer` | optional | Space between widgets, in steps of 0.25rem (4 = 1rem) | | **refreshIntervalSeconds** | `number` | optional | Auto-refresh interval in seconds | -| **refreshInterval** | `never` | optional | [REMOVED] `dashboard.refreshInterval` was renamed to `refreshIntervalSeconds` in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Rename the key to `refreshIntervalSeconds`; the value (seconds) is unchanged. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **refreshInterval** | `never` | optional | [REMOVED] `dashboard.refreshInterval` was renamed to `refreshIntervalSeconds` in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Rename the key to `refreshIntervalSeconds`; the value (seconds) is unchanged. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **dateRange** | `{ field?: string; defaultRange?: Enum<'today' \| 'yesterday' \| 'this_week' \| 'last_week' \| 'this_month' \| 'last_month' \| …>; allowCustomRange?: boolean }` | optional | Global dashboard date range filter configuration | | **globalFilters** | `{ name?: string; field: string; object?: string; label?: string \| Record; … }[]` | optional | Global filters that apply to all widgets in the dashboard | -| **aria** | `never` | optional | [REMOVED] `dashboard.aria` was removed in @objectstack/spec 17.0.0 (audit close-out) — no dashboard renderer ever applied it, so declared ARIA attributes silently did not reach the DOM. 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] `dashboard.performance` was removed in @objectstack/spec 17.0.0 (audit close-out) — no renderer or runtime read it; dashboard 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. | +| **aria** | `never` | optional | [REMOVED] `dashboard.aria` was removed in @objectstack/spec 17.0.0 (audit close-out) — no dashboard renderer ever applied it, so declared ARIA attributes silently did not reach the DOM. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **performance** | `never` | optional | [REMOVED] `dashboard.performance` was removed in @objectstack/spec 17.0.0 (audit close-out) — no renderer or runtime read it; dashboard performance tuning was never implemented. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this dashboard. | | **_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. | @@ -70,9 +70,9 @@ const result = DashboardSchema.parse(data); | **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 (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 (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 (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. | +| **actionUrl** | `never` | optional | [REMOVED] `dashboard.widgets[].actionUrl` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **actionType** | `never` | optional | [REMOVED] `dashboard.widgets[].actionType` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **actionIcon** | `never` | optional | [REMOVED] `dashboard.widgets[].actionIcon` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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) | @@ -82,8 +82,8 @@ const result = DashboardSchema.parse(data); | **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 (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 with it. 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 (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. 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. | +| **responsive** | `never` | optional | [REMOVED] `dashboard.widgets[].responsive` was removed in @objectstack/spec 17.0.0 (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 with it. 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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **aria** | `never` | optional | [REMOVED] `dashboard.widgets[].aria` was removed in @objectstack/spec 17.0.0 (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. 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; `--write` applies the ones it can prove, and you apply the rest by hand. | ### Nested Shape: `Dashboard.dateRange` @@ -175,9 +175,9 @@ Dashboard header action | **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 (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 (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 (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. | +| **actionUrl** | `never` | optional | [REMOVED] `dashboard.widgets[].actionUrl` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **actionType** | `never` | optional | [REMOVED] `dashboard.widgets[].actionType` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **actionIcon** | `never` | optional | [REMOVED] `dashboard.widgets[].actionIcon` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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) | @@ -187,8 +187,8 @@ Dashboard header action | **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 (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 with it. 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 (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. 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. | +| **responsive** | `never` | optional | [REMOVED] `dashboard.widgets[].responsive` was removed in @objectstack/spec 17.0.0 (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 with it. 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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **aria** | `never` | optional | [REMOVED] `dashboard.widgets[].aria` was removed in @objectstack/spec 17.0.0 (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. 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; `--write` applies the ones it can prove, and you apply the rest by hand. | ### Allowed Values: `DashboardWidget.type` @@ -217,20 +217,20 @@ Dashboard header action | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **type** | `never` | optional | [REMOVED] `dashboard.widgets[].chartConfig.type` was removed in @objectstack/spec 17.5.0 (ADR-0021 · ADR-0049 D2) — on a dataset-bound widget the dataset already decides nothing about the chart family — the WIDGET's own `type` does, and it always won: the dashboard renderer maps the widget type to the chart family and never reads this key, so the authored value could only agree with the dataset selection or silently disagree with it. Delete the key. Write the family on the widget instead: `type: 'line'` beside `dataset`, not inside `chartConfig`. The key is NOT gone from the chart config itself: it stays authorable on the react `` tier, where the chart is bound to inline `data` and there is no dataset to derive it from. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **type** | `never` | optional | [REMOVED] `dashboard.widgets[].chartConfig.type` was removed in @objectstack/spec 17.5.0 (ADR-0021 · ADR-0049 D2) — on a dataset-bound widget the dataset already decides nothing about the chart family — the WIDGET's own `type` does, and it always won: the dashboard renderer maps the widget type to the chart family and never reads this key, so the authored value could only agree with the dataset selection or silently disagree with it. Delete the key. Write the family on the widget instead: `type: 'line'` beside `dataset`, not inside `chartConfig`. The key is NOT gone from the chart config itself: it stays authorable on the react `` tier, where the chart is bound to inline `data` and there is no dataset to derive it from. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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** | `never` | optional | [REMOVED] `dashboard.widgets[].chartConfig.xAxis` was removed in @objectstack/spec 17.5.0 (ADR-0021 · ADR-0049 D2) — on a dataset-bound widget the dataset already decides which column the category axis plots — it is the dataset DIMENSION the widget selects, so the authored value could only agree with the dataset selection or silently disagree with it. Delete the key. Select the dimension instead: `dimensions: ['stage']` on the widget. Axis APPEARANCE (title, number format, min/max, grid lines, log scale) has no home on a dataset-bound widget — the dataset's dimension declaration is what labels and formats the axis. The key is NOT gone from the chart config itself: it stays authorable on the react `` tier, where the chart is bound to inline `data` and there is no dataset to derive it from. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **yAxis** | `never` | optional | [REMOVED] `dashboard.widgets[].chartConfig.yAxis` was removed in @objectstack/spec 17.5.0 (ADR-0021 · ADR-0049 D2) — on a dataset-bound widget the dataset already decides which columns the value axis plots — they are the dataset MEASURES the widget selects, so the authored value could only agree with the dataset selection or silently disagree with it. Delete the key. Select the measures instead: `values: ['amount']` on the widget, one entry per mark. A second axis is a second measure, not a second axis declaration. The key is NOT gone from the chart config itself: it stays authorable on the react `` tier, where the chart is bound to inline `data` and there is no dataset to derive it from. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **series** | `never` | optional | [REMOVED] `dashboard.widgets[].chartConfig.series` was removed in @objectstack/spec 17.5.0 (ADR-0021 · ADR-0049 D2) — on a dataset-bound widget the dataset already decides which series exist and which column each one reads — series membership follows from the measures in `values` and the split in `dimensions`, so the authored value could only agree with the dataset selection or silently disagree with it. Delete the key. Select the measures and the split instead: `values` declares one series per measure and a second `dimensions` entry splits them. Per-series appearance (label, colour, mark type, stacking) is not authorable on a dataset-bound widget; `colors` on this same chart config is the palette channel that remains. The key is NOT gone from the chart config itself: it stays authorable on the react `` tier, where the chart is bound to inline `data` and there is no dataset to derive it from. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **xAxis** | `never` | optional | [REMOVED] `dashboard.widgets[].chartConfig.xAxis` was removed in @objectstack/spec 17.5.0 (ADR-0021 · ADR-0049 D2) — on a dataset-bound widget the dataset already decides which column the category axis plots — it is the dataset DIMENSION the widget selects, so the authored value could only agree with the dataset selection or silently disagree with it. Delete the key. Select the dimension instead: `dimensions: ['stage']` on the widget. Axis APPEARANCE (title, number format, min/max, grid lines, log scale) has no home on a dataset-bound widget — the dataset's dimension declaration is what labels and formats the axis. The key is NOT gone from the chart config itself: it stays authorable on the react `` tier, where the chart is bound to inline `data` and there is no dataset to derive it from. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **yAxis** | `never` | optional | [REMOVED] `dashboard.widgets[].chartConfig.yAxis` was removed in @objectstack/spec 17.5.0 (ADR-0021 · ADR-0049 D2) — on a dataset-bound widget the dataset already decides which columns the value axis plots — they are the dataset MEASURES the widget selects, so the authored value could only agree with the dataset selection or silently disagree with it. Delete the key. Select the measures instead: `values: ['amount']` on the widget, one entry per mark. A second axis is a second measure, not a second axis declaration. The key is NOT gone from the chart config itself: it stays authorable on the react `` tier, where the chart is bound to inline `data` and there is no dataset to derive it from. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **series** | `never` | optional | [REMOVED] `dashboard.widgets[].chartConfig.series` was removed in @objectstack/spec 17.5.0 (ADR-0021 · ADR-0049 D2) — on a dataset-bound widget the dataset already decides which series exist and which column each one reads — series membership follows from the measures in `values` and the split in `dimensions`, so the authored value could only agree with the dataset selection or silently disagree with it. Delete the key. Select the measures and the split instead: `values` declares one series per measure and a second `dimensions` entry splits them. Per-series appearance (label, colour, mark type, stacking) is not authorable on a dataset-bound widget; `colors` on this same chart config is the palette channel that remains. The key is NOT gone from the chart config itself: it stays authorable on the react `` tier, where the chart is bound to inline `data` and there is no dataset to derive it from. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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** | `never` | optional | [REMOVED] `ChartConfig.aria` — authored as `dashboard.widgets[].chartConfig.aria`, `report.chart.aria` and `report.blocks[].chart.aria` — was removed in @objectstack/spec 17 (ADR-0049 D2). No chart renderer ever applied it: the chart implementation declares no `aria` prop, the presentation lowering names it nowhere, and the react `` block never published it, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied on this same chart config is its sibling `description`, which the chart renderer lowers onto the chart graphic as `role="img"` plus `aria-label`. The shared `AriaProps` shape is NOT gone — `ariaLabel` / `ariaDescribedBy` / `role` stay live in the `aria` block on `page.aria`, `page.components[].aria` and the list view `aria`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **aria** | `never` | optional | [REMOVED] `ChartConfig.aria` — authored as `dashboard.widgets[].chartConfig.aria`, `report.chart.aria` and `report.blocks[].chart.aria` — was removed in @objectstack/spec 17 (ADR-0049 D2). No chart renderer ever applied it: the chart implementation declares no `aria` prop, the presentation lowering names it nowhere, and the react `` block never published it, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied on this same chart config is its sibling `description`, which the chart renderer lowers onto the chart graphic as `role="img"` plus `aria-label`. The shared `AriaProps` shape is NOT gone — `ariaLabel` / `ariaDescribedBy` / `role` stay live in the `aria` block on `page.aria`, `page.components[].aria` and the list view `aria`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | ### Nested Shape: `DashboardWidget.compareTo` @@ -260,20 +260,20 @@ Chart APPEARANCE for a dataset-bound widget (ADR-0021): title, subtitle, descrip | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **type** | `never` | optional | [REMOVED] `dashboard.widgets[].chartConfig.type` was removed in @objectstack/spec 17.5.0 (ADR-0021 · ADR-0049 D2) — on a dataset-bound widget the dataset already decides nothing about the chart family — the WIDGET's own `type` does, and it always won: the dashboard renderer maps the widget type to the chart family and never reads this key, so the authored value could only agree with the dataset selection or silently disagree with it. Delete the key. Write the family on the widget instead: `type: 'line'` beside `dataset`, not inside `chartConfig`. The key is NOT gone from the chart config itself: it stays authorable on the react `` tier, where the chart is bound to inline `data` and there is no dataset to derive it from. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **type** | `never` | optional | [REMOVED] `dashboard.widgets[].chartConfig.type` was removed in @objectstack/spec 17.5.0 (ADR-0021 · ADR-0049 D2) — on a dataset-bound widget the dataset already decides nothing about the chart family — the WIDGET's own `type` does, and it always won: the dashboard renderer maps the widget type to the chart family and never reads this key, so the authored value could only agree with the dataset selection or silently disagree with it. Delete the key. Write the family on the widget instead: `type: 'line'` beside `dataset`, not inside `chartConfig`. The key is NOT gone from the chart config itself: it stays authorable on the react `` tier, where the chart is bound to inline `data` and there is no dataset to derive it from. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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** | `never` | optional | [REMOVED] `dashboard.widgets[].chartConfig.xAxis` was removed in @objectstack/spec 17.5.0 (ADR-0021 · ADR-0049 D2) — on a dataset-bound widget the dataset already decides which column the category axis plots — it is the dataset DIMENSION the widget selects, so the authored value could only agree with the dataset selection or silently disagree with it. Delete the key. Select the dimension instead: `dimensions: ['stage']` on the widget. Axis APPEARANCE (title, number format, min/max, grid lines, log scale) has no home on a dataset-bound widget — the dataset's dimension declaration is what labels and formats the axis. The key is NOT gone from the chart config itself: it stays authorable on the react `` tier, where the chart is bound to inline `data` and there is no dataset to derive it from. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **yAxis** | `never` | optional | [REMOVED] `dashboard.widgets[].chartConfig.yAxis` was removed in @objectstack/spec 17.5.0 (ADR-0021 · ADR-0049 D2) — on a dataset-bound widget the dataset already decides which columns the value axis plots — they are the dataset MEASURES the widget selects, so the authored value could only agree with the dataset selection or silently disagree with it. Delete the key. Select the measures instead: `values: ['amount']` on the widget, one entry per mark. A second axis is a second measure, not a second axis declaration. The key is NOT gone from the chart config itself: it stays authorable on the react `` tier, where the chart is bound to inline `data` and there is no dataset to derive it from. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **series** | `never` | optional | [REMOVED] `dashboard.widgets[].chartConfig.series` was removed in @objectstack/spec 17.5.0 (ADR-0021 · ADR-0049 D2) — on a dataset-bound widget the dataset already decides which series exist and which column each one reads — series membership follows from the measures in `values` and the split in `dimensions`, so the authored value could only agree with the dataset selection or silently disagree with it. Delete the key. Select the measures and the split instead: `values` declares one series per measure and a second `dimensions` entry splits them. Per-series appearance (label, colour, mark type, stacking) is not authorable on a dataset-bound widget; `colors` on this same chart config is the palette channel that remains. The key is NOT gone from the chart config itself: it stays authorable on the react `` tier, where the chart is bound to inline `data` and there is no dataset to derive it from. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **xAxis** | `never` | optional | [REMOVED] `dashboard.widgets[].chartConfig.xAxis` was removed in @objectstack/spec 17.5.0 (ADR-0021 · ADR-0049 D2) — on a dataset-bound widget the dataset already decides which column the category axis plots — it is the dataset DIMENSION the widget selects, so the authored value could only agree with the dataset selection or silently disagree with it. Delete the key. Select the dimension instead: `dimensions: ['stage']` on the widget. Axis APPEARANCE (title, number format, min/max, grid lines, log scale) has no home on a dataset-bound widget — the dataset's dimension declaration is what labels and formats the axis. The key is NOT gone from the chart config itself: it stays authorable on the react `` tier, where the chart is bound to inline `data` and there is no dataset to derive it from. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **yAxis** | `never` | optional | [REMOVED] `dashboard.widgets[].chartConfig.yAxis` was removed in @objectstack/spec 17.5.0 (ADR-0021 · ADR-0049 D2) — on a dataset-bound widget the dataset already decides which columns the value axis plots — they are the dataset MEASURES the widget selects, so the authored value could only agree with the dataset selection or silently disagree with it. Delete the key. Select the measures instead: `values: ['amount']` on the widget, one entry per mark. A second axis is a second measure, not a second axis declaration. The key is NOT gone from the chart config itself: it stays authorable on the react `` tier, where the chart is bound to inline `data` and there is no dataset to derive it from. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **series** | `never` | optional | [REMOVED] `dashboard.widgets[].chartConfig.series` was removed in @objectstack/spec 17.5.0 (ADR-0021 · ADR-0049 D2) — on a dataset-bound widget the dataset already decides which series exist and which column each one reads — series membership follows from the measures in `values` and the split in `dimensions`, so the authored value could only agree with the dataset selection or silently disagree with it. Delete the key. Select the measures and the split instead: `values` declares one series per measure and a second `dimensions` entry splits them. Per-series appearance (label, colour, mark type, stacking) is not authorable on a dataset-bound widget; `colors` on this same chart config is the palette channel that remains. The key is NOT gone from the chart config itself: it stays authorable on the react `` tier, where the chart is bound to inline `data` and there is no dataset to derive it from. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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** | `never` | optional | [REMOVED] `ChartConfig.aria` — authored as `dashboard.widgets[].chartConfig.aria`, `report.chart.aria` and `report.blocks[].chart.aria` — was removed in @objectstack/spec 17 (ADR-0049 D2). No chart renderer ever applied it: the chart implementation declares no `aria` prop, the presentation lowering names it nowhere, and the react `` block never published it, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied on this same chart config is its sibling `description`, which the chart renderer lowers onto the chart graphic as `role="img"` plus `aria-label`. The shared `AriaProps` shape is NOT gone — `ariaLabel` / `ariaDescribedBy` / `role` stay live in the `aria` block on `page.aria`, `page.components[].aria` and the list view `aria`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **aria** | `never` | optional | [REMOVED] `ChartConfig.aria` — authored as `dashboard.widgets[].chartConfig.aria`, `report.chart.aria` and `report.blocks[].chart.aria` — was removed in @objectstack/spec 17 (ADR-0049 D2). No chart renderer ever applied it: the chart implementation declares no `aria` prop, the presentation lowering names it nowhere, and the react `` block never published it, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied on this same chart config is its sibling `description`, which the chart renderer lowers onto the chart graphic as `role="img"` plus `aria-label`. The shared `AriaProps` shape is NOT gone — `ariaLabel` / `ariaDescribedBy` / `role` stay live in the `aria` block on `page.aria`, `page.components[].aria` and the list view `aria`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | ### Nested Shape: `DashboardWidgetChartConfig.annotations[number]` diff --git a/content/docs/references/ui/page.mdx b/content/docs/references/ui/page.mdx index 58a1291e20f..3c1a87db191 100644 --- a/content/docs/references/ui/page.mdx +++ b/content/docs/references/ui/page.mdx @@ -178,7 +178,7 @@ View filter rule | **template** | `string` | optional (default: `"default"`) | Layout template name (e.g. "header-sidebar-main") | | **regions** | `{ name: string; width?: Enum<'small' \| 'medium' \| 'large' \| 'full'>; components: object[] }[]` | optional | Layout regions (header, main, sidebar, footer) with their components. Optional — list pages use interfaceConfig, slotted pages use slots, and an empty full page falls back to the synthesized default layout. | | **isDefault** | `boolean` | optional (default: `false`) | | -| **assignedProfiles** | `never` | optional | [REMOVED] `page.assignedProfiles` was removed in @objectstack/spec 17.5.0 (ADR-0090 D2, ADR-0049 enforce-or-remove) — it was named for the Profile concept ADR-0090 D2 deleted, and it gated nothing: no renderer, route or metadata read door ever read the key, so a page that "assigned profiles" stayed open to every caller who could reach it. Delete the key. Page audience is the permission set's: gate the DATA the page shows with the object's permission sets, and bind those sets to people through positions (`sys_position_permission_set`) — those are the checks the runtime actually runs. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **assignedProfiles** | `never` | optional | [REMOVED] `page.assignedProfiles` was removed in @objectstack/spec 17.5.0 (ADR-0090 D2, ADR-0049 enforce-or-remove) — it was named for the Profile concept ADR-0090 D2 deleted, and it gated nothing: no renderer, route or metadata read door ever read the key, so a page that "assigned profiles" stayed open to every caller who could reach it. Delete the key. Page audience is the permission set's: gate the DATA the page shows with the object's permission sets, and bind those sets to people through positions (`sys_position_permission_set`) — those are the checks the runtime actually runs. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **interfaceConfig** | `{ source?: string; columns?: string[] \| object[]; sort?: object[]; filterBy?: object[]; … }` | optional | Interface-level page configuration (for Airtable-style interface pages) | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | | **kind** | `Enum<'full' \| 'slotted' \| 'html' \| 'react' \| 'jsx'>` | optional (default: `"full"`) | Page override mode. full \| slotted = structured authoring; html = author-written constrained JSX compiled (parsed, never executed) to the tree (ADR-0080; the legacy value 'jsx' is a deprecated alias), styled by the registered components' structured props plus a JSON `style` object with hsl(var(--token)) theme colors; react = real-React source executed at render by the runtime (ADR-0081), styled by inline `style` with the same token colors; it runs author JS, so it is gated by a host capability that defaults ON and is disabled server-side via the OS_PAGE_REACT=off env toggle. Do not author Tailwind classes in page source in either tier: `source` is runtime metadata the build-time Tailwind never scans, so utility classNames silently produce no CSS (ADR-0065; ADR-0080). | @@ -257,7 +257,7 @@ View filter rule | **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 `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?: object[]; sort?: object[]; … }` | optional | Per-element data binding for multi-object pages | -| **responsive** | `never` | optional | [REMOVED] `page.components[].responsive` was removed in @objectstack/spec 17 (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. | +| **responsive** | `never` | optional | [REMOVED] `page.components[].responsive` was removed in @objectstack/spec 17 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | ### Nested Shape: `PageComponent.responsiveStyles` @@ -354,7 +354,7 @@ View filter rule | **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 `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?: object[]; sort?: object[]; … }` | optional | Per-element data binding for multi-object pages | -| **responsive** | `never` | optional | [REMOVED] `page.components[].responsive` was removed in @objectstack/spec 17 (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. | +| **responsive** | `never` | optional | [REMOVED] `page.components[].responsive` was removed in @objectstack/spec 17 (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; `--write` applies the ones it can prove, and you apply the rest 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 18edfc2b84b..dcbbd6b2440 100644 --- a/content/docs/references/ui/report.mdx +++ b/content/docs/references/ui/report.mdx @@ -101,7 +101,7 @@ const result = JoinedReportBlockSchema.parse(data); | **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** | `never` | optional | [REMOVED] `ChartConfig.aria` — authored as `dashboard.widgets[].chartConfig.aria`, `report.chart.aria` and `report.blocks[].chart.aria` — was removed in @objectstack/spec 17 (ADR-0049 D2). No chart renderer ever applied it: the chart implementation declares no `aria` prop, the presentation lowering names it nowhere, and the react `` block never published it, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied on this same chart config is its sibling `description`, which the chart renderer lowers onto the chart graphic as `role="img"` plus `aria-label`. The shared `AriaProps` shape is NOT gone — `ariaLabel` / `ariaDescribedBy` / `role` stay live in the `aria` block on `page.aria`, `page.components[].aria` and the list view `aria`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **aria** | `never` | optional | [REMOVED] `ChartConfig.aria` — authored as `dashboard.widgets[].chartConfig.aria`, `report.chart.aria` and `report.blocks[].chart.aria` — was removed in @objectstack/spec 17 (ADR-0049 D2). No chart renderer ever applied it: the chart implementation declares no `aria` prop, the presentation lowering names it nowhere, and the react `` block never published it, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied on this same chart config is its sibling `description`, which the chart renderer lowers onto the chart graphic as `role="img"` plus `aria-label`. The shared `AriaProps` shape is NOT gone — `ariaLabel` / `ariaDescribedBy` / `role` stay live in the `aria` block on `page.aria`, `page.components[].aria` and the list view `aria`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | ### Nested Shape: `Report.blocks[number]` @@ -148,7 +148,7 @@ const result = JoinedReportBlockSchema.parse(data); | **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** | `never` | optional | [REMOVED] `ChartConfig.aria` — authored as `dashboard.widgets[].chartConfig.aria`, `report.chart.aria` and `report.blocks[].chart.aria` — was removed in @objectstack/spec 17 (ADR-0049 D2). No chart renderer ever applied it: the chart implementation declares no `aria` prop, the presentation lowering names it nowhere, and the react `` block never published it, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied on this same chart config is its sibling `description`, which the chart renderer lowers onto the chart graphic as `role="img"` plus `aria-label`. The shared `AriaProps` shape is NOT gone — `ariaLabel` / `ariaDescribedBy` / `role` stay live in the `aria` block on `page.aria`, `page.components[].aria` and the list view `aria`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **aria** | `never` | optional | [REMOVED] `ChartConfig.aria` — authored as `dashboard.widgets[].chartConfig.aria`, `report.chart.aria` and `report.blocks[].chart.aria` — was removed in @objectstack/spec 17 (ADR-0049 D2). No chart renderer ever applied it: the chart implementation declares no `aria` prop, the presentation lowering names it nowhere, and the react `` block never published it, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied on this same chart config is its sibling `description`, which the chart renderer lowers onto the chart graphic as `role="img"` plus `aria-label`. The shared `AriaProps` shape is NOT gone — `ariaLabel` / `ariaDescribedBy` / `role` stay live in the `aria` block on `page.aria`, `page.components[].aria` and the list view `aria`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | ### Allowed Values: `ReportChart.type` diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index f3b26251a7b..59dab1a53ae 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -197,7 +197,7 @@ Column footer summary configuration | **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| 'markdown' \| 'html' \| 'richtext' \| 'number' \| 'currency' \| 'percent' \| 'date' \| … +35 more>` | optional | Field type (auto-infers widget if omitted) | | **options** | `{ label: string; value: string; description?: string; color?: string; … }[]` | optional | Options for select/multiselect/radio/checkboxes fields (per-option `default` is not accepted here — declare the pre-selected choice on the object definition). On a metadata form (schema-bound, built by `defineForm`), an enum-typed row may list its members here, to give them human labels or to offer a deliberate subset. An option `value` is a lowercase system identifier, so a row whose members cannot be spelled as option values (a hyphen, a capital) omits `options`: the control derives the members from the served JSON Schema, and their meanings go in `helpText`. | | **reference** | `string` | optional | Target object name for lookup/master_detail fields | -| **publicPicker** | `never` | optional | [REMOVED] `view.form.sections[].fields[].publicPicker` was removed in @objectstack/spec 17.6.0 (ADR-0087 D2) — an anonymous public form no longer offers record search: lookup, `master_detail` and `user` fields are always left off the anonymous rendering, and the anonymous record-search route (`GET /forms/:slug/lookup/:field`) no longer exists. Delete the key (the whole `publicPicker` block). To let a visitor choose from a fixed list, use a `select` field with static `options`; to let them pick an existing record, put the form behind sign-in. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **publicPicker** | `never` | optional | [REMOVED] `view.form.sections[].fields[].publicPicker` was removed in @objectstack/spec 17.6.0 (ADR-0087 D2) — an anonymous public form no longer offers record search: lookup, `master_detail` and `user` fields are always left off the anonymous rendering, and the anonymous record-search route (`GET /forms/:slug/lookup/:field`) no longer exists. Delete the key (the whole `publicPicker` block). To let a visitor choose from a fixed list, use a `select` field with static `options`; to let them pick an existing record, put the form behind sign-in. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **maxLength** | `integer` | optional | Maximum character length (positive integer; for text/textarea/email/url/phone) | | **minLength** | `integer` | optional | Minimum character length (positive integer; `minLength: 0` is refused — express "no minimum" by omitting the key) | | **min** | `number` | optional | Minimum value (for number/currency/percent/slider) | @@ -327,7 +327,7 @@ Form-view select option — the object-field option shape minus the per-option ` | **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>` | optional | Field type (auto-infers widget if omitted) | | **options** | `{ label: string; value: string; description?: string; color?: string; … }[]` | optional | Options for select/multiselect/radio/checkboxes fields (per-option `default` is not accepted here — declare the pre-selected choice on the object definition). On a metadata form (schema-bound, built by `defineForm`), an enum-typed row may list its members here, to give them human labels or to offer a deliberate subset. An option `value` is a lowercase system identifier, so a row whose members cannot be spelled as option values (a hyphen, a capital) omits `options`: the control derives the members from the served JSON Schema, and their meanings go in `helpText`. | | **reference** | `string` | optional | Target object name for lookup/master_detail fields | -| **publicPicker** | `never` | optional | [REMOVED] `view.form.sections[].fields[].publicPicker` was removed in @objectstack/spec 17.6.0 (ADR-0087 D2) — an anonymous public form no longer offers record search: lookup, `master_detail` and `user` fields are always left off the anonymous rendering, and the anonymous record-search route (`GET /forms/:slug/lookup/:field`) no longer exists. Delete the key (the whole `publicPicker` block). To let a visitor choose from a fixed list, use a `select` field with static `options`; to let them pick an existing record, put the form behind sign-in. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **publicPicker** | `never` | optional | [REMOVED] `view.form.sections[].fields[].publicPicker` was removed in @objectstack/spec 17.6.0 (ADR-0087 D2) — an anonymous public form no longer offers record search: lookup, `master_detail` and `user` fields are always left off the anonymous rendering, and the anonymous record-search route (`GET /forms/:slug/lookup/:field`) no longer exists. Delete the key (the whole `publicPicker` block). To let a visitor choose from a fixed list, use a `select` field with static `options`; to let them pick an existing record, put the form behind sign-in. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **maxLength** | `integer` | optional | Maximum character length (positive integer; for text/textarea/email/url/phone) | | **minLength** | `integer` | optional | Minimum character length (positive integer; `minLength: 0` is refused — express "no minimum" by omitting the key) | | **min** | `number` | optional | Minimum value (for number/currency/percent/slider) | @@ -398,12 +398,12 @@ Form-view select option — the object-field option shape minus the per-option ` | **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?: object[]; amountField?: string; … }[]` | optional | Inline master-detail child collections | -| **defaultSort** | `never` | optional | [REMOVED] `form.defaultSort` was removed in @objectstack/spec 17.0.0 (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. | +| **defaultSort** | `never` | optional | [REMOVED] `form.defaultSort` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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). | | **buttons** | `{ submit?: object; cancel?: object; reset?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm. | | **defaults** | `Record` | optional | Initial field values for create-mode forms (folded into ObjectUI ObjectForm initial values;). | -| **aria** | `never` | optional | [REMOVED] `form.aria` was removed in @objectstack/spec 17.0.0 (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. | +| **aria** | `never` | optional | [REMOVED] `form.aria` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | ### Nested Shape: `FormView.data[provider='object']` @@ -788,7 +788,7 @@ Map 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 }` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout | -| **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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 | @@ -804,17 +804,17 @@ Map view configuration | **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** | `never` | optional | [REMOVED] `view.list.tabs` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's `listViews` instead: the tab's `name` becomes the entry's key, its `label` the entry's `label`, and its `filter` rules join the view's own `filter` on that entry (copy the view's `columns` too); a tab whose `view` already named a list view needs nothing more. Every `listViews` entry renders as a tab in the switcher. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **tabs** | `never` | optional | [REMOVED] `view.list.tabs` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's `listViews` instead: the tab's `name` becomes the entry's key, its `label` the entry's `label`, and its `filter` rules join the view's own `filter` on that entry (copy the view's `columns` too); a tab whose `view` already named a list view needs nothing more. Every `listViews` entry renders as a tab in the switcher. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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 (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 (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 (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 (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 (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. | +| **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **striped** | `never` | optional | [REMOVED] `view.striped` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **bordered** | `never` | optional | [REMOVED] `view.bordered` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **virtualScroll** | `never` | optional | [REMOVED] `view.virtualScroll` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | ### Nested Shape: `ListView.data[provider='object']` @@ -1180,7 +1180,7 @@ View filter rule | **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 }` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout | -| **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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 | @@ -1196,17 +1196,17 @@ View filter rule | **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** | `never` | optional | [REMOVED] `view.list.tabs` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's `listViews` instead: the tab's `name` becomes the entry's key, its `label` the entry's `label`, and its `filter` rules join the view's own `filter` on that entry (copy the view's `columns` too); a tab whose `view` already named a list view needs nothing more. Every `listViews` entry renders as a tab in the switcher. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **tabs** | `never` | optional | [REMOVED] `view.list.tabs` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's `listViews` instead: the tab's `name` becomes the entry's key, its `label` the entry's `label`, and its `filter` rules join the view's own `filter` on that entry (copy the view's `columns` too); a tab whose `view` already named a list view needs nothing more. Every `listViews` entry renders as a tab in the switcher. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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 (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 (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 (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 (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 (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. | +| **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **striped** | `never` | optional | [REMOVED] `view.striped` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **bordered** | `never` | optional | [REMOVED] `view.bordered` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **virtualScroll** | `never` | optional | [REMOVED] `view.virtualScroll` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | | **userFilters** | `{ element?: Enum<'dropdown' \| 'toggle'>; fields?: object[] }` | optional | | ### Nested Shape: `ObjectListView.data[provider='object']` @@ -1763,7 +1763,7 @@ Tab configuration for multi-tab view interface | **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 }` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout | -| **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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 | @@ -1779,17 +1779,17 @@ Tab configuration for multi-tab view interface | **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** | `never` | optional | [REMOVED] `view.list.tabs` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's `listViews` instead: the tab's `name` becomes the entry's key, its `label` the entry's `label`, and its `filter` rules join the view's own `filter` on that entry (copy the view's `columns` too); a tab whose `view` already named a list view needs nothing more. Every `listViews` entry renders as a tab in the switcher. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **tabs** | `never` | optional | [REMOVED] `view.list.tabs` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's `listViews` instead: the tab's `name` becomes the entry's key, its `label` the entry's `label`, and its `filter` rules join the view's own `filter` on that entry (copy the view's `columns` too); a tab whose `view` already named a list view needs nothing more. Every `listViews` entry renders as a tab in the switcher. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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 (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 (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 (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 (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 (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. | +| **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **striped** | `never` | optional | [REMOVED] `view.striped` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **bordered** | `never` | optional | [REMOVED] `view.bordered` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **virtualScroll** | `never` | optional | [REMOVED] `view.virtualScroll` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | | **userFilters** | `{ element?: Enum<'dropdown' \| 'toggle'>; fields?: object[] }` | optional | | ### Nested Shape: `View.form` @@ -1815,12 +1815,12 @@ Tab configuration for multi-tab view interface | **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?: object[]; amountField?: string; … }[]` | optional | Inline master-detail child collections | -| **defaultSort** | `never` | optional | [REMOVED] `form.defaultSort` was removed in @objectstack/spec 17.0.0 (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. | +| **defaultSort** | `never` | optional | [REMOVED] `form.defaultSort` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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). | | **buttons** | `{ submit?: object; cancel?: object; reset?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm. | | **defaults** | `Record` | optional | Initial field values for create-mode forms (folded into ObjectUI ObjectForm initial values;). | -| **aria** | `never` | optional | [REMOVED] `form.aria` was removed in @objectstack/spec 17.0.0 (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. | +| **aria** | `never` | optional | [REMOVED] `form.aria` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | ### Nested Shape: `View.listViews[string]` @@ -1848,7 +1848,7 @@ Tab configuration for multi-tab view interface | **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 }` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout | -| **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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 | @@ -1864,17 +1864,17 @@ Tab configuration for multi-tab view interface | **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** | `never` | optional | [REMOVED] `view.list.tabs` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's `listViews` instead: the tab's `name` becomes the entry's key, its `label` the entry's `label`, and its `filter` rules join the view's own `filter` on that entry (copy the view's `columns` too); a tab whose `view` already named a list view needs nothing more. Every `listViews` entry renders as a tab in the switcher. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **tabs** | `never` | optional | [REMOVED] `view.list.tabs` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's `listViews` instead: the tab's `name` becomes the entry's key, its `label` the entry's `label`, and its `filter` rules join the view's own `filter` on that entry (copy the view's `columns` too); a tab whose `view` already named a list view needs nothing more. Every `listViews` entry renders as a tab in the switcher. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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 (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 (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 (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 (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 (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. | +| **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **striped** | `never` | optional | [REMOVED] `view.striped` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **bordered** | `never` | optional | [REMOVED] `view.bordered` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **virtualScroll** | `never` | optional | [REMOVED] `view.virtualScroll` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | | **userFilters** | `{ element?: Enum<'dropdown' \| 'toggle'>; fields?: object[] }` | optional | | ### Nested Shape: `View.formViews[string]` @@ -1900,12 +1900,12 @@ Tab configuration for multi-tab view interface | **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?: object[]; amountField?: string; … }[]` | optional | Inline master-detail child collections | -| **defaultSort** | `never` | optional | [REMOVED] `form.defaultSort` was removed in @objectstack/spec 17.0.0 (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. | +| **defaultSort** | `never` | optional | [REMOVED] `form.defaultSort` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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). | | **buttons** | `{ submit?: object; cancel?: object; reset?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm. | | **defaults** | `Record` | optional | Initial field values for create-mode forms (folded into ObjectUI ObjectForm initial values;). | -| **aria** | `never` | optional | [REMOVED] `form.aria` was removed in @objectstack/spec 17.0.0 (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. | +| **aria** | `never` | optional | [REMOVED] `form.aria` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | ### Nested Shape: `View.protection` @@ -2051,8 +2051,8 @@ This schema accepts one of the following structures: | **isDefault** | `boolean` | optional | Whether this is the object's default view in the switcher. | | **order** | `integer` | optional | Sort order within the object's view switcher / left rail. | | **scope** | `Enum<'package' \| 'shared' \| 'personal'>` | optional | Identity layer (defaults to `package` for source-loaded views). | -| **owner** | `never` | optional | [REMOVED] `view.owner` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the user a `personal` view item belonged to, and nothing ever read it: the view switcher (`GET /meta/view?object=`) serves every item bound to the object without looking at `owner`, so a view marked as one user's was listed for every user who can read the object. Delete the key. Nothing restricts a view item to one user today — per-user view scoping is a parked direction (ADR-0017), not a shipped mechanism — so a view item is visible to everyone who can read its object. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **hidden** | `never` | optional | [REMOVED] `view.hidden` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it promised to hide a view item from the switcher, and nothing ever read it: `GET /meta/view?object=` and the console's view switcher list every item bound to the object, `hidden: true` included. Delete the key; to take a view out of the switcher, delete the view item itself (or stop shipping it from source). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **owner** | `never` | optional | [REMOVED] `view.owner` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the user a `personal` view item belonged to, and nothing ever read it: the view switcher (`GET /meta/view?object=`) serves every item bound to the object without looking at `owner`, so a view marked as one user's was listed for every user who can read the object. Delete the key. Nothing restricts a view item to one user today — per-user view scoping is a parked direction (ADR-0017), not a shipped mechanism — so a view item is visible to everyone who can read its object. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **hidden** | `never` | optional | [REMOVED] `view.hidden` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it promised to hide a view item from the switcher, and nothing ever read it: `GET /meta/view?object=` and the console's view switcher list every item bound to the object, `hidden: true` included. Delete the key; to take a view out of the switcher, delete the view item itself (or stop shipping it from source). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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. | @@ -2089,7 +2089,7 @@ This schema accepts one of the following structures: | **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 }` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout | -| **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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 | @@ -2105,17 +2105,17 @@ This schema accepts one of the following structures: | **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** | `never` | optional | [REMOVED] `view.list.tabs` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's `listViews` instead: the tab's `name` becomes the entry's key, its `label` the entry's `label`, and its `filter` rules join the view's own `filter` on that entry (copy the view's `columns` too); a tab whose `view` already named a list view needs nothing more. Every `listViews` entry renders as a tab in the switcher. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **tabs** | `never` | optional | [REMOVED] `view.list.tabs` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's `listViews` instead: the tab's `name` becomes the entry's key, its `label` the entry's `label`, and its `filter` rules join the view's own `filter` on that entry (copy the view's `columns` too); a tab whose `view` already named a list view needs nothing more. Every `listViews` entry renders as a tab in the switcher. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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 (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 (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 (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 (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 (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. | +| **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **striped** | `never` | optional | [REMOVED] `view.striped` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **bordered** | `never` | optional | [REMOVED] `view.bordered` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **virtualScroll** | `never` | optional | [REMOVED] `view.virtualScroll` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | ### Nested Shape: `ViewItem[viewKind='list'].protection` @@ -2141,8 +2141,8 @@ This schema accepts one of the following structures: | **isDefault** | `boolean` | optional | Whether this is the object's default view in the switcher. | | **order** | `integer` | optional | Sort order within the object's view switcher / left rail. | | **scope** | `Enum<'package' \| 'shared' \| 'personal'>` | optional | Identity layer (defaults to `package` for source-loaded views). | -| **owner** | `never` | optional | [REMOVED] `view.owner` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the user a `personal` view item belonged to, and nothing ever read it: the view switcher (`GET /meta/view?object=`) serves every item bound to the object without looking at `owner`, so a view marked as one user's was listed for every user who can read the object. Delete the key. Nothing restricts a view item to one user today — per-user view scoping is a parked direction (ADR-0017), not a shipped mechanism — so a view item is visible to everyone who can read its object. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **hidden** | `never` | optional | [REMOVED] `view.hidden` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it promised to hide a view item from the switcher, and nothing ever read it: `GET /meta/view?object=` and the console's view switcher list every item bound to the object, `hidden: true` included. Delete the key; to take a view out of the switcher, delete the view item itself (or stop shipping it from source). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **owner** | `never` | optional | [REMOVED] `view.owner` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the user a `personal` view item belonged to, and nothing ever read it: the view switcher (`GET /meta/view?object=`) serves every item bound to the object without looking at `owner`, so a view marked as one user's was listed for every user who can read the object. Delete the key. Nothing restricts a view item to one user today — per-user view scoping is a parked direction (ADR-0017), not a shipped mechanism — so a view item is visible to everyone who can read its object. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **hidden** | `never` | optional | [REMOVED] `view.hidden` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it promised to hide a view item from the switcher, and nothing ever read it: `GET /meta/view?object=` and the console's view switcher list every item bound to the object, `hidden: true` included. Delete the key; to take a view out of the switcher, delete the view item itself (or stop shipping it from source). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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. | @@ -2175,12 +2175,12 @@ This schema accepts one of the following structures: | **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?: object[]; amountField?: string; … }[]` | optional | Inline master-detail child collections | -| **defaultSort** | `never` | optional | [REMOVED] `form.defaultSort` was removed in @objectstack/spec 17.0.0 (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. | +| **defaultSort** | `never` | optional | [REMOVED] `form.defaultSort` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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). | | **buttons** | `{ submit?: object; cancel?: object; reset?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm. | | **defaults** | `Record` | optional | Initial field values for create-mode forms (folded into ObjectUI ObjectForm initial values;). | -| **aria** | `never` | optional | [REMOVED] `form.aria` was removed in @objectstack/spec 17.0.0 (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. | +| **aria** | `never` | optional | [REMOVED] `form.aria` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | ### Nested Shape: `ViewItem[viewKind='form'].protection` @@ -2224,8 +2224,8 @@ This schema accepts one of the following structures: | **isDefault** | `boolean` | optional | Whether this is the object's default view in the switcher. | | **order** | `integer` | optional | Sort order within the object's view switcher / left rail. | | **scope** | `Enum<'package' \| 'shared' \| 'personal'>` | optional | Identity layer (defaults to `package` for source-loaded views). | -| **owner** | `never` | optional | [REMOVED] `view.owner` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the user a `personal` view item belonged to, and nothing ever read it: the view switcher (`GET /meta/view?object=`) serves every item bound to the object without looking at `owner`, so a view marked as one user's was listed for every user who can read the object. Delete the key. Nothing restricts a view item to one user today — per-user view scoping is a parked direction (ADR-0017), not a shipped mechanism — so a view item is visible to everyone who can read its object. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **hidden** | `never` | optional | [REMOVED] `view.hidden` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it promised to hide a view item from the switcher, and nothing ever read it: `GET /meta/view?object=` and the console's view switcher list every item bound to the object, `hidden: true` included. Delete the key; to take a view out of the switcher, delete the view item itself (or stop shipping it from source). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **owner** | `never` | optional | [REMOVED] `view.owner` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the user a `personal` view item belonged to, and nothing ever read it: the view switcher (`GET /meta/view?object=`) serves every item bound to the object without looking at `owner`, so a view marked as one user's was listed for every user who can read the object. Delete the key. Nothing restricts a view item to one user today — per-user view scoping is a parked direction (ADR-0017), not a shipped mechanism — so a view item is visible to everyone who can read its object. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **hidden** | `never` | optional | [REMOVED] `view.hidden` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it promised to hide a view item from the switcher, and nothing ever read it: `GET /meta/view?object=` and the console's view switcher list every item bound to the object, `hidden: true` included. Delete the key; to take a view out of the switcher, delete the view item itself (or stop shipping it from source). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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. | @@ -2267,7 +2267,7 @@ This schema accepts one of the following structures: | **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 }` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout | -| **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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 | @@ -2283,17 +2283,17 @@ This schema accepts one of the following structures: | **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** | `never` | optional | [REMOVED] `view.list.tabs` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's `listViews` instead: the tab's `name` becomes the entry's key, its `label` the entry's `label`, and its `filter` rules join the view's own `filter` on that entry (copy the view's `columns` too); a tab whose `view` already named a list view needs nothing more. Every `listViews` entry renders as a tab in the switcher. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **tabs** | `never` | optional | [REMOVED] `view.list.tabs` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's `listViews` instead: the tab's `name` becomes the entry's key, its `label` the entry's `label`, and its `filter` rules join the view's own `filter` on that entry (copy the view's `columns` too); a tab whose `view` already named a list view needs nothing more. Every `listViews` entry renders as a tab in the switcher. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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 (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 (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 (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 (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 (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. | +| **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **striped** | `never` | optional | [REMOVED] `view.striped` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **bordered** | `never` | optional | [REMOVED] `view.bordered` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **virtualScroll** | `never` | optional | [REMOVED] `view.virtualScroll` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | ### Nested Shape: `ViewItemWire[viewKind='list'].protection` @@ -2326,8 +2326,8 @@ This schema accepts one of the following structures: | **isDefault** | `boolean` | optional | Whether this is the object's default view in the switcher. | | **order** | `integer` | optional | Sort order within the object's view switcher / left rail. | | **scope** | `Enum<'package' \| 'shared' \| 'personal'>` | optional | Identity layer (defaults to `package` for source-loaded views). | -| **owner** | `never` | optional | [REMOVED] `view.owner` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the user a `personal` view item belonged to, and nothing ever read it: the view switcher (`GET /meta/view?object=`) serves every item bound to the object without looking at `owner`, so a view marked as one user's was listed for every user who can read the object. Delete the key. Nothing restricts a view item to one user today — per-user view scoping is a parked direction (ADR-0017), not a shipped mechanism — so a view item is visible to everyone who can read its object. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **hidden** | `never` | optional | [REMOVED] `view.hidden` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it promised to hide a view item from the switcher, and nothing ever read it: `GET /meta/view?object=` and the console's view switcher list every item bound to the object, `hidden: true` included. Delete the key; to take a view out of the switcher, delete the view item itself (or stop shipping it from source). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **owner** | `never` | optional | [REMOVED] `view.owner` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the user a `personal` view item belonged to, and nothing ever read it: the view switcher (`GET /meta/view?object=`) serves every item bound to the object without looking at `owner`, so a view marked as one user's was listed for every user who can read the object. Delete the key. Nothing restricts a view item to one user today — per-user view scoping is a parked direction (ADR-0017), not a shipped mechanism — so a view item is visible to everyone who can read its object. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **hidden** | `never` | optional | [REMOVED] `view.hidden` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it promised to hide a view item from the switcher, and nothing ever read it: `GET /meta/view?object=` and the console's view switcher list every item bound to the object, `hidden: true` included. Delete the key; to take a view out of the switcher, delete the view item itself (or stop shipping it from source). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **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. | @@ -2365,12 +2365,12 @@ This schema accepts one of the following structures: | **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?: object[]; amountField?: string; … }[]` | optional | Inline master-detail child collections | -| **defaultSort** | `never` | optional | [REMOVED] `form.defaultSort` was removed in @objectstack/spec 17.0.0 (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. | +| **defaultSort** | `never` | optional | [REMOVED] `form.defaultSort` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest 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). | | **buttons** | `{ submit?: object; cancel?: object; reset?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm. | | **defaults** | `Record` | optional | Initial field values for create-mode forms (folded into ObjectUI ObjectForm initial values;). | -| **aria** | `never` | optional | [REMOVED] `form.aria` was removed in @objectstack/spec 17.0.0 (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. | +| **aria** | `never` | optional | [REMOVED] `form.aria` was removed in @objectstack/spec 17.0.0 (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; `--write` applies the ones it can prove, and you apply the rest by hand. | ### Nested Shape: `ViewItemWire[viewKind='form'].protection` diff --git a/packages/drivers/driver-turso/src/spec/turso.zod.ts b/packages/drivers/driver-turso/src/spec/turso.zod.ts index a05fe3f0973..e25cca4d550 100644 --- a/packages/drivers/driver-turso/src/spec/turso.zod.ts +++ b/packages/drivers/driver-turso/src/spec/turso.zod.ts @@ -60,7 +60,7 @@ const TIMEOUT_RETIRED = '`turso config.timeout` was renamed to `timeoutMs` in @objectstack/driver-turso 17 — the unit of a ' + 'duration-shaped number lives in the key name, not only in the describe prose. Rename the key to ' + '`timeoutMs`; the value (milliseconds) is unchanged. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** * The prescriptions the two REMOVED keys raise (ADR-0049 enforce-or-remove). @@ -72,14 +72,14 @@ const LOCAL_PATH_RETIRED = + 'effect: no code read it, and the embedded replica\'s local file has always been named by `url` ' + '(`file:./replica.db`, with `syncUrl` pointing at the remote primary). Delete the key; a path it ' + 'named that differs from `url` belongs in `url`. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; const WASM_RETIRED = '`turso config.wasm` was removed in @objectstack/driver-turso 17 (ADR-0049) — it never had an ' + 'effect: nothing selects a WASM build of libSQL, and the driver loads whatever `@libsql/client` ' + 'resolves to on the host runtime. Delete the key; a runtime that cannot load native bindings uses ' + 'the remote arm (`libsql://` / `https://`), which needs none. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; // ========================================================================== // 2a. Transport coherence — what `new TursoDriver` refuses diff --git a/packages/lint/src/data-model-rules.ts b/packages/lint/src/data-model-rules.ts index cf7676d3a9d..7cd2a99f4d5 100644 --- a/packages/lint/src/data-model-rules.ts +++ b/packages/lint/src/data-model-rules.ts @@ -456,8 +456,12 @@ export function lintUnscopedDeclaredIndexes(objects: any[]): LocatedLintIssue[] fix: `State the scope: \`unique: 'global'\` (installation-wide — the exact index bare \`true\` built) or ` + `\`unique: 'organization'\` (one holder per organization — the driver prepends the NULL-safe ` + - `organization key part at registration). Run \`os migrate meta --from 17\` to list the mechanical ` + - `edits for existing sources; apply them by hand.`, + `organization key part at registration). ` + + // The house sentence, plain-quoted (not a template literal) so this site + // is judged by `retired-key-migrate-sentence.test.ts`'s scan of this + // package: escaped template backticks hide it from that scan. + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; ' + + '`--write` applies the ones it can prove, and you apply the rest by hand.', }); } } diff --git a/packages/lint/src/validate-expressions.test.ts b/packages/lint/src/validate-expressions.test.ts index 87d6a5d0c63..fa0a3d80a44 100644 --- a/packages/lint/src/validate-expressions.test.ts +++ b/packages/lint/src/validate-expressions.test.ts @@ -167,11 +167,11 @@ describe('validateStackExpressions (ADR-0032 build-time)', () => { // the retired key's fate. This branch can DELETE the key outright // (`template`/`recipients`/`variables`/`script`), so "rewrite it" read two // ways ("it" = the key vs. "it" = your sources); #9529 then withdrew the - // automatic-rewrite claim outright — the tool lists the edits and never - // writes a source file. Pinned here AND class-wide in + // automatic-rewrite claim outright, and #9591 names `--write`, which writes + // only the edits it can prove — never the unqualified claim. Pinned here AND class-wide in // `retired-key-migrate-sentence.test.ts` (widened to `packages/lint/src`). expect(issues[0].message).toMatch( - /Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand\.$/, + /Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand\.$/, ); expect(issues[0].message).not.toMatch(/rewrite it automatically/); expect(issues[0].message).not.toMatch(/rewrite existing sources automatically/); diff --git a/packages/lint/src/validate-expressions.ts b/packages/lint/src/validate-expressions.ts index aa5d3b35ba4..e3d797e92a3 100644 --- a/packages/lint/src/validate-expressions.ts +++ b/packages/lint/src/validate-expressions.ts @@ -1837,16 +1837,17 @@ export function runStackExpressionPasses(stack: AnyRec, options: StackExpression ? `\`actionType: '${action}'\` named a registered function — move it to \`function: '${action}'\`. ` : `Use a \`notify\` node for mail, a \`connector_action\` (Slack connector) or \`http\` node ` + `for Slack, and a registered function for logic. `) + - // #6856 route D (maintainer-ruled), reworded under #9529: the house - // sentence names the TOOL's behaviour, never the retired key's fate — - // "rewrite it" read two ways over a branch that DELETES the key - // (template/recipients/variables/script), and the tool never rewrote a - // source file at all. Plain-quoted (not a template literal) + // #6856 route D (maintainer-ruled), reworded under #9529 and #9591: the + // house sentence names the TOOL's behaviour, never the retired key's + // fate — "rewrite it" read two ways over a branch that DELETES the key + // (template/recipients/variables/script) — and claims no more than the + // tool does: the default run lists, `--write` writes only the edits it + // can prove. Plain-quoted (not a template literal) // so this site is a member of `retired-key-migrate-sentence.test.ts`'s // widened scan (#7030) on the same textual shape as the spec corpus — no // interpolation lives in this clause, so nothing is lost switching quote style. 'Run `os migrate meta --from 16` to list the mechanical edits for existing ' - + 'sources; apply them by hand.', + + 'sources; `--write` applies the ones it can prove, and you apply the rest by hand.', source: JSON.stringify({ id: node.id, type: node.type, config: cfg }), }); } else if (!fn) { diff --git a/packages/lint/src/validate-widget-bindings.ts b/packages/lint/src/validate-widget-bindings.ts index 29871c26cdf..63571559921 100644 --- a/packages/lint/src/validate-widget-bindings.ts +++ b/packages/lint/src/validate-widget-bindings.ts @@ -1189,7 +1189,7 @@ export function validateWidgetBindings(stack: AnyRec): WidgetBindingFinding[] { `Delete the key: \`chartConfig.xAxis\` is refused on a dataset-bound widget ` + `(ADR-0021) and the x-axis binding comes from this widget's \`dimensions\`. ` + `Run \`os migrate meta --from 17\` to list the mechanical edits for existing ` + - `sources; apply them by hand.` + + `sources; \`--write\` applies the ones it can prove, and you apply the rest by hand.` + `${suggestName(xAxis.field, dimensionNames)} ${suppressHint}`, }); } diff --git a/packages/spec/src/ai/agent-lifecycle-retirement.test.ts b/packages/spec/src/ai/agent-lifecycle-retirement.test.ts index 6306647904c..83ac9faba7c 100644 --- a/packages/spec/src/ai/agent-lifecycle-retirement.test.ts +++ b/packages/spec/src/ai/agent-lifecycle-retirement.test.ts @@ -47,7 +47,7 @@ import { defineStack, ObjectStackDefinitionSchema } from '../stack.zod'; import { AgentSchema, type Agent } from './agent.zod'; const MIGRATE_SENTENCE = - 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; const CONVERSION_ID = 'agent-lifecycle-removed'; diff --git a/packages/spec/src/ai/agent-memory-store-retirement.test.ts b/packages/spec/src/ai/agent-memory-store-retirement.test.ts index aa495bcca4c..1e4f3ffe4c7 100644 --- a/packages/spec/src/ai/agent-memory-store-retirement.test.ts +++ b/packages/spec/src/ai/agent-memory-store-retirement.test.ts @@ -46,7 +46,7 @@ import { defineStack, ObjectStackDefinitionSchema } from '../stack.zod'; import { AgentSchema, type Agent } from './agent.zod'; const MIGRATE_SENTENCE = - 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; const MEMORY_AGENT = { name: 'memory_agent', diff --git a/packages/spec/src/ai/agent.test.ts b/packages/spec/src/ai/agent.test.ts index 5224c1799bc..586c8ab0bc8 100644 --- a/packages/spec/src/ai/agent.test.ts +++ b/packages/spec/src/ai/agent.test.ts @@ -638,7 +638,7 @@ describe('StructuredOutputConfigSchema', () => { // the issue `code`, the `path` naming the position, and the prescription text. const MIGRATE_SENTENCE = - 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; const AGENT_BASE = { name: 'answer_agent', diff --git a/packages/spec/src/ai/agent.zod.ts b/packages/spec/src/ai/agent.zod.ts index 80b6593b395..5be4658dfe1 100644 --- a/packages/spec/src/ai/agent.zod.ts +++ b/packages/spec/src/ai/agent.zod.ts @@ -67,7 +67,7 @@ const JSON_ONLY_FORMAT_FIX = + 'Schema in `schema`, or `json_object` — or delete the `structuredOutput` block if the agent needs ' + 'no output contract; at `agent.structuredOutput.fallbackFormat`, name one of those two or delete ' + 'the key. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; const STRUCTURED_OUTPUT_FORMAT_RETIRED = { regex: @@ -93,7 +93,7 @@ const COERCE_TYPES_RETIRED = + 'whose `agent.structuredOutput.transformPipeline` lists it before its first turn. Delete the ' + 'step and declare the exact types in `schema`, so the answer is validated as the model wrote ' + 'it; `trim`, `parse_json` and `validate` are unchanged. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** * Structured Output Format @@ -200,7 +200,7 @@ const LONG_TERM_STORE_RETIRED = + '`vector` store (the old default) and `redis` before an agent\'s first turn. Delete the key; ' + 'long-term memory is configured by `enabled`, `maxEntries` and ' + '`agent.memory.reflectionInterval`, and where the notes are kept is the platform\'s choice. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; const LONG_TERM_STORE_SPELLING_RETIRED = 'there is no storage-backend key on long-term memory — where the notes are kept is the ' @@ -366,7 +366,7 @@ export const AgentSchema = lazySchema(() => strictObject({ + '`instructions` and `tools`, selected by its `triggerConditions` (ADR-0064); multi-step ' + 'process orchestration is a Flow (ADR-0019); a record\'s status transitions are a ' + '`state_machine` validation rule on the object (ADR-0020). ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** @@ -416,7 +416,7 @@ export const AgentSchema = lazySchema(() => strictObject({ 'RUNTIME half (tool resolution, which lives in cloud `service-ai`), not this ' + 'rejection: the authoring invariant binds you here, and ADR-0109 ' + '(Accepted — implemented) is the in-repo record that carries it. ' + - 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** Knowledge */ @@ -432,7 +432,7 @@ export const AgentSchema = lazySchema(() => strictObject({ 'the agent record. Delete the block. Restrict retrieval at the knowledge-service / ' + 'source level (per-source permissions), and describe intended grounding in ' + '`instructions` so the model asks for the right sources. ' + - 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** Interface */ diff --git a/packages/spec/src/ai/skill.zod.ts b/packages/spec/src/ai/skill.zod.ts index 113b20fedcb..f00557fe64e 100644 --- a/packages/spec/src/ai/skill.zod.ts +++ b/packages/spec/src/ai/skill.zod.ts @@ -389,7 +389,7 @@ export const SkillSchema = lazySchema(() => strictObject({ "`triggerConditions` (AND of context field/operator/value) intersected with the agent's " + '`skills[]`, plus explicit /skill-name pinning. Delete the key. Put routing intent in ' + '`triggerConditions`; describe intent in `description`/`instructions` for the LLM. ' + - 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** diff --git a/packages/spec/src/api/endpoint.zod.ts b/packages/spec/src/api/endpoint.zod.ts index ab1d46b74a8..7cba11deccb 100644 --- a/packages/spec/src/api/endpoint.zod.ts +++ b/packages/spec/src/api/endpoint.zod.ts @@ -200,7 +200,7 @@ export const ApiEndpointSchema = strictObject({ + 'the unit of a duration-shaped number lives in the key name, not only ' + 'in the describe prose. Rename the key to `cacheTtlSeconds`; the value (seconds) is ' + 'unchanged, and it stays GET-only. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), // ADR-0010 — runtime protection envelope (internal — set by the loader). diff --git a/packages/spec/src/api/rest-server.zod.ts b/packages/spec/src/api/rest-server.zod.ts index 1a2fe82c75a..e7c0ba878e1 100644 --- a/packages/spec/src/api/rest-server.zod.ts +++ b/packages/spec/src/api/rest-server.zod.ts @@ -197,7 +197,7 @@ export const RestApiConfigSchema = lazySchema(() => z.object({ + '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.', + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** diff --git a/packages/spec/src/automation/flow.zod.ts b/packages/spec/src/automation/flow.zod.ts index 3498ae5f1d8..8d7bee83d29 100644 --- a/packages/spec/src/automation/flow.zod.ts +++ b/packages/spec/src/automation/flow.zod.ts @@ -704,7 +704,7 @@ function flowNodeObject() { return strictObject( '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.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** @@ -778,7 +778,7 @@ function flowNodeObject() { return strictObject( + "(`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.', + + 'sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), onTimeout: retiredKey( '`waitEventConfig.onTimeout` was removed in @objectstack/spec 17. It had no readers at ' @@ -786,7 +786,7 @@ function flowNodeObject() { return strictObject( + '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.', + + 'sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), }).superRefine((wec, ctx) => { // A timer with nothing to count is the forever-hang in its second @@ -1062,7 +1062,7 @@ export const FlowSchema = lazySchema(() => strictObject( '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.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** Trigger Type */ @@ -1086,7 +1086,7 @@ export const FlowSchema = lazySchema(() => strictObject( '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.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), // ADR-0049 / #1888 — ENFORCED. The service-automation engine establishes the // declared identity for the run's data operations and restores the caller's @@ -1244,7 +1244,7 @@ export const FlowSchema = lazySchema(() => strictObject( "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.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), }).superRefine((eh, ctx) => { // `strategy: 'retry'` with 0 attempts is `strategy: 'fail'` wearing a diff --git a/packages/spec/src/automation/schemaless-node-config.zod.ts b/packages/spec/src/automation/schemaless-node-config.zod.ts index bbae9357f35..fa6b7847f4c 100644 --- a/packages/spec/src/automation/schemaless-node-config.zod.ts +++ b/packages/spec/src/automation/schemaless-node-config.zod.ts @@ -319,7 +319,8 @@ export const ScriptConfigSchema = lazySchema(() => strictObject({ + 'the Slack connector, or an `http` node posting to a webhook; for anything else, move the ' + 'name into `config.function`. ' + 'Run `os migrate meta --from 16` to list the mechanical edits for the shorthand ' - + 'case into `config.function`; the stub and marker values are removed.', + + 'case into `config.function`; `--write` applies the ones it can prove, and the stub and ' + + 'marker values are removed.', ), template: retiredKey( '`script.config.template` was removed in @objectstack/spec 17 — it fed only the ' @@ -327,27 +328,27 @@ export const ScriptConfigSchema = lazySchema(() => strictObject({ + 'id was ever resolved. Delete the key. A `notify` node carries its own `title`/`message`, and ' + 'stored templates live in the messaging service (`sys_notification_template`), not on the ' + 'node. ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), recipients: retiredKey( '`script.config.recipients` was removed in @objectstack/spec 17 — the addresses were ' + 'logged, never messaged: the `email`/`slack` branches it fed delivered nothing. Use a ' + '`notify` node, whose `recipients` (user ids, field refs or addresses) reach the messaging ' + 'service for real. ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), variables: retiredKey( '`script.config.variables` was removed in @objectstack/spec 17 — it injected values ' + 'into a template no side effect ever rendered. Delete the key. A `notify` node carries ' + 'structured data in `payload`; a registered function takes it in `config.inputs`. ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), script: retiredKey( '`script.config.script` was removed in @objectstack/spec 17 — the built-in runtime has ' + 'no server-side JS sandbox, so an inline body was recognized and never executed: the node ' + 'warned and completed as a no-op. Move the logic into a registered function ' + '(`defineStack({ functions })`) and name it in `config.function`. ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), })); diff --git a/packages/spec/src/data/analytics.zod.ts b/packages/spec/src/data/analytics.zod.ts index 5f579398d24..2343320739a 100644 --- a/packages/spec/src/data/analytics.zod.ts +++ b/packages/spec/src/data/analytics.zod.ts @@ -172,7 +172,7 @@ function timeUpdateIntervalRefusalMessage(input: unknown): string { + `${declared}, so the name resolved to a refusal or to one group per distinct timestamp. ` + `Ask for the coarsest interval that still answers your question (${declared}), or drop ` + 'the key and group on the raw timestamp deliberately. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.' + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.' ); } return ( @@ -229,7 +229,7 @@ export type TimeUpdateInterval = z.input; * value still owes its author. */ const CUBE_MEMBER_NAME_MIGRATE = - 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; const cubeMemberNameRemoved = (qualifiedKey: string, bag: 'measures' | 'dimensions', member: string) => `\`${qualifiedKey}\` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — ` @@ -373,7 +373,7 @@ export const MetricSchema = lazySchema(() => strictObject( + '(canonical Query DSL FilterCondition), or declare the measure on an ADR-0021 dataset, whose ' + 'measure takes a structured `filter` — a metric\'s own `sql` is a column reference and ' + 'carries no condition. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', }, }, { @@ -517,7 +517,7 @@ const CUBE_JOIN_DERIVED_ON = + 'joined object and is the whole of the contract.'; const CUBE_JOIN_MIGRATE = - 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; const CUBE_JOIN_SQL_REMOVED = '`joins..sql` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — it ' @@ -622,7 +622,7 @@ const CUBE_REFRESH_KEY_REMOVED = + 'nothing read it: no analytics result is cached, so neither `every` nor `sql` ever refreshed ' + 'anything. Delete the key; every analytics query is computed when it is asked. A refresh cadence ' + 'is declared again when a result cache exists. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** * Cube Schema diff --git a/packages/spec/src/data/cube-member-inner-name-retirement.test.ts b/packages/spec/src/data/cube-member-inner-name-retirement.test.ts index b9df0c2258d..9fb89866921 100644 --- a/packages/spec/src/data/cube-member-inner-name-retirement.test.ts +++ b/packages/spec/src/data/cube-member-inner-name-retirement.test.ts @@ -75,9 +75,9 @@ const CUBE = { // Unanchored, because a thrown `ZodError`'s message is the JSON of its issues; // the key-first house convention is asserted on the issue message itself below. const METRIC_PRESCRIPTION = - /`measures\.\.name` was removed in @objectstack\/spec 17\.5\.0 \(ADR-0049 enforce-or-remove\).*the record key is the metric's name.*Delete the key\..*rename its key in `measures`.*Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand\./s; + /`measures\.\.name` was removed in @objectstack\/spec 17\.5\.0 \(ADR-0049 enforce-or-remove\).*the record key is the metric's name.*Delete the key\..*rename its key in `measures`.*Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand\./s; const DIMENSION_PRESCRIPTION = - /`dimensions\.\.name` was removed in @objectstack\/spec 17\.5\.0 \(ADR-0049 enforce-or-remove\).*the record key is the dimension's name.*Delete the key\..*rename its key in `dimensions`.*Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand\./s; + /`dimensions\.\.name` was removed in @objectstack\/spec 17\.5\.0 \(ADR-0049 enforce-or-remove\).*the record key is the dimension's name.*Delete the key\..*rename its key in `dimensions`.*Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand\./s; /** What a 17.4-or-earlier parse emitted: the REQUIRED inner name on every member, equal to its key. */ const persistedCube = () => ({ diff --git a/packages/spec/src/data/cube-refresh-key-retirement.test.ts b/packages/spec/src/data/cube-refresh-key-retirement.test.ts index cb0d30b9189..f19373150f3 100644 --- a/packages/spec/src/data/cube-refresh-key-retirement.test.ts +++ b/packages/spec/src/data/cube-refresh-key-retirement.test.ts @@ -64,7 +64,7 @@ const CUBE = { // The four clauses the ruling set: nothing read it, no cache exists, delete it, // and a cadence is declared again when a cache exists. const PRESCRIPTION = - /`analytics_cube\.refreshKey` was removed in @objectstack\/spec 17 \(ADR-0049 enforce-or-remove\) — nothing read it: no analytics result is cached.*Delete the key; every analytics query is computed when it is asked\. A refresh cadence is declared again when a result cache exists\. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand\./s; + /`analytics_cube\.refreshKey` was removed in @objectstack\/spec 17 \(ADR-0049 enforce-or-remove\) — nothing read it: no analytics result is cached.*Delete the key; every analytics query is computed when it is asked\. A refresh cadence is declared again when a result cache exists\. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand\./s; /** What an author could write under `refreshKey` before the removal. */ const AUTHORED = [ diff --git a/packages/spec/src/data/currency-precision-iso4217.test.ts b/packages/spec/src/data/currency-precision-iso4217.test.ts index 2b130a1e546..dbdbe0089ed 100644 --- a/packages/spec/src/data/currency-precision-iso4217.test.ts +++ b/packages/spec/src/data/currency-precision-iso4217.test.ts @@ -62,7 +62,7 @@ describe('`currencyConfig.precision` is removed: refused with the prescription, expect(issue.message).toContain('Do not move the number to the field-level `precision`'); expect(issue.message).toContain('Delete the key.'); expect(issue.message).toContain( - 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ); }); diff --git a/packages/spec/src/data/datasource.zod.ts b/packages/spec/src/data/datasource.zod.ts index bea475baf59..b959a3ec1b2 100644 --- a/packages/spec/src/data/datasource.zod.ts +++ b/packages/spec/src/data/datasource.zod.ts @@ -54,7 +54,7 @@ const RETIRED_CAPABILITIES: Record = { + 'metadata, so declaring a capability here never changed which engine path ran. Delete the ' + 'block. If you wrote `readOnly: true`, read its note below — it did NOT make anything ' + 'read-only. ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', readOnly: CAPABILITIES_REMOVED_PREFIX + '`readOnly` in particular NEVER made a datasource read-only: no write path consulted it, ' @@ -87,7 +87,7 @@ const RETIRED_DATASOURCE_BLOCKS: Record = { + 'enforced, but they are a DIFFERENT key on a different type and spell the delay `backoffMs`, ' + 'not `baseDelayMs`. Moving these values onto a hook or a job only makes sense if you ' + 'actually want that hook or job retried. ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', healthCheck: '`datasource.healthCheck` was removed in @objectstack/spec 17.0.0 (ADR-0049) — no ' + 'health-check loop ever read it, so `enabled: true` scheduled nothing and the two timeouts ' @@ -95,12 +95,12 @@ const RETIRED_DATASOURCE_BLOCKS: Record = { + '(`ping()` / `checkHealth()`), which the datasource admin service calls for "Test ' + 'connection". The only recurring datasource timer is `external.validation.checkIntervalMs`, ' + 'which checks SCHEMA DRIFT — a different concern, not a liveness probe. Delete the block. ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', externalLabel: '`external.label` was removed in @objectstack/spec 17.0.0 (ADR-0049) — nothing read ' + "the federation block's own label. Use the datasource's TOP-LEVEL `label`, which is what " + 'Setup → Datasources actually renders. ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', externalRequirePermission: '`external.requirePermission` was removed in @objectstack/spec 17.0.0 (ADR-0049) — no ' + 'authorization check ever consulted it, so a permission named here gated nothing. Access to ' @@ -108,7 +108,7 @@ const RETIRED_DATASOURCE_BLOCKS: Record = { + 'exactly as for a managed datasource. Naming a permission that is never required is the ' + 'false-compliance shape ADR-0049 exists to remove — grant or withhold the object ' + 'permissions instead. ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', }; /** @@ -166,7 +166,7 @@ const RETIRED_READ_REPLICAS = + 'Delete the key. There is no read-replica routing to migrate to — if your database fronts ' + 'its replicas behind one endpoint (pgpool, ProxySQL, an RDS reader endpoint), point ' + '`config` at that endpoint, which is the only read-scaling path that works today. ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** * Driver Identifier diff --git a/packages/spec/src/data/driver/memory.zod.ts b/packages/spec/src/data/driver/memory.zod.ts index 4e7b1c339cb..355f581cbd0 100644 --- a/packages/spec/src/data/driver/memory.zod.ts +++ b/packages/spec/src/data/driver/memory.zod.ts @@ -126,7 +126,7 @@ export const FilePersistenceConfigSchema = lazySchema(() => strictObject( + '@objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not ' + 'only in the describe prose. Rename the key to `autoSaveIntervalMs`; the value ' + '(milliseconds) and the 2000 default are unchanged. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), }, ).describe('File-system persistence configuration')); @@ -223,7 +223,7 @@ export const AutoPersistenceConfigSchema = lazySchema(() => strictObject( + '@objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not ' + 'only in the describe prose. Rename the key to `autoSaveIntervalMs`; the value ' + '(milliseconds) is unchanged. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** * localStorage key override when running in a browser. diff --git a/packages/spec/src/data/driver/turso.zod.ts b/packages/spec/src/data/driver/turso.zod.ts index eed1efdaa33..8ecc2e683f1 100644 --- a/packages/spec/src/data/driver/turso.zod.ts +++ b/packages/spec/src/data/driver/turso.zod.ts @@ -490,7 +490,7 @@ export const TursoConfigSchema = lazySchema(() => strictObject( '`turso config.timeout` was renamed to `timeoutMs` in @objectstack/spec 17 — the unit of a ' + 'duration-shaped number lives in the key name, not only in the describe prose. Rename the ' + 'key to `timeoutMs`; the value (milliseconds) is unchanged. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** Pin the transport instead of inferring it from `url`. */ diff --git a/packages/spec/src/data/field.zod.ts b/packages/spec/src/data/field.zod.ts index 4ac5e67179a..90f0b83561e 100644 --- a/packages/spec/src/data/field.zod.ts +++ b/packages/spec/src/data/field.zod.ts @@ -455,7 +455,7 @@ const CURRENCY_CONFIG_DECIMAL_PLACES_GUIDANCE: Readonly> + '— no renderer or runtime ever read it: ' + CURRENCY_DECIMAL_PLACES_ARE_THE_CURRENCYS + ' Delete the key. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', decimals: '`currencyConfig.decimals` is not a currency configuration key, and nothing replaces it: ' + CURRENCY_DECIMAL_PLACES_ARE_THE_CURRENCYS @@ -1838,7 +1838,7 @@ export const FieldSchema = lazySchema(() => { conditionalRequired: retiredKey( '`conditionalRequired` was removed in @objectstack/spec 17 — 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.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** diff --git a/packages/spec/src/data/hook-body.zod.ts b/packages/spec/src/data/hook-body.zod.ts index c5123b08bbb..55bf0f90fde 100644 --- a/packages/spec/src/data/hook-body.zod.ts +++ b/packages/spec/src/data/hook-body.zod.ts @@ -18,7 +18,7 @@ const CRYPTO_HASH_RETIRED = + 'in the host (a Connector recipe, or an engine-side hook) instead. If you need hashing in ' + 'a body, reopen it through the capability admission process — implementation first, the ' + 'declaration lands with the implementation. ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** * Capability tokens a script body may request. diff --git a/packages/spec/src/data/hook.zod.ts b/packages/spec/src/data/hook.zod.ts index b20a239cc9c..1e26d6a3b5b 100644 --- a/packages/spec/src/data/hook.zod.ts +++ b/packages/spec/src/data/hook.zod.ts @@ -413,7 +413,7 @@ export const HookSchema = lazySchema(() => strictObject( 'only in the description, beside a body-level `timeoutMs` and a `retryPolicy.backoffMs` that ' + 'spell theirs, so the same number read as two conventions on one surface. ' + 'Rename the key to `timeoutMs`; the value (milliseconds) is unchanged. ' + - 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** diff --git a/packages/spec/src/data/mapping.zod.ts b/packages/spec/src/data/mapping.zod.ts index 0cc71aefaae..ea5ecf870b2 100644 --- a/packages/spec/src/data/mapping.zod.ts +++ b/packages/spec/src/data/mapping.zod.ts @@ -44,14 +44,14 @@ const RETIRED_EXTRACT_QUERY = + 'export path that does not exist. Delete the key. Exports run through the ordinary ' + 'query API (`POST /api/v1/data/:object/query`); if a mapping-driven export is ever ' + 'designed, this is where it plugs back in. ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; const RETIRED_ERROR_POLICY = '`mapping.errorPolicy` was removed in @objectstack/spec 17.0.0 (ADR-0049) — no ' + 'import code ever read it, so `skip` / `abort` / `retry` selected between three ' + 'behaviours that were all the same behaviour. Delete the key. Error handling on the ' + 'import path belongs to the import REQUEST\'s own options, not to the stored mapping. ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; const RETIRED_BATCH_SIZE = '`mapping.batchSize` was removed in @objectstack/spec 17.0.0 (ADR-0049) — no ' @@ -61,7 +61,7 @@ const RETIRED_BATCH_SIZE = + 'the seed loader\'s and the NoSQL driver cursor\'s are all LIVE and enforced — but each is ' + 'a DIFFERENT key on a different type sizing its own path, and none of them sizes a ' + 'mapping import. ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; const MAPPING_RETIRED_KEY_GUIDANCE: Readonly> = { extractQuery: RETIRED_EXTRACT_QUERY, @@ -107,21 +107,21 @@ const RETIRED_LOOKUP_OBJECT = + '`reference` names the lookup object), so "Lookup Object" steered nothing. Delete the key; ' + 'point `target` at a reference field and the referenced object is the field\'s own ' + '`reference`. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; const RETIRED_LOOKUP_FROM_FIELD = '`fieldMapping[].params.fromField` was removed in @objectstack/spec 17 (ADR-0049) — ' + 'the `lookup` transform never read it: the import pipeline matches the cell\'s display ' + 'value (name / email / id) against the referenced object itself, not against a ' + 'mapping-declared match field, so "Match on" steered nothing. Delete the key. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; const RETIRED_LOOKUP_TO_FIELD = '`fieldMapping[].params.toField` was removed in @objectstack/spec 17 (ADR-0049) — ' + 'the `lookup` transform never read it: reference resolution always writes the referenced ' + 'record\'s id (what a reference column stores), so "Value to take" steered nothing. ' + 'Delete the key. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; const RETIRED_LOOKUP_AUTO_CREATE = '`fieldMapping[].params.autoCreate` was removed in @objectstack/spec 17 (ADR-0049) — ' @@ -129,7 +129,7 @@ const RETIRED_LOOKUP_AUTO_CREATE = + 'created: with or without this key, a cell that resolves to no record FAILS its row with an ' + 'unresolved-reference error (`import_reference_not_found`). Delete the key; create or ' + 'import the referenced records first, then import the rows that point at them. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; const PARAMS_RETIRED_KEY_GUIDANCE: Readonly> = { object: RETIRED_LOOKUP_OBJECT, diff --git a/packages/spec/src/data/object.zod.ts b/packages/spec/src/data/object.zod.ts index 0bfe6efcf46..20fb460d6f0 100644 --- a/packages/spec/src/data/object.zod.ts +++ b/packages/spec/src/data/object.zod.ts @@ -148,13 +148,13 @@ const CAPABILITIES_RETIRED_KEY_GUIDANCE: Record = { 'soft-delete that never ran). Delete the key. For recoverability use per-field ' + '`trackHistory` (audit trail) or a `lifecycle` policy; soft delete is parked, ' + 'and if built returns as a live enforced flag (ADR-0049 prune-or-build). ' + - 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', mru: '`enable.mru` was removed from @objectstack/spec in the 16.x line (' + 'ADR-0049) — Most-Recently-Used tracking was never implemented; no reader ' + 'existed, so the flag changed nothing. Delete the key. If MRU tracking is ' + 'built it returns as a live enforced flag (ADR-0049 prune-or-build). ' + - 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', }; /** @@ -411,7 +411,7 @@ const DECLARED_INDEX_BARE_TRUE_RETIRED = + "nothing on disk changes) or `unique: 'organization'` (one holder per organization — the " + 'driver prepends the NULL-safe organization key part to `fields` at registration). ' + 'Field-level `unique: true` is unaffected. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** * Prescriptive rejection for a mis-spelled `unique` scope **on a DECLARED @@ -546,7 +546,7 @@ export const IndexSchema = lazySchema(() => strictObject({ '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.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), partial: retiredKey( '`indexes[].partial` was removed in @objectstack/spec 17.0.0 (ADR-0049) — no ' + @@ -556,7 +556,7 @@ export const IndexSchema = lazySchema(() => strictObject({ '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.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), })); @@ -593,7 +593,7 @@ const TENANCY_RETIRED_KEY_GUIDANCE: Record = { '`@objectstack/metadata-core`; an object whose tenant column genuinely is not ' + '`organization_id` declares `tenancy.tenantField`, which both walls it and stamps ' + 'its platform rows. ' + - 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', }; /** @@ -1478,7 +1478,7 @@ const MANAGED_BY_SYSTEM_RETIRED = + 'and can be deleted; keep it only to NARROW. CSV `import` is deliberately NOT in that ' + 'default: it stays opt-in per object via `userActions: { import: true }`, which ' + 'is what a v16 `system` object already resolved to. ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** * Known-confusable schema keys → precise authoring guidance. diff --git a/packages/spec/src/data/query.zod.ts b/packages/spec/src/data/query.zod.ts index fff6642c8d9..62db53f803b 100644 --- a/packages/spec/src/data/query.zod.ts +++ b/packages/spec/src/data/query.zod.ts @@ -91,7 +91,7 @@ const AGG_RETIRED_TAIL = 'There is no replacement in the query vocabulary: read the rows with an ordinary `fields` ' + 'query and shape them in the caller, or model the roll-up as a stored field. It returns ' + 'only WITH a portable lowering — ADR-0049\'s enforce leg, implementation first. ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; const ARRAY_AGG_RETIRED = '`array_agg`' + AGG_RETIRED_MIDDLE diff --git a/packages/spec/src/data/unique-scope-message.test.ts b/packages/spec/src/data/unique-scope-message.test.ts index ff9bfc304e8..de793a74dc2 100644 --- a/packages/spec/src/data/unique-scope-message.test.ts +++ b/packages/spec/src/data/unique-scope-message.test.ts @@ -126,7 +126,7 @@ describe('unique scope rejection message — the two surfaces disagree about bar expect(issue.message).toContain("`unique: 'organization'` (one holder per organization"); expect(issue.message).toContain('Field-level `unique: true` is unaffected.'); expect(issue.message).toContain( - 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ); expect(issue.message).not.toMatch(/#\d{3,5}\b/); }); diff --git a/packages/spec/src/integration/connector-resilience-keys-retirement.test.ts b/packages/spec/src/integration/connector-resilience-keys-retirement.test.ts index f1f01fee1fd..fb4cd56b533 100644 --- a/packages/spec/src/integration/connector-resilience-keys-retirement.test.ts +++ b/packages/spec/src/integration/connector-resilience-keys-retirement.test.ts @@ -91,7 +91,7 @@ const POINTS_AT: Record = { }; const MIGRATE_SENTENCE = - /Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand\.$/; + /Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand\.$/; function issueAt(result: { success: boolean; error?: { issues: readonly { path: PropertyKey[]; code: string; message: string }[] } }, at: string) { expect(result.success, `the parse must refuse \`${at}\``).toBe(false); diff --git a/packages/spec/src/integration/connector-sync-retirement.test.ts b/packages/spec/src/integration/connector-sync-retirement.test.ts index 9a13d1f46b1..e21e1a27a85 100644 --- a/packages/spec/src/integration/connector-sync-retirement.test.ts +++ b/packages/spec/src/integration/connector-sync-retirement.test.ts @@ -86,7 +86,7 @@ const POINTS_AT = { } as const; const MIGRATE_SENTENCE = - /Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand\.$/; + /Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand\.$/; const KEYS = ['syncConfig', 'fieldMappings'] as const; const AUTHORED: Record<(typeof KEYS)[number], unknown> = { diff --git a/packages/spec/src/integration/connector-triggers-retirement.test.ts b/packages/spec/src/integration/connector-triggers-retirement.test.ts index 092c75278a5..9de511e7a4e 100644 --- a/packages/spec/src/integration/connector-triggers-retirement.test.ts +++ b/packages/spec/src/integration/connector-triggers-retirement.test.ts @@ -81,7 +81,7 @@ const PRESCRIPTION = /^`connector\.triggers` was removed in @objectstack\/spec 1 const POINTS_AT = ['`connector_action` node', 'an `api` flow', '`schedule` flow', 'Delete the key'] as const; const MIGRATE_SENTENCE = - /Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand\.$/; + /Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand\.$/; /** The provider-bound refusal's reason, which was untrue and must not survive anywhere. */ const OLD_REASON = 'derives them from the upstream'; diff --git a/packages/spec/src/integration/connector.test.ts b/packages/spec/src/integration/connector.test.ts index d8beb8d0afa..295008e88f9 100644 --- a/packages/spec/src/integration/connector.test.ts +++ b/packages/spec/src/integration/connector.test.ts @@ -1022,7 +1022,7 @@ describe('connector.errorMapping retirement', () => { // (`retired-key-migrate-sentence.test.ts`) holds the wording; this only // pins that THIS prescription carries it, with the right `--from`. expect(issue!.message).toMatch( - /Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand\.$/, + /Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand\.$/, ); // Customer-facing text carries the ADR, never an issue id — a `#NNNN` // token resolves to nothing for the reader who meets this refusal; the diff --git a/packages/spec/src/integration/connector.zod.ts b/packages/spec/src/integration/connector.zod.ts index d1902dadc01..84b04fa30ed 100644 --- a/packages/spec/src/integration/connector.zod.ts +++ b/packages/spec/src/integration/connector.zod.ts @@ -230,7 +230,7 @@ const SYNC_CONFIG_RETIRED = + 'names the `rest` or `openapi` connector it pulls from, the read action and an optional ' + 'timestamp `watermark`, with a `job` for the cadence. Its pull runs when a `job` drives it — ' + 'a `job` whose `pull: { mapping }` names that mapping; the binding alone moves no rows. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** * The prescription an author meets when they write `fieldMappings` on a @@ -244,7 +244,7 @@ const FIELD_MAPPINGS_RETIRED = + '`ConnectorFieldMapping` shape leaves with it. Map fields on the sync\'s TARGET instead: a ' + '`mapping`\'s `fieldMapping` (`source` → `target`, with a `transform` the import path ' + 'executes), which its `connectorSource` pulls through. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; // ============================================================================ // REMOVED: the connector-nested webhook shape (ADR-0049 enforce-or-remove) // ============================================================================ @@ -413,7 +413,7 @@ const ERROR_MAPPING_RETIRED = + '`ErrorMappingRule` and the `ConnectorErrorCategory` enum). There is no replacement, ' + "because no error-mapping engine exists: a connector's failures reach callers as the " + "provider's own errors (ADR-0097). " - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; // ============================================================================ // REMOVED: `connectionTimeoutMs` — the connect-phase deadline (ADR-0049) @@ -491,7 +491,7 @@ const CONNECTION_TIMEOUT_MS_RETIRED = + 'Use `requestTimeoutMs` for the deadline the platform does keep — it is applied as ' + "`resilientFetch`'s per-attempt timeout — and bound the connect phase at a connector " + 'provider or upstream gateway on a transport that can separate the phases. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** * The retired default the published 17.x toolchain MATERIALIZED, captured as a @@ -622,7 +622,7 @@ const HEALTH_RETIRED = + 'computed, not authored: `GET /api/v1/automation/connectors` reports each connector\'s ' + '`state` (`ready` or `degraded`). Put health probes and circuit breaking in the connector ' + 'provider or an upstream gateway. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** * The prescription an author meets when they write `status` — in `tsc` and at @@ -638,7 +638,7 @@ const STATUS_RETIRED = + 'withdraws a materialized instance or marks a catalog-only descriptor, and whether a ' + 'registered connector can be dispatched is computed by the runtime and reported as `state` ' + '(`ready` or `degraded`) on `GET /api/v1/automation/connectors` — no authored value sets it. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** * The prescription an author meets when they write `webhooks` on a connector — @@ -653,7 +653,7 @@ const WEBHOOKS_RETIRED = + '`WebhookSignatureAlgorithm`). To have a webhook actually sent, declare it in the stack\'s ' + 'top-level `webhooks:` collection, which is materialized into `sys_webhook` and delivered on ' + 'record events — note that doing so STARTS deliveries this connector never made. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; // ============================================================================ // REMOVED: `triggers` (ADR-0049) @@ -720,7 +720,7 @@ const TRIGGERS_RETIRED = + 'system, write a flow that calls the connector\'s action in a `connector_action` node: for ' + 'an external event, an `api` flow that the event\'s sender calls; for a scheduled pull, a ' + '`schedule` flow. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; // ============================================================================ // Base Connector Schema @@ -967,7 +967,7 @@ const ConnectorBaseSchema = lazySchema(() => z.object({ 'here was inert while reading like a configured cap. Delete the key. Do NOT substitute ' + '`shared` `RateLimitConfig` — that is the inbound limiter and would cap the wrong direction; ' + 'until an outbound throttle exists, rate-limit at the connector provider or upstream gateway. ' + - 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** diff --git a/packages/spec/src/kernel/manifest-permissions-string-list.test.ts b/packages/spec/src/kernel/manifest-permissions-string-list.test.ts index f15541df832..c53d6e5d9d4 100644 --- a/packages/spec/src/kernel/manifest-permissions-string-list.test.ts +++ b/packages/spec/src/kernel/manifest-permissions-string-list.test.ts @@ -59,7 +59,7 @@ const legal = () => ({ * two-clause `os migrate meta` sentence naming the case the conversion covers. */ const PRESCRIPTION = - /^Expected the plugin permission block `\{ services\?, hooks\?, network\?, fs\? \}`, received a flat list\..*removed in @objectstack\/spec 17 \(ADR-0049 enforce-or-remove\).*translate each one by hand, or delete `permissions` when the plugin needs none\. Run `os migrate meta --from 17` to list the mechanical edits for the package manifest case; a granted-permission record is not a source it reads\.$/s; + /^Expected the plugin permission block `\{ services\?, hooks\?, network\?, fs\? \}`, received a flat list\..*removed in @objectstack\/spec 17 \(ADR-0049 enforce-or-remove\).*translate each one by hand, or delete `permissions` when the plugin needs none\. Run `os migrate meta --from 17` to list the mechanical edits for the package manifest case; `--write` applies the ones it can prove, and a granted-permission record is not a source it reads\.$/s; const permissionsIssue = (input: unknown) => { const result = ManifestSchema.safeParse(input); diff --git a/packages/spec/src/kernel/manifest.zod.ts b/packages/spec/src/kernel/manifest.zod.ts index b422387cec5..619cb5d85c6 100644 --- a/packages/spec/src/kernel/manifest.zod.ts +++ b/packages/spec/src/kernel/manifest.zod.ts @@ -42,7 +42,8 @@ const PLUGIN_PERMISSIONS_LIST_FORM = + 'in the four lists instead — platform services, lifecycle hooks, network hosts and filesystem paths, ' + 'e.g. `{ services: [\'object\'], network: [\'api.acme.com\'] }`. A permission string has no mechanical ' + 'mapping onto them, so translate each one by hand, or delete `permissions` when the plugin needs none. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for the package manifest case; a granted-permission record is not a source it reads.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for the package manifest case; ' + + '`--write` applies the ones it can prove, and a granted-permission record is not a source it reads.'; const PLUGIN_PERMISSIONS_SHAPE = { services: z.array(z.string()).optional() diff --git a/packages/spec/src/migrations/entries/semantic/18.currency-config-precision-retired.ts b/packages/spec/src/migrations/entries/semantic/18.currency-config-precision-retired.ts index b3f083aae79..7ae3e649f4a 100644 --- a/packages/spec/src/migrations/entries/semantic/18.currency-config-precision-retired.ts +++ b/packages/spec/src/migrations/entries/semantic/18.currency-config-precision-retired.ts @@ -42,5 +42,5 @@ export const entry: SemanticMigration = { + 'its amounts exactly as before the upgrade, because the key never changed a rendered ' + 'amount. `os migrate meta --stored --apply` rewrites stored rows so the per-row notice ' + 'stops. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; ' - + 'apply them by hand.', + + '`--write` applies the ones it can prove, and you apply the rest by hand.', }; diff --git a/packages/spec/src/migrations/entries/semantic/18.flow-decision-edge-branching-first-match.ts b/packages/spec/src/migrations/entries/semantic/18.flow-decision-edge-branching-first-match.ts index 440dc7bf773..28f33b913bc 100644 --- a/packages/spec/src/migrations/entries/semantic/18.flow-decision-edge-branching-first-match.ts +++ b/packages/spec/src/migrations/entries/semantic/18.flow-decision-edge-branching-first-match.ts @@ -83,7 +83,7 @@ export const entry: SemanticMigration = { + '`mode` beside a non-empty `conditions` list, or with a `mode` outside ' + '`\'exclusive\' | \'inclusive\'`, is refused at registration and by `os validate` with the ' + 'schema\'s own sentence; nothing else about `conditions`-list decisions changes. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', // The edits this entry judges: every `mode: 'inclusive'` that conversion // writes is one decision to keep, delete or narrow, per the criteria above. conversionIds: ['flow-decision-mode-inclusive-explicit'], diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-report-joined-chart-retired.ts b/packages/spec/src/migrations/entries/semantic/18.ui-report-joined-chart-retired.ts index cf990528ea3..1e3260ff0af 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-report-joined-chart-retired.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-report-joined-chart-retired.ts @@ -63,5 +63,5 @@ export const entry: SemanticMigration = { + 'Census at the time of the change: zero joined reports with a chart in this repo\'s ' + 'example apps and in the hotcrm reference app, against a lit control (non-joined reports ' + 'carrying a chart: one and five). ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', }; diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index cd840b3e8cb..1173ab67cce 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -9645,7 +9645,7 @@ const step18: MigrationStep = { + 'its amounts exactly as before the upgrade, because the key never changed a rendered ' + 'amount. `os migrate meta --stored --apply` rewrites stored rows so the per-row notice ' + 'stops. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; ' - + 'apply them by hand.', + + '`--write` applies the ones it can prove, and you apply the rest by hand.', }, { id: 'dashboard-header-modal-target-page-only', @@ -13539,7 +13539,7 @@ const step18: MigrationStep = { + '`mode` beside a non-empty `conditions` list, or with a `mode` outside ' + '`\'exclusive\' | \'inclusive\'`, is refused at registration and by `os validate` with the ' + 'schema\'s own sentence; nothing else about `conditions`-list decisions changes. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', // The edits this entry judges: every `mode: 'inclusive'` that conversion // writes is one decision to keep, delete or narrow, per the criteria above. conversionIds: ['flow-decision-mode-inclusive-explicit'], @@ -22143,7 +22143,7 @@ const step18: MigrationStep = { + 'Census at the time of the change: zero joined reports with a chart in this repo\'s ' + 'example apps and in the hotcrm reference app, against a lit control (non-joined reports ' + 'carrying a chart: one and five). ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', }, { id: 'ui-report-joined-container-selection-refused', diff --git a/packages/spec/src/security/permission.zod.ts b/packages/spec/src/security/permission.zod.ts index 0dddd0bdb43..bc2476ae334 100644 --- a/packages/spec/src/security/permission.zod.ts +++ b/packages/spec/src/security/permission.zod.ts @@ -464,7 +464,7 @@ const ObjectPermissionBaseSchema = lazySchema(() => strictObject( 'granting the bit delivered nothing. Delete the key — a dispatched `restore` stays denied ' + 'fail-closed by the permission evaluator\'s destructive-operation backstop, and the bit ' + 'returns with the M2 lifecycle initiative alongside the operation it gates. ' + - 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), allowPurge: retiredKey( '`objects..allowPurge` was removed in @objectstack/spec 17 (ADR-0049) — ' + @@ -474,7 +474,7 @@ const ObjectPermissionBaseSchema = lazySchema(() => strictObject( 'dispatched `purge` stays denied fail-closed by the permission evaluator\'s ' + 'destructive-operation backstop, and the bit returns with the M2 lifecycle initiative' + ' alongside the operation it gates. ' + - 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** diff --git a/packages/spec/src/security/rls-tags-retirement.test.ts b/packages/spec/src/security/rls-tags-retirement.test.ts index 47bbb5a6df3..539c2c99ce1 100644 --- a/packages/spec/src/security/rls-tags-retirement.test.ts +++ b/packages/spec/src/security/rls-tags-retirement.test.ts @@ -65,7 +65,7 @@ const TAGS = ['compliance', 'gdpr']; // Unanchored, because a thrown `ZodError`'s message is the JSON of its issues; // the key-first house convention is asserted on the issue message itself below. const PRESCRIPTION = - /`rowLevelSecurity\[\]\.tags` was removed in @objectstack\/spec 17\.5\.0 \(ADR-0049 enforce-or-remove\).*Delete the key\..*`positions`.*Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand\./s; + /`rowLevelSecurity\[\]\.tags` was removed in @objectstack\/spec 17\.5\.0 \(ADR-0049 enforce-or-remove\).*Delete the key\..*`positions`.*Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand\./s; const permissionSet = (policy: Record) => ({ name: 'compliance_reviewer', diff --git a/packages/spec/src/security/rls.zod.ts b/packages/spec/src/security/rls.zod.ts index febec4e618c..d065797e736 100644 --- a/packages/spec/src/security/rls.zod.ts +++ b/packages/spec/src/security/rls.zod.ts @@ -134,7 +134,7 @@ const RLS_POLICY_TAGS_RETIRED = + 'on them, so a tag scoped, restricted and reported nothing. Delete the key. A tag never limited ' + 'whom a policy applies to; to do that, list the positions in `positions`. A policy is identified ' + 'by its `name` and its `object`; say why it exists in `description`. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** * Row-Level Security Policy Schema @@ -548,7 +548,7 @@ export const RowLevelSecurityPolicySchema = lazySchema(() => strictObject( priority: retiredKey( '`rowLevelSecurity[].priority` was removed in @objectstack/spec 17.0.0. ' + 'It never had an effect. 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.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** diff --git a/packages/spec/src/shared/mapping.zod.ts b/packages/spec/src/shared/mapping.zod.ts index 506db6ca70e..67e7d0b81e2 100644 --- a/packages/spec/src/shared/mapping.zod.ts +++ b/packages/spec/src/shared/mapping.zod.ts @@ -101,7 +101,7 @@ export const FieldMappingSchema = lazySchema(() => z.object({ + '`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.', + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** diff --git a/packages/spec/src/shared/retired-key-migrate-sentence.test.ts b/packages/spec/src/shared/retired-key-migrate-sentence.test.ts index 0e379551cf4..3d6e5a9fb2c 100644 --- a/packages/spec/src/shared/retired-key-migrate-sentence.test.ts +++ b/packages/spec/src/shared/retired-key-migrate-sentence.test.ts @@ -1,37 +1,44 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. /** - * [#6856, reworded #9529] Class pin: every `os migrate meta --from ` - * prescription sentence in `packages/spec/src` is the house sentence, whether - * the backing ADR-0087 conversion STRIPS the key or REWRITES the value - * (maintainer-ruled route D, 2026-08-09; reworded by maintainer ruling - * 2026-08-18): + * [#6856, reworded #9529, `--write` named #9591] Class pin: every + * `os migrate meta --from ` prescription sentence in the corpora below is + * the house sentence, whether the backing ADR-0087 conversion STRIPS the key + * or REWRITES the value (maintainer-ruled route D, 2026-08-09; reworded by + * maintainer ruling 2026-08-18): * * Run `os migrate meta --from ` to list the mechanical edits for - * existing sources; apply them by hand. + * existing sources; `--write` applies the ones it can prove, and you + * apply the rest by hand. * * The sentence states a property of the TOOL, never the fate of the key — the * retired "rewrite it" spelling was misread over strip conversions because * "it" has two antecedents (the key vs your sources), and the key's fate is * the body prose's job ("Delete the key…", "Rename the key to…"). * - * [#9529] It must also be TRUE of the tool, which is why the wording moved off - * "rewrite existing sources automatically": `os migrate meta` replays the chain - * in memory and prints the attributed mechanical change list; the only file it - * writes is the `--out` JSON snapshot. It has never written an authored source - * file, so 90-odd shipped prescriptions promised an affordance that did not - * exist. This pin therefore holds BOTH directions — the new sentence is - * required where a prescription names the command, and the withdrawn claim is - * a hard RED wherever it reappears. Full rule: `shared/retired-key.ts` module - * docblock. (When #9591's in-place codemod lands, the claim may be restored — - * by editing this pin in the same PR, never by exempting a site.) + * [#9529] It must also be TRUE of the tool. The sentence once promised + * "rewrite existing sources automatically" while `os migrate meta` wrote no + * authored source at all, so #9529 withdrew the claim. [#9591] `--write` has + * since landed, and the claim came back only as far as the tool honours it: + * the default run still writes no source (it lists the mechanical edits and + * writes only the `--out` snapshot), and `--write` rewrites in place only the + * edits it can prove — each traced to one literal in one project file — and + * lists every other with its reason. Hence the sentence NAMES `--write` and + * QUALIFIES it, and the rest stays the author's. This pin holds every + * direction: the house sentence is required where a prescription names the + * command (so the #9529 sentence, which never mentions `--write`, is now RED + * too); the unqualified automatic-rewrite claim stays a hard RED wherever it + * reappears; and the two facts the sentence rests on — that `--write` exists + * and that it is not the default — are read from the command itself. Full + * rule: `shared/retired-key.ts` module docblock. * * ONE allowed variant, by SHAPE and never by site (#6935's no-allowlist * discipline): a conversion that covers only PART of the value keeps the - * two-clause form naming which part — - * "… to list the mechanical edits for the case; ." (`ui/dashboard.zod.ts` `compareTo.offset` is the model; the - * script node's `config.actionType` is the other member.) + * two-clause form naming which part, with the same `--write` clause — + * "… to list the mechanical edits for the case; `--write` applies the + * ones it can prove, and ." + * (`ui/dashboard.zod.ts` `compareTo.offset` is the model; the script node's + * `config.actionType` is the other member.) * * [#7030] Widened, not duplicated: `packages/lint/src/validate-expressions.ts` * carries one live occurrence of the identical sentence (the lint diagnostic @@ -44,6 +51,12 @@ * site could only drift from this one the moment either wording changes; * one pin covering both corpora cannot. * + * [#9591] Widened once more, on the same terms: `@objectstack/driver-turso`'s + * config schema carries three live occurrences (its `timeout` / `localPath` / + * `wasm` tombstones), whose docblock defers to `retired-key.ts` for the + * wording. The pin did not walk it, so a rewording would have left those three + * on the old sentence with every assertion here green. + * * Mechanism: a SOURCE scan over string literals (this pin pins textual facts — * the sentences ARE text in source). Comment lines are skipped: descriptive * prose about the tool ("`os migrate meta` rewrites sources") is not a @@ -52,6 +65,11 @@ * not tombstone prescriptions an author meets in a parse error. That is a * scope bound on the spec corpus, not a per-site exemption: every * prescription string in every scanned file is judged, with no allowlist. + * The scan reads single- and double-quoted literals only: inside a template + * literal the backticks are escaped, so `MARKER` never matches there. A + * sentence that should be judged is therefore written plain-quoted, as the + * lint corpus's two sites are (`validate-expressions.ts`, + * `data-model-rules.ts`). * * What this pin deliberately does NOT check: a tombstone whose prescription * carries no `os migrate meta` sentence at all (#6914's worklist) — absence of @@ -67,8 +85,15 @@ import { describe, expect, it } from 'vitest'; const HERE = path.dirname(url.fileURLToPath(import.meta.url)); const SPEC_SRC_ROOT = path.resolve(HERE, '..'); -/** #7030: `packages/lint/src`, the one other corpus carrying this sentence. */ +/** #7030: `packages/lint/src`, a second corpus carrying this sentence. */ const LINT_SRC_ROOT = path.resolve(HERE, '../../../lint/src'); +/** #9591: `@objectstack/driver-turso`'s config schema, a third. */ +const TURSO_SRC_ROOT = path.resolve(HERE, '../../../drivers/driver-turso/src'); +/** + * #9591: the command the sentence names. Read, never imported — the pin asks + * two facts of the command's own flag table, not of a built CLI. + */ +const MIGRATE_META_COMMAND = path.resolve(HERE, '../../../cli/src/commands/migrate/meta.ts'); /** * [#10848] The population widened by EXACTLY ONE governed file (maintainer * ruling 2026-08-22, deliberately not all of `.claude/`): the retirement @@ -123,6 +148,12 @@ const CORPORA: Corpus[] = [ root: LINT_SRC_ROOT, outOfScope: new Set(), }, + { + // #9591: the `turso` config tombstones (`spec/turso.zod.ts`). + name: 'driver-turso', + root: TURSO_SRC_ROOT, + outOfScope: new Set(), + }, ]; const MARKER = /(?:Run )?`os migrate meta --from \d+`/g; @@ -133,21 +164,23 @@ const MARKER = /(?:Run )?`os migrate meta --from \d+`/g; * quote), so a prescription cannot bury the command mid-prose either. */ const HOUSE_AT_MARKER = - /^Run `os migrate meta --from \d+` to list the mechanical edits for existing sources; apply them by hand\.['"]/; + /^Run `os migrate meta --from \d+` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand\.['"]/; /** * MIXED two-clause shape: clause one names the part of the value the chain - * covers mechanically, clause two says what it does with the rest. Shape, not - * sites. + * covers mechanically, clause two says what `--write` does with it and what + * happens to the rest. Shape, not sites. */ const MIXED_AT_MARKER = - /^Run `os migrate meta --from \d+` to list the mechanical edits for the [^;'"]+ case[^;'"]*; [^;'"]+\.['"]/; + /^Run `os migrate meta --from \d+` to list the mechanical edits for the [^;'"]+ case[^;'"]*; `--write` applies the ones it can prove, and [^;'"]+\.['"]/; /** * [#9529] The withdrawn claim, in every spelling the sweep found. Judged over * the SAME reconstructed text as the house form, but as a hard RED wherever it * appears — a site reverting to it fails even if it never names `--from ` - * (three enum-value prescriptions spell the bare command). + * (a prescription can spell the bare command, or name the tool mid-prose). + * [#9591] Unchanged by `--write`: the tool still never rewrites existing + * sources UNQUALIFIED, so every spelling below stays withdrawn. */ const WITHDRAWN_CLAIM = /to rewrite (?:existing sources|it) automatically|rewrites (?:it for you|existing sources|author(?:ed)? sources|your sources?|your source files?)/g; @@ -268,7 +301,7 @@ function claimTree(): Array<{ file: string; line: number; excerpt: string }> { } describe('`os migrate meta` sentences are the house sentence, across corpora', () => { - it('every prescription sentence in packages/spec/src and packages/lint/src is house-form or MIXED two-clause', () => { + it('every prescription sentence in every corpus is house-form or MIXED two-clause', () => { const judged = judgeTree(); const violations = judged.filter((j) => !j.ok); expect( @@ -292,22 +325,38 @@ describe('`os migrate meta` sentences are the house sentence, across corpora', ( expect(judged.every((j) => j.ok)).toBe(true); }); - it('anti-vacuity: the lint corpus specifically is reached, not just outnumbered by spec', () => { + it('anti-vacuity: each widened corpus is reached, not just outnumbered by spec', () => { // The combined floor above (>=50) is already satisfied by packages/spec/src - // alone, so a broken LINT_SRC_ROOT (wrong relative path, corpus silently - // walking zero files) would NOT fail it — this assertion is the one thing - // that actually exercises #7030's widening rather than merely declaring it. + // alone, so a broken LINT_SRC_ROOT or TURSO_SRC_ROOT (wrong relative path, + // corpus silently walking zero files) would NOT fail it — this assertion is + // the one thing that actually exercises #7030's and #9591's widenings + // rather than merely declaring them. const judged = judgeTree(); - const lintSites = judged.filter((j) => j.file.startsWith('lint:')); - expect(lintSites.length).toBeGreaterThanOrEqual(1); - expect(lintSites.every((j) => j.ok)).toBe(true); + for (const corpus of CORPORA.filter((c) => c.name !== 'spec')) { + const sites = judged.filter((j) => j.file.startsWith(`${corpus.name}:`)); + expect(sites.length, corpus.name).toBeGreaterThanOrEqual(1); + expect(sites.every((j) => j.ok), corpus.name).toBe(true); + } + }); + + it('the sentence is TRUE of the command it names: `--write` exists, and the default run does not write', () => { + // The two facts the house sentence rests on, read from `os migrate meta`'s + // own flag table: it names `--write` because that flag exists, and it says + // the default run LISTS because `--write` is not the default. Renaming the + // flag, or flipping its default, would leave every prescription above + // stating something the tool no longer does — so either reds here, not in + // an author's terminal. + const command = fs.readFileSync(MIGRATE_META_COMMAND, 'utf8'); + const flag = /\n\s*write: Flags\.boolean\(\{([\s\S]*?)\n\s*\}\),/.exec(command); + expect(flag, 'os migrate meta declares no `write` boolean flag').not.toBeNull(); + expect(flag![1]).toMatch(/\bdefault: false\b/); }); it('the withdrawn automatic-rewrite claim is absent from every prescription', () => { - // The other direction of the same ruling: requiring the new sentence where + // The other direction of the same ruling: requiring the house sentence where // `--from ` appears would still let the claim survive in a prescription - // that spells the bare command (`CHATTER_POSITION_RETIRED` does) or names - // the tool mid-prose. `os migrate meta` writes no authored source file. + // that spells the bare command or names the tool mid-prose. `--write` + // rewrites only the edits it can prove; no spelling here is ever true. const claims = claimTree(); expect( claims, @@ -329,11 +378,17 @@ describe('`os migrate meta` sentences are the house sentence, across corpora', ( for (const src of withdrawn) { expect(findWithdrawnClaims(src, 'withdrawn.zod.ts'), src).not.toEqual([]); } - // And the house sentence itself must NOT trip it. + // And neither legal shape may trip it: naming `--write` is not the claim. expect(findWithdrawnClaims( - "const h = 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.';", + "const h = 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; " + + "`--write` applies the ones it can prove, and you apply the rest by hand.';", 'house.zod.ts', )).toEqual([]); + expect(findWithdrawnClaims( + "const m = 'Run `os migrate meta --from 16` to list the mechanical edits for the `1y` case; " + + "`--write` applies the ones it can prove, and the other durations are reported for you to re-state.';", + 'mixed.zod.ts', + )).toEqual([]); }); it('goes RED on the retired "rewrite it" spelling, naming the site', () => { @@ -360,10 +415,21 @@ describe('`os migrate meta` sentences are the house sentence, across corpora', ( "'Delete the key. Run `os migrate meta --from 16` to rewrite existing sources automatically.'", // …and the MIXED shape's withdrawn spelling. "'Run `os migrate meta --from 16` to rewrite the `1y` case automatically; the rest are reported.'", + // #9591: the #9529 sentence, true of the default run but silent on + // `--write` — the ruling requires the sentence to name it. + "'Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.'", + // …and the MIXED shape's #9529 spelling, silent on `--write` the same way. + "'Run `os migrate meta --from 16` to list the mechanical edits for the `1y` case; the rest are reported.'", + // #9591: names `--write` but drops the qualification — the unproved edits + // are left unaccounted for, which is the unqualified claim by omission. + "'Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies them.'", + // #9591: names `--write` and qualifies it, but leaves the rest unowned. + "'Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove.'", // Emptied sentence: the command with no object at all. "'Delete the key. Run `os migrate meta --from 16`.'", // Sentence not final in its literal: prose buries the command. - "'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. Also do X.'", + "'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; " + + "`--write` applies the ones it can prove, and you apply the rest by hand. Also do X.'", ]; for (const literal of bad) { const judged = judgeMigrateSentences(`const s = ${literal};`, 'bad.zod.ts'); @@ -375,13 +441,20 @@ describe('`os migrate meta` sentences are the house sentence, across corpora', ( it('accepts the two legal shapes, including across concatenation seams', () => { const good = [ // House, single literal. - "const a = '`k` was removed (#1). Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.';", + "const a = '`k` was removed (#1). Delete the key. Run `os migrate meta --from 16` to list the mechanical edits " + + "for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.';", // House, sentence split across a cross-line concatenation seam. - "const b = '`k` was removed (#1). Run `os migrate meta --from 16` to list the mechanical edits for existing sources; '\n + 'apply them by hand.';", + "const b = '`k` was removed (#1). Run `os migrate meta --from 16` to list the mechanical edits for existing sources; '\n" + + " + '`--write` applies the ones it can prove, and you apply the rest by hand.';", + // House, split mid-clause across a double-quoted seam. + "const c = \"Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies \"\n" + + " + 'the ones it can prove, and you apply the rest by hand.';", // MIXED two-clause (the dashboard model). - "const c = 'Run `os migrate meta --from 16` to list the mechanical edits for the `1y` case; the other durations are reported for you to re-state.';", + "const d = 'Run `os migrate meta --from 16` to list the mechanical edits for the `1y` case; " + + "`--write` applies the ones it can prove, and the other durations are reported for you to re-state.';", // MIXED two-clause (the script `actionType` member). - "const d = 'Run `os migrate meta --from 16` to list the mechanical edits for the shorthand case into `config.function`; the stub and marker values are removed.';", + "const e = 'Run `os migrate meta --from 16` to list the mechanical edits for the shorthand case into " + + "`config.function`; `--write` applies the ones it can prove, and the stub and marker values are removed.';", ]; for (const src of good) { const judged = judgeMigrateSentences(src, 'good.zod.ts'); @@ -429,8 +502,8 @@ describe('`os migrate meta` sentences are the house sentence, across corpora', ( * property, since nothing but a code span could ever satisfy it, but a trap * that fires the moment the corpus stops being one file. So a non-span * occurrence is judged for its WORDING, ending at its own period. What that - * gives up, deliberately and only in prose: burying (`… apply them by hand. - * Also do X.`) passes there, while it stays RED inside a template and in every + * gives up, deliberately and only in prose: burying (`… apply the rest by + * hand. Also do X.`) passes there, while it stays RED inside a template and in every * `.ts` prescription above — the two places a reader copies text from. */ const SKILL_FROM_OPERAND = /(?:\d+|<[^>`]+>)/.source; @@ -439,8 +512,8 @@ const SKILL_MARKER = new RegExp( 'g', ); /** The two legal wordings, from the marker up to the sentence's final period. */ -const SKILL_HOUSE_BODY = `^Run \`os migrate meta --from ${SKILL_FROM_OPERAND}\` to list the mechanical edits for existing sources; apply them by hand\\.`; -const SKILL_MIXED_BODY = `^Run \`os migrate meta --from ${SKILL_FROM_OPERAND}\` to list the mechanical edits for the [^;]+ case[^;]*; [^;]+?\\.`; +const SKILL_HOUSE_BODY = `^Run \`os migrate meta --from ${SKILL_FROM_OPERAND}\` to list the mechanical edits for existing sources; \`--write\` applies the ones it can prove, and you apply the rest by hand\\.`; +const SKILL_MIXED_BODY = `^Run \`os migrate meta --from ${SKILL_FROM_OPERAND}\` to list the mechanical edits for the [^;]+ case[^;]*; \`--write\` applies the ones it can prove, and [^;]+?\\.`; /** Container-final anchors: a code span closes; a quoted sentence just ends. */ const SKILL_TEMPLATE_END = '``'; const SKILL_PROSE_END = '(?:\\s|$)'; @@ -568,8 +641,10 @@ describe('the retirement playbook and the published skill catalog agree with thi it('the markdown judge is not vacuous — template and prose anchors each hold', () => { const judge = (flat: string): MarkdownSite[] => judgeMarkdownSentences({ file: 'synthetic.md', flat }); - const house = 'Run `os migrate meta --from ` to list the mechanical edits for existing sources; apply them by hand.'; - const mixed = 'Run `os migrate meta --from 16` to list the mechanical edits for the `1y` case; the rest are reported.'; + const house = 'Run `os migrate meta --from ` to list the mechanical edits for existing sources; ' + + '`--write` applies the ones it can prove, and you apply the rest by hand.'; + const mixed = 'Run `os migrate meta --from 16` to list the mechanical edits for the `1y` case; ' + + '`--write` applies the ones it can prove, and the rest are reported.'; // A taught template must close its code span, in both legal shapes. expect(judge(`5. \`\`${house}\`\``).map((s) => [s.template, s.ok])).toEqual([[true, true]]); expect(judge(`\`\`${mixed}\`\``).map((s) => [s.template, s.ok])).toEqual([[true, true]]); @@ -579,6 +654,10 @@ describe('the retirement playbook and the published skill catalog agree with thi expect(judge(`error text … ${house} expected: never`).map((s) => [s.template, s.ok])).toEqual([[false, true]]); expect(judge('Run `os migrate meta --from 16` to rewrite it automatically.').map((s) => s.ok)).toEqual([false]); expect(judge('Run `os migrate meta --from 16` to remove it.').map((s) => s.ok)).toEqual([false]); + // #9591: the #9529 template, silent on `--write`, is no longer taught. + expect(judge( + '``Run `os migrate meta --from ` to list the mechanical edits for existing sources; apply them by hand.``', + ).map((s) => [s.template, s.ok])).toEqual([[true, false]]); // Naming the command mid-prose without the leading `Run` is not a sentence. expect(judge('Stored flows convert with `os migrate meta --from 16`.')).toEqual([]); // The withdrawn claim trips wherever it appears, fence or prose. diff --git a/packages/spec/src/shared/retired-key.test.ts b/packages/spec/src/shared/retired-key.test.ts index fa427a47937..ce257d8c22e 100644 --- a/packages/spec/src/shared/retired-key.test.ts +++ b/packages/spec/src/shared/retired-key.test.ts @@ -174,7 +174,7 @@ const HEADING_RETIRED = '`text.variant: "heading"` was removed in @objectstack/spec 99 — a heading is a ' + 'document level, never a text style, so the renderer had to guess one. Use `h2`, or pick the ' + 'level you mean. ' - + 'Run `os migrate meta --from 98` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 98` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** A second retirement on the SAME enum, and one with no conversion behind it. */ const SUBHEADING_RETIRED = diff --git a/packages/spec/src/shared/retired-key.ts b/packages/spec/src/shared/retired-key.ts index 0b3d44fb78e..20a4fde53f4 100644 --- a/packages/spec/src/shared/retired-key.ts +++ b/packages/spec/src/shared/retired-key.ts @@ -52,36 +52,41 @@ * A prescription whose surface an ADR-0087 conversion covers closes with * exactly this sentence, whether the conversion STRIPS the key or REWRITES * its value (#6856, maintainer-ruled 2026-08-09; reworded #9529, - * maintainer-ruled 2026-08-18): + * maintainer-ruled 2026-08-18; `--write` named #9591): * * Run `os migrate meta --from ` to list the mechanical edits for - * existing sources; apply them by hand. + * existing sources; `--write` applies the ones it can prove, and you + * apply the rest by hand. * * The sentence states a property of the TOOL — what running it gets you — * never the fate of the key. Two rulings shaped it, and both still bind: * - * - **It must be TRUE of the tool.** The sentence used to promise - * "rewrite existing sources automatically", and `os migrate meta` has - * never written an authored source file: it replays the conversion - * chain over the loaded stack in memory, prints the attributed - * mechanical change list (`Applied N mechanical change(s)`, one line per - * site), and writes exactly one file — the `--out` JSON snapshot, when - * you ask for it. Porting the listed edits into the project's own `.ts` - * sources is the author's work, which is why the sentence says so - * (#9529; the in-place AST codemod is commissioned separately as #9591, - * and the automatic-rewrite claim may return with it). + * - **It must be TRUE of the tool.** The default run writes no authored + * source file: it replays the conversion chain over the loaded stack in + * memory, prints the attributed mechanical change list (`Applied N + * mechanical change(s)`, one line per site), and writes only the `--out` + * JSON snapshot, when you ask for it. `--write` rewrites in place only + * the edits it can prove — each traced to one literal in one project + * file, held to a re-run of the chain — and lists every other with the + * reason it was not written. So the sentence names `--write` (the + * default run still only lists), and it never says "rewrite existing + * sources automatically" unqualified: the edits `--write` cannot prove + * are the author's work, which is why the sentence says so. The + * unqualified claim stays a hard RED in the class pin. * - **One antecedent.** The retired "rewrite it" spelling was misread over * strip conversions because "it" names either the key or your sources; - * "existing sources" names one thing. The KEY's fate belongs in the body + * "existing sources" names one thing, and "the ones" can only be edits + * (an edit is what gets applied). The KEY's fate belongs in the body * prose ("Delete the key…", "Rename the key to…"), which every * prescription already carries — the sentence never restates it. * * ONE exception: a conversion that covers only PART of the value keeps the * two-clause form naming which part — "… to list the mechanical edits for - * the case; ." (model: - * `ui/dashboard.zod.ts` `compareTo.offset`). Both shapes are pinned - * class-wide by `retired-key-migrate-sentence.test.ts`; a new spelling - * fails the pin, not code review. + * the case; `--write` applies the ones it can prove, and ." (model: `ui/dashboard.zod.ts` `compareTo.offset`). + * Both shapes are pinned class-wide by + * `retired-key-migrate-sentence.test.ts`; a new spelling fails the pin, not + * code review. * * Tombstones age out, exactly like the `UNKNOWN_KEY_GUIDANCE` entries in * `data/object.zod.ts`: drop one ~two majors after the removal, by which point @@ -172,7 +177,8 @@ type Tombstone = z.ZodOptional>; * conditionalRequired: retiredKey( * '`conditionalRequired` was removed in @objectstack/spec 17.0.0 (#3855). ' + * 'Rename the key to `requiredWhen` — 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.', + * 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; ' + + * '`--write` applies the ones it can prove, and you apply the rest by hand.', * ), * ``` */ diff --git a/packages/spec/src/shared/retry-policy.zod.ts b/packages/spec/src/shared/retry-policy.zod.ts index 9ac7fc9c4ee..779bad5bc50 100644 --- a/packages/spec/src/shared/retry-policy.zod.ts +++ b/packages/spec/src/shared/retry-policy.zod.ts @@ -126,7 +126,7 @@ export function retryPolicyShape() { "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.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), }; } diff --git a/packages/spec/src/stack.zod.ts b/packages/spec/src/stack.zod.ts index 33d56a7c04b..8ee45603473 100644 --- a/packages/spec/src/stack.zod.ts +++ b/packages/spec/src/stack.zod.ts @@ -560,7 +560,7 @@ const STACK_DEFINITION_COLLECTIONS_SHAPE = { + "form view, a share link, or `book.audience: 'public'`. A stack that mounts no auth at all now " + 'fails at boot rather than silently serving object data to anonymous callers. ' + 'Run `os migrate meta --from 16` to list the mechanical edits for existing ' - + 'sources; apply them by hand.', + + 'sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** Enable environment-scoped routing for data/meta/AI APIs. */ enableProjectScoping: z.boolean().optional(), diff --git a/packages/spec/src/system/book.zod.ts b/packages/spec/src/system/book.zod.ts index a3e0522bd71..41cd8fc2888 100644 --- a/packages/spec/src/system/book.zod.ts +++ b/packages/spec/src/system/book.zod.ts @@ -76,7 +76,7 @@ const BOOK_TRANSLATIONS_RETIRED = + '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.'; + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; export const BookGroupSchema = lazySchema(() => z.object({ diff --git a/packages/spec/src/system/job.zod.ts b/packages/spec/src/system/job.zod.ts index 02454615c12..f3680455f83 100644 --- a/packages/spec/src/system/job.zod.ts +++ b/packages/spec/src/system/job.zod.ts @@ -167,7 +167,7 @@ const JOB_ID_RETIRED = + 'key, the `sys_job` row key, and the `JobExecution.jobId` stamp. Two jobs differing only ' + 'in `id` were the same job. Delete the key; rename the job via `name` if you need a ' + 'different identity. ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** * `job.timeout` → `job.timeoutMs` (#14478). The unit lived only in the @@ -180,7 +180,7 @@ const JOB_TIMEOUT_RETIRED = + 'in the description while the sibling `retryPolicy.backoffMs` spells its own, so the same number ' + 'read as two conventions on one surface. Rename the key to `timeoutMs`; the value (milliseconds) ' + 'is unchanged. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** * A job with nothing to run. Before `body` existed `handler` was required, so diff --git a/packages/spec/src/system/translation.zod.ts b/packages/spec/src/system/translation.zod.ts index 8bdef8c3164..2d70234867c 100644 --- a/packages/spec/src/system/translation.zod.ts +++ b/packages/spec/src/system/translation.zod.ts @@ -552,7 +552,7 @@ const TRANSLATION_KEY_GUIDANCE: Record._validations..message', which the write " + 'path resolves. Delete this key. Run ' - + '`os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', + + '`os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', o: "`o` is the retired object-first dialect, which no resolver reads — use 'objects.'", app: "`app` is the retired object-first dialect, which no resolver reads — use 'apps.'", nav: "`nav` is the retired object-first dialect, which no resolver reads — use 'apps..navigation..label'", @@ -601,8 +601,8 @@ const PER_APP_SETTINGS_PLATFORM_ONLY = + 'on this face, so the Settings UI shell strings an application may translate (the source ' + 'badges, under `settingsCommon.sourceLabels`) are NOT what is being refused here — only the ' + "per-namespace manifest copy under 'settings' is. " - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply ' - + 'them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies ' + + 'the ones it can prove, and you apply the rest by hand.'; /** The per-app door's guidance: the shared table plus the platform-only `settings`. */ const APP_TRANSLATION_KEY_GUIDANCE: Record = { @@ -642,8 +642,8 @@ const ITEM_SETTINGS_PLATFORM_ONLY = + "bundle declares, 'settingsCommon' among them: the Settings UI shell strings an application " + 'may translate (the source badges, under `settingsCommon.sourceLabels`) are NOT what is being ' + "refused here — only the per-namespace manifest copy under 'settings' is. " - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply ' - + 'them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies ' + + 'the ones it can prove, and you apply the rest by hand.'; /** The item door's guidance: the shared table plus the platform-only `settings`. */ const ITEM_TRANSLATION_KEY_GUIDANCE: Record = { @@ -754,7 +754,7 @@ const WIDGET_SUB_CAPTION_RETIRED = + 'wrote, so it translated a string that existed only when this entry put it there. Delete the ' + 'entry. A widget has one authored description, `widget.description`, which renders as the ' + "card-header subtitle; translate it through this widget's `description` entry. " - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** * The former `subtitle → subCaption` alias, re-homed as `guidance` when @@ -1162,7 +1162,7 @@ const appTranslationDataShape = () => ({ + '`ComponentPropsMap` declares it and the resolver no longer overlays it. The live form ' + "surface's submit copy is `object-form`'s `submitText` (`I18nLabelSchema`), localizable " + 'at its own authoring site. Delete the key. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', submit: '`submit` was the alias spelling of `submitLabel`, which was removed in ' + '@objectstack/spec 17 (ADR-0049) — no component in `ComponentPropsMap` ' diff --git a/packages/spec/src/ui/action.zod.ts b/packages/spec/src/ui/action.zod.ts index bf2974e7e5c..8038754f9ce 100644 --- a/packages/spec/src/ui/action.zod.ts +++ b/packages/spec/src/ui/action.zod.ts @@ -622,7 +622,7 @@ const GLOBAL_NAV_RETIRED = + '`record_related`, `record_section`), or — for an action that deliberately has no UI home, ' + 'such as an object-less one invoked over REST/MCP/AI — declare it headless with ' + '`locations: []`, which keeps its capability gate, param contract and audit trail. ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** * Action Location — where an action is allowed to surface in the UI. @@ -1222,7 +1222,7 @@ const actionObject = () => strictObject({ execute: retiredKey( '`execute` was removed in @objectstack/spec 17 — 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.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** @@ -1534,14 +1534,14 @@ const actionObject = () => strictObject({ "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.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), bulkEnabled: retiredKey( '`action.bulkEnabled` was removed in @objectstack/spec 17.0.0 (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.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** @@ -1785,7 +1785,7 @@ const actionObject = () => strictObject({ 'actions, author `ariaLabel` / `ariaDescribedBy` / `role` in the `aria` block of the placing ' + 'node: `page.components[].aria` (the component that renders the actions) or the list view ' + '`aria`. ' + - 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), // ADR-0010 — runtime protection envelope (internal — set by the loader). diff --git a/packages/spec/src/ui/app.zod.ts b/packages/spec/src/ui/app.zod.ts index 57952abe704..cf863f614ae 100644 --- a/packages/spec/src/ui/app.zod.ts +++ b/packages/spec/src/ui/app.zod.ts @@ -971,7 +971,7 @@ const AREA_ORDER_RETIRED = + 'authored, so declaration order already IS display order. Delete the key and reorder the ' + '`areas` array itself. NOTE the neighbour that behaves differently: a navigation ITEM\'s ' + '`order` is genuinely sorted — this removal does not touch it. Run ' - + '`os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.'; + + '`os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** * `app.areas[].visible` and `app.areas[].requiredPermissions`, retired in @@ -1022,7 +1022,7 @@ const AREA_VISIBLE_RETIRED = + 'server-side. The distinction survives at every level: `visible` is CEL ' + 'evaluated in the browser, so it hides an entry that has already been sent, while ' + '`requiredPermissions` stops that entry from being served at all. Run ' - + '`os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.'; + + '`os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; const AREA_REQUIRED_PERMISSIONS_RETIRED = '`areas[].requiredPermissions` was removed in @objectstack/spec 17.0.0 (ADR-0049) — ' @@ -1037,7 +1037,7 @@ const AREA_REQUIRED_PERMISSIONS_RETIRED = + 'area-level key is not revived. Still evaluated client-side ONLY, at every level: ' + '`visible` (CEL) and `requiresObject` — so anything that must never reach the browser ' + 'goes in `requiredPermissions`, never in `visible`. ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** * Navigation Area Schema @@ -1177,7 +1177,7 @@ const CONTEXT_SELECTOR_RETIRED_KEY_GUIDANCE: Readonly> = + 'offered an All row regardless of this flag, so `includeAll: false` hardened nothing ' + 'and `includeAll: true` unlocked nothing. Delete the key. To widen what a selector ' + 'offers, widen `optionsSource.filter` instead. ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', showall: '`contextSelectors[].includeAll` (which `showall` aliased) was removed in ' + '@objectstack/spec 17.0.0 — selectors are mandatory-scope and never render an ' @@ -1186,7 +1186,7 @@ const CONTEXT_SELECTOR_RETIRED_KEY_GUIDANCE: Readonly> = '`contextSelectors[].placement` was removed in @objectstack/spec 17.0.0 (' + 'ADR-0049) — no renderer ever read it. Selectors always render in the sidebar header ' + "block, and `'topbar'` placed nothing in the topbar. Delete the key. Run " - + '`os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', + + '`os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', location: '`contextSelectors[].placement` (which `location` aliased) was removed in ' + '@objectstack/spec 17.0.0 — selectors always render in the sidebar header. ' @@ -1369,7 +1369,7 @@ const HOME_PAGE_ID_RETIRED = + '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.'; + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** * The prescription for every author-shaped spelling of the ADR-0045 publish @@ -1461,7 +1461,7 @@ export const AppSchema = lazySchema(() => strictObject( '`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.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** Description */ @@ -1603,7 +1603,7 @@ export const AppSchema = lazySchema(() => strictObject( '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.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), apis: retiredKey( '`App.apis` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — ' + @@ -1625,7 +1625,7 @@ export const AppSchema = lazySchema(() => strictObject( 'register the route on `kernel:ready` (NOT the manifest `contributes.routes` key — ' + 'removed in @objectstack/spec 17: nothing ever read it, and authoring it is ' + 'now rejected with its own prescription). ' + - 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** @@ -1640,13 +1640,13 @@ export const AppSchema = lazySchema(() => strictObject( '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.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), embed: retiredKey( '`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.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** @@ -1659,7 +1659,7 @@ export const AppSchema = lazySchema(() => strictObject( '`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.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** @@ -1708,7 +1708,7 @@ export const AppSchema = lazySchema(() => strictObject( '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.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** diff --git a/packages/spec/src/ui/aria-carrier-tombstones.test.ts b/packages/spec/src/ui/aria-carrier-tombstones.test.ts index eacd69e872d..60823c620dd 100644 --- a/packages/spec/src/ui/aria-carrier-tombstones.test.ts +++ b/packages/spec/src/ui/aria-carrier-tombstones.test.ts @@ -110,7 +110,7 @@ describe('the `aria` tombstones name only live `AriaProps` carriers', () => { // site that already flipped once. expect(message).toContain('Delete the key.'); expect(message).toContain('os migrate meta --from 16'); - expect(message).toMatch(/to list the mechanical edits for existing sources; apply them by hand\.$/); + expect(message).toMatch(/to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand\.$/); }); it('the App.aria tombstone points at a page component, not at the retired widget surface', () => { @@ -220,7 +220,7 @@ describe('the `aria` tombstones name only live `AriaProps` carriers', () => { // None of the above bought by weakening the prescription itself. expect(message).toContain('Delete the key.'); expect(message).toContain('os migrate meta --from 17'); - expect(message).toMatch(/to list the mechanical edits for existing sources; apply them by hand\.$/); + expect(message).toMatch(/to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand\.$/); }); it('the tombstone rides the `.extend()` onto ReportChart', () => { @@ -294,7 +294,7 @@ describe('the `aria` tombstones name only live `AriaProps` carriers', () => { // None of the above bought by weakening the prescription itself. expect(message).toContain('Delete the key.'); expect(message).toContain('os migrate meta --from 17'); - expect(message).toMatch(/to list the mechanical edits for existing sources; apply them by hand\.$/); + expect(message).toMatch(/to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand\.$/); }); it('the action tombstone refuses on the object-nested coordinate too', () => { diff --git a/packages/spec/src/ui/chart.zod.ts b/packages/spec/src/ui/chart.zod.ts index 3c319ca893b..200e9b99a19 100644 --- a/packages/spec/src/ui/chart.zod.ts +++ b/packages/spec/src/ui/chart.zod.ts @@ -751,7 +751,7 @@ export const ChartConfigSchema = lazySchema(() => strictObject( 'renderer lowers onto the chart graphic as `role="img"` plus `aria-label`. The shared ' + '`AriaProps` shape is NOT gone — `ariaLabel` / `ariaDescribedBy` / `role` stay live in ' + 'the `aria` block on `page.aria`, `page.components[].aria` and the list view `aria`. ' + - 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), }, )); diff --git a/packages/spec/src/ui/component.test.ts b/packages/spec/src/ui/component.test.ts index bd6762ed1bb..776b6d64bb5 100644 --- a/packages/spec/src/ui/component.test.ts +++ b/packages/spec/src/ui/component.test.ts @@ -1841,7 +1841,7 @@ describe('ElementTextPropsSchema', () => { * for) and the house `os migrate meta` sentence. */ const MIGRATE_SENTENCE = - 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; it.each([ ['heading', 'h2'], @@ -2127,7 +2127,7 @@ describe('Interactive Elements — element:filter (retired, no renderer)', () => // sentence (the D2 conversion `element-filter-removed` strips it). it('rejects the retired `targetVariable` with its prescription', () => { expect(() => ElementFilterPropsSchema.parse({ targetVariable: 'active_filter' })) - .toThrow(/`element:filter` property `targetVariable`.*removed.*Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand/s); + .toThrow(/`element:filter` property `targetVariable`.*removed.*Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand/s); }); // The migrated shape — `element-filter-removed` strips all six keys and @@ -2180,7 +2180,7 @@ describe('Interactive Elements — element:form (retired, no renderer)', () => { expect(() => ElementFormPropsSchema.parse({ onSubmit: 'navigate_to("page_detail")' })) .toThrow(/`element:form` property `onSubmit`.*removed/s); expect(() => ElementFormPropsSchema.parse({ aria: { label: 'Form' } })) - .toThrow(/`element:form` property `aria`.*removed.*Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand/s); + .toThrow(/`element:form` property `aria`.*removed.*Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand/s); }); // The migrated shape — `element-form-removed` strips all six keys and diff --git a/packages/spec/src/ui/component.zod.ts b/packages/spec/src/ui/component.zod.ts index 3465a94665f..0e8ed804c3d 100644 --- a/packages/spec/src/ui/component.zod.ts +++ b/packages/spec/src/ui/component.zod.ts @@ -628,7 +628,7 @@ export const PageHeaderProps = strictObject({ + 'input, so an authored value was accepted and dropped. Delete the key. The header\'s own ' + 'identity is drawn by the record chrome (`recordChrome`, on by default) and each action ' + 'carries its own `icon`. ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** * REMOVED (#20758, ADR-0049 enforce-or-remove through the ADR-0087 D2 route, @@ -652,7 +652,7 @@ export const PageHeaderProps = strictObject({ + 'no renderer ever drew a trail for it: objectui drew an empty slot and nothing filled it, ' + 'and the navigation trail is drawn once, by the app shell\'s header. Delete the key, whether ' + 'it was `true` or `false`; the shell\'s trail is unchanged. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), actions: z.array(z.string()).optional().describe('Action IDs to show in header'), /** @@ -761,7 +761,7 @@ export const PageTabsProps = strictObject({ + 'a props key named `type` collides with the page component\'s own dispatch key, so it is ' + 'unauthorable in the flat and JSX carriers and was never validated in them. Rename the key ' + 'to `tabStyle`; the value (`line` | `card` | `pill`) is unchanged. ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), position: z.enum(['top', 'left']).default('top'), /** @@ -1000,7 +1000,7 @@ export const PageCardProps = strictObject({ + 'never published it as an input, so an authored value was accepted and dropped. Delete the ' + 'key and author the buttons as components in the card\'s `children` or `footer` ' + '(`element:button`, `record:quick_actions`), which is what actually renders. ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** * Card content, in order — the canonical composition slot, matching every @@ -1028,7 +1028,7 @@ export const PageCardProps = strictObject({ + 'it was a second spelling of the composition slot every other container calls `children`, ' + 'and the renderer reads both. Rename the key to `children`; the value (an array of child ' + 'components) is unchanged. ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', )), /** * Slot for footer content — a declared, rendered slot distinct from @@ -1194,7 +1194,7 @@ export const RecordDetailsProps = strictObject({ + 'values took the same branch and the key selected nothing. Delete the key — the body is ' + 'already chosen by what you author: `sections` renders the explicit groups (the old ' + '`custom`), and omitting it falls back to the object\'s `highlightFields` (the old `auto`). ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** * Field groups rendered as the detail body, IN ORDER. @@ -1643,7 +1643,7 @@ export const RecordHighlightsField = z.union([ + 'the field list as plain strings), so an authored value was accepted and drawn by ' + 'nothing. Delete the key — no replacement: the renderer never drew it, and the chip ' + 'renders label and value only. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', }, }, { name: z.string().describe('Field name on the record'), @@ -1778,7 +1778,9 @@ export const RecordActivityProps = strictObject({ aria: AriaPropsSchema.optional().describe('ARIA accessibility attributes'), }); -// `position` old-spelling prescriptions (#8762). Declared with `//` on purpose — +// `position` old-spelling prescriptions (#8762). Each names `--from 17`: the +// conversion is registered `toMajor: 18`, and a bare `os migrate meta` is +// refused for a missing `--from`. Declared with `//` on purpose — // the `LIST_VIEW_EXPORT_PDF_RETIRED` placement note applies here too: build-docs // takes a file's first JSDoc per exported symbol, and these need no doc page. // This is an enum-VALUE narrowing, so there is no `retiredKey()` tombstone to @@ -1790,18 +1792,18 @@ const CHATTER_POSITION_RETIRED: ReadonlyMap = new Map([ + 'no renderer branch ever compared the old vocabulary: `RecordChatterPanel` docks on ' + "'right'/'left' and renders in flow on 'bottom', so a spec-valid 'sidebar' silently fell " + "through to the in-flow render. Write 'right' — the docked side panel 'sidebar' meant. " - + 'Run `os migrate meta` to list the mechanical edits for existing sources ' - + '(registered under protocol major 18); apply them by hand.'], + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; ' + + '`--write` applies the ones it can prove, and you apply the rest by hand.'], ['inline', "'inline' was removed from `record:chatter` / `record:discussion` `position` — " + 'no renderer branch ever compared the old vocabulary. Write \'bottom\' — the renderer\'s ' - + "in-flow branch, which is where 'inline' already rendered. Run `os migrate meta` to " - + 'list the mechanical edits for existing sources (registered under protocol major 18); ' - + 'apply them by hand.'], + + "in-flow branch, which is where 'inline' already rendered. Run `os migrate meta --from 17` to " + + 'list the mechanical edits for existing sources; ' + + '`--write` applies the ones it can prove, and you apply the rest by hand.'], ['drawer', "'drawer' was removed from `record:chatter` / `record:discussion` `position` " + 'with no successor: no renderer branch ever implemented an overlay drawer — the value fell ' + "through to the in-flow render. Write 'right' — the docked side panel is the nearest " - + 'surviving shape of a side drawer. Run `os migrate meta` to list the mechanical edits ' - + 'for existing sources (registered under protocol major 18); apply them by hand.'], + + 'surviving shape of a side drawer. Run `os migrate meta --from 17` to list the mechanical edits ' + + 'for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'], ]); /** @@ -2507,12 +2509,12 @@ const ELEMENT_TEXT_VARIANT_RETIRED = { '`heading` was removed from `element:text` `variant` (`ElementTextPropsSchema.variant`) in ' + `@objectstack/spec 17.7.0 — ${ELEMENT_TEXT_VARIANT_VOCABULARY} Write \`h2\` — the heading element ` + '`heading` always rendered, now drawn in the `h2` style — or the level the page outline means. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', subheading: '`subheading` was removed from `element:text` `variant` (`ElementTextPropsSchema.variant`) in ' + `@objectstack/spec 17.7.0 — ${ELEMENT_TEXT_VARIANT_VOCABULARY} Write \`h3\` — the heading element ` + '`subheading` always rendered, now drawn in the `h3` style — or the level the page outline means. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', } as const; export const ElementTextPropsSchema = lazySchema(() => strictObject({ @@ -2861,7 +2863,7 @@ const elementFilterRetired = (key: string): string => + 'no-renderer exclusion), so every key on this element was a capability claim nothing ' + 'kept. Delete the `element:filter` component; list surfaces own their filtering — use a ' + "view's `userFilters` quick-filter bar or the list toolbar's filter builder. " - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** * RETIRED at element grain (#9220, ADR-0049 enforce-or-remove). `element:filter` @@ -2912,7 +2914,7 @@ const elementFormRetired = (key: string): string => + 'and use the object-bound `object-form` block instead — it is rendered, ' + 'designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, ' + '`submitText`). ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** * RETIRED at element grain (#9249, ADR-0049 enforce-or-remove). `element:form` @@ -3095,7 +3097,7 @@ export const ElementRecordPickerPropsSchema = lazySchema(() => strictObject({ + "component's `id`, so authoring only `targetVariable` bound nothing while reporting " + 'success. Delete the key; to bind the picked record id, declare it on the variable — ' + "`variables: [{ name: '', type: 'record_id', source: '' }]`. " - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), placeholder: I18nLabelSchema.optional().describe('Placeholder text'), /** Shown in place of the row list when the query returns nothing. */ @@ -3109,7 +3111,7 @@ export const ElementRecordPickerPropsSchema = lazySchema(() => strictObject({ + '(ADR-0087 D2) — it was a required declaration no renderer ever read, while the ' + 'renderer honoured `labelField` for the same thing and defaulted to `name`. Rename the key ' + 'to `labelField`; the value (a field name) is unchanged. ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** * REMOVED (#5775). ADR-0049 enforce-or-remove: the control has no search @@ -3121,7 +3123,7 @@ export const ElementRecordPickerPropsSchema = lazySchema(() => strictObject({ + 'renderer ever read it and it narrowed nothing. Delete the key. To restrict which records ' + 'the picker offers, use `filter` (or the component-level `dataSource.filter`), which the ' + 'query path does apply. ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** * REMOVED (#5775). ADR-0049 enforce-or-remove: the control is a single-select @@ -3132,7 +3134,7 @@ export const ElementRecordPickerPropsSchema = lazySchema(() => strictObject({ + '(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.', + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** ARIA accessibility */ aria: AriaPropsSchema.optional().describe('ARIA accessibility attributes'), @@ -3188,7 +3190,7 @@ export const ElementTextInputPropsSchema = lazySchema(() => strictObject({ + "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.', + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** ARIA accessibility */ aria: AriaPropsSchema.optional().describe('ARIA accessibility attributes'), @@ -4632,7 +4634,7 @@ export const ObjectGridPropsSchema = lazySchema(() => strictObject({ + '`sort` was absent, so one intent had two spellings and a grid authoring both silently ignored ' + 'this one. Rename the key to `sort` and wrap the value in an array (`defaultSort: { field, order }` ' + 'becomes `sort: [{ field, order }]`); the pair itself is unchanged. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** * Pagination config — the two members whose value is a PAGE SIZE bounded to @@ -4874,7 +4876,7 @@ export const ObjectGridPropsSchema = lazySchema(() => strictObject({ + 'it was the legacy second spelling of `resizable`, read only when `resizable` was absent, so one ' + 'switch had two spellings and a grid authoring both silently ignored this one. Use `resizable`. ' + 'Rename the key; the value (a boolean) is unchanged. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), reorderableColumns: z.boolean().optional().describe('Allow column drag-reorder'), frozenColumns: z.number().optional().describe('How many leading columns stay frozen (default 1)'), @@ -5959,7 +5961,7 @@ export const ObjectKanbanPropsSchema = lazySchema(() => strictObject({ + '`onQuickAdd`, and `onQuickAdd` is a host-supplied function JSON cannot carry and no ' + 'producer ever put on an `object-kanban` node, so authoring it was a parse-clean no-op. ' + 'Delete the key; `object-kanban` offers no quick-add control. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), coverImageField: z.string().optional().describe('Image field rendered as the card cover'), /** @@ -6350,13 +6352,13 @@ const OBJECT_FORM_LAYOUT_RETIRED: ReadonlyMap = new Map([ + "`object-form` presentation folds it to 'vertical'. Write 'vertical', or omit `layout` " + "('vertical' is the renderer default); for a multi-column form set `columns` (e.g. " + '`columns: 2`), which the renderer honours under either layout. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'], + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'], ['inline', "'inline' was removed from the `object-form` `layout` enum in @objectstack/spec 17.5.0 " + '(ADR-0049 enforce-or-remove) — no renderer ever gave it a behaviour of its own: every ' + "`object-form` presentation folds it to 'vertical', and a row of inline inputs is a " + "toolbar / filter-row pattern, not a record-form layout. Write 'vertical', or omit " + "`layout` ('vertical' is the renderer default). " - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'], + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'], ]); /** @@ -7279,7 +7281,7 @@ const MASTER_DETAIL_DETAIL_SORT_FIELD_RETIRED = + 'of those names — except on an entry that names `relationshipField` and at least one column and ' + 'gives every column a `type`, which the renderer keeps exactly as authored and stamps no line ' + 'position on. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; function masterDetailDetailEntry() { return strictObject({ diff --git a/packages/spec/src/ui/dashboard.zod.ts b/packages/spec/src/ui/dashboard.zod.ts index 9a8c2284f0c..0b9247ea909 100644 --- a/packages/spec/src/ui/dashboard.zod.ts +++ b/packages/spec/src/ui/dashboard.zod.ts @@ -346,8 +346,8 @@ const COMPARE_TO_OFFSET_RETIRED = + "(`'7d'`, `'1M'`, …) there is no faithful one-key rewrite: state the window you want on the " + "widget's own `filter` and compare it with `previousPeriod`, which shifts by whatever length " + 'that window resolves to. ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for the `1y` case; the other durations ' - + 'are reported for you to re-state.'; + + 'Run `os migrate meta --from 16` to list the mechanical edits for the `1y` case; `--write` applies the ones it can prove, ' + + 'and the other durations are reported for you to re-state.'; // The two string arms. They parsed, and on a dataset widget they then did // NOTHING — DatasetWidget dropped them deliberately, because forwarding one made @@ -361,7 +361,7 @@ const COMPARE_TO_STRING_RETIRED = (kind: 'previousPeriod' | 'previousYear') => + 'analytics executor actually reads it (`DatasetSelection.compareTo`). Add `dimension` only ' + 'when the selection has more than one dated time dimension; with one, the executor resolves ' + 'it. ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; // ── Per-widget action button prescriptions (#5010) ─────────────────────────── // @@ -384,7 +384,7 @@ const WIDGET_ACTION_RETIRED = (key: 'actionUrl' | 'actionType' | 'actionIcon') = + '(`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.'; + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** * The one widget `type` that reads `options.stageOrder`. @@ -970,7 +970,7 @@ const WIDGET_CHART_STRUCTURE_RETIRED = (key: string, carried: string, instead: s + ' The key is NOT gone from the chart config itself: it stays authorable on the react ' + '`` tier, where the chart is bound to inline `data` and there is no dataset ' + 'to derive it from. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** * `dashboard.widgets[].chartConfig` — the chart config as a DATASET-BOUND @@ -1402,7 +1402,7 @@ export const DashboardWidgetSchema = lazySchema(() => strictObject({ '`ResponsiveConfig` shape; that key was measured equally unread and removed with the shape ' + 'with it. 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.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), // `aria` REMOVED (#5010, ADR-0049 D2): the same "false compliance" the @@ -1430,7 +1430,7 @@ export const DashboardWidgetSchema = lazySchema(() => strictObject({ '`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.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), // ADR-0021 single-form: every widget binds a `dataset` and selects `values` // (both required above) — there is no inline-query shape to disambiguate. @@ -1775,7 +1775,7 @@ export const DashboardSchema = lazySchema(() => strictObject({ '`dashboard.refreshInterval` was renamed to `refreshIntervalSeconds` in @objectstack/spec 17 ' + '— the unit of a duration-shaped number lives in the key name, not only in the describe ' + 'prose. Rename the key to `refreshIntervalSeconds`; the value (seconds) is unchanged. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** Dashboard Date Range (Global time filter) */ @@ -1799,13 +1799,13 @@ export const DashboardSchema = lazySchema(() => strictObject({ '`dashboard.aria` was removed in @objectstack/spec 17.0.0 (audit close-out) — no ' + 'dashboard renderer ever applied it, so declared ARIA attributes silently did not reach ' + 'the DOM. Delete the key. ' + - 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), performance: retiredKey( '`dashboard.performance` was removed in @objectstack/spec 17.0.0 (audit ' + 'close-out) — no renderer or runtime read it; dashboard 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.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** * ADR-0010 §3.7 — Package-level protection envelope. Package diff --git a/packages/spec/src/ui/form-field-public-picker-retirement.test.ts b/packages/spec/src/ui/form-field-public-picker-retirement.test.ts index 640b06c6a5c..e4bb7fb1f11 100644 --- a/packages/spec/src/ui/form-field-public-picker-retirement.test.ts +++ b/packages/spec/src/ui/form-field-public-picker-retirement.test.ts @@ -55,7 +55,7 @@ const PICKER = { displayFields: ['name'], maxResults: 10, object: 'crm_contact' // Unanchored, because a thrown `ZodError`'s message is the JSON of its issues; // the key-first house convention is asserted on the issue message itself below. const PRESCRIPTION = - /`view\.form\.sections\[\]\.fields\[\]\.publicPicker` was removed in @objectstack\/spec 17\.6\.0 \(ADR-0087 D2\).*no longer offers record search.*Delete the key.*`select` field with static `options`.*behind sign-in.*Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand\./s; + /`view\.form\.sections\[\]\.fields\[\]\.publicPicker` was removed in @objectstack\/spec 17\.6\.0 \(ADR-0087 D2\).*no longer offers record search.*Delete the key.*`select` field with static `options`.*behind sign-in.*Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand\./s; /** A ViewItem-branch form carrying the given field entries (the `saveMetaItem` door). */ const viewItem = (fields: unknown[]) => ({ diff --git a/packages/spec/src/ui/form-select-option.test.ts b/packages/spec/src/ui/form-select-option.test.ts index 932e0ff7ea7..071987c6b4a 100644 --- a/packages/spec/src/ui/form-select-option.test.ts +++ b/packages/spec/src/ui/form-select-option.test.ts @@ -66,7 +66,7 @@ describe('form-view options refuse the per-option `default` key', () => { expect(m).toContain('`default: true`'); expect(m).toMatch(/`defaultValue` winning when both are declared/); // House migrate sentence (route D wording — a property of the tool). - expect(m).toContain('Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'); + expect(m).toContain('Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'); // No rename suggestion toward a key this shape refuses. expect(m).not.toContain('Did you mean'); }); diff --git a/packages/spec/src/ui/list-view-export-options.ts b/packages/spec/src/ui/list-view-export-options.ts index 044319e8dd0..20c07f58707 100644 --- a/packages/spec/src/ui/list-view-export-options.ts +++ b/packages/spec/src/ui/list-view-export-options.ts @@ -39,7 +39,7 @@ export const LIST_VIEW_EXPORT_PDF_RETIRED = + 'export: ObjectGrid dropped the declared format from the export menu with only a runtime ' + "console.warn, so authoring it was a parse-clean no-op. Delete the value; the surviving " + "formats are 'csv', 'xlsx' and 'json'. " - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** * Export formats the platform actually delivers (#8010): `csv`/`json` on both diff --git a/packages/spec/src/ui/master-detail-detail-sort-field-retirement.test.ts b/packages/spec/src/ui/master-detail-detail-sort-field-retirement.test.ts index 6e4be9eb57b..b55f882800a 100644 --- a/packages/spec/src/ui/master-detail-detail-sort-field-retirement.test.ts +++ b/packages/spec/src/ui/master-detail-detail-sort-field-retirement.test.ts @@ -59,7 +59,7 @@ const REGISTERED_KEY = 'ui/ObjectMasterDetailFormProps:details.sortField'; // Unanchored, because a thrown `ZodError`'s message is the JSON of its issues; // the key-first house convention is asserted on the issue message itself. const PRESCRIPTION = - /`object-master-detail-form` property `details\[\]\.sortField` was removed in @objectstack\/spec 17 \(ADR-0087 D2\) — the console reads no authored value.*Delete the key\..*Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand\./s; + /`object-master-detail-form` property `details\[\]\.sortField` was removed in @objectstack\/spec 17 \(ADR-0087 D2\) — the console reads no authored value.*Delete the key\..*Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand\./s; /** A detail entry's live keys — the showcase project workspace's entry shape. */ const ENTRY = { title: 'Lines', childObject: 'crm_invoice_line', addLabel: 'Add line' } as const; diff --git a/packages/spec/src/ui/page-header-breadcrumb-retirement.test.ts b/packages/spec/src/ui/page-header-breadcrumb-retirement.test.ts index d6f640c172a..37fe2b3f473 100644 --- a/packages/spec/src/ui/page-header-breadcrumb-retirement.test.ts +++ b/packages/spec/src/ui/page-header-breadcrumb-retirement.test.ts @@ -54,7 +54,7 @@ const REGISTERED_KEY = 'ui/PageHeaderProps:breadcrumb'; // Unanchored, because a thrown `ZodError`'s message is the JSON of its issues; // the key-first house convention is asserted on the issue message itself. const PRESCRIPTION = - /`page:header` property `breadcrumb` was removed in @objectstack\/spec 17 \(ADR-0087 D2\) — no renderer ever drew a trail for it.*Delete the key, whether it was `true` or `false`.*Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand\./s; + /`page:header` property `breadcrumb` was removed in @objectstack\/spec 17 \(ADR-0087 D2\) — no renderer ever drew a trail for it.*Delete the key, whether it was `true` or `false`.*Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand\./s; /** A page header's live keys — what an author commonly writes, not the retired one. */ const HEADER = { title: 'Lead', subtitle: '{company}', actions: ['convert_lead'] } as const; diff --git a/packages/spec/src/ui/page.zod.ts b/packages/spec/src/ui/page.zod.ts index a4ac77be3a8..df6a66792d1 100644 --- a/packages/spec/src/ui/page.zod.ts +++ b/packages/spec/src/ui/page.zod.ts @@ -117,7 +117,7 @@ export const RETIRED_PAGE_COMPONENT_TYPES: ReadonlyMap = new Map + 'no-renderer exclusion), so every key on this element was a capability claim nothing ' + 'kept. Delete the `element:filter` component; list surfaces own their filtering — use a ' + "view's `userFilters` quick-filter bar or the list toolbar's filter builder. " - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'], + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'], // #21504, ADR-0049 enforce-or-remove (triage ruling 5963897014: retire, // refused by name — the `user:profile` precedent above). Zero producers // measured in objectstack, cloud and hotcrm, and objectui registers no @@ -149,7 +149,7 @@ export const RETIRED_PAGE_COMPONENT_TYPES: ReadonlyMap = new Map + 'and use the object-bound `object-form` block instead — it is rendered, ' + 'designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, ' + '`submitText`). ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'], + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'], ]); /** @@ -450,7 +450,7 @@ export const PageComponentSchema = lazySchema(() => strictObject({ '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.', + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** ARIA accessibility attributes */ @@ -730,7 +730,7 @@ export function checkPageRequiresKind( + ': it exists only on the kinds whose source the platform compiles at save, `html` and its ' + 'deprecated alias `jsx`, where it is derived from the source and stored. On a ' + `\`${kind}\` page nothing derives it and nothing enforces it. Delete the key. ` - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', }); } @@ -772,7 +772,7 @@ const PAGE_ASSIGNED_PROFILES_RETIRED = + "audience is the permission set's: gate the DATA the page shows with the object's permission " + 'sets, and bind those sets to people through positions (`sys_position_permission_set`) — ' + 'those are the checks the runtime actually runs. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** * The wrong-layer pointer `profiles` / `assignedTo` now carry. Deliberately the diff --git a/packages/spec/src/ui/report.test.ts b/packages/spec/src/ui/report.test.ts index 7267ed356f2..774a06ba4b7 100644 --- a/packages/spec/src/ui/report.test.ts +++ b/packages/spec/src/ui/report.test.ts @@ -236,7 +236,7 @@ describe('A joined report draws no chart — block `chart` removed, container `c const CHART = { type: 'bar', xAxis: 'status', yAxis: 'task_count' } as const; const BLOCK_FIRST_SENTENCE = '`report.blocks[].chart` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove)'; const CONTAINER_FIRST_SENTENCE = 'a `joined` report draws no chart — it draws each block as a table and never reads `chart`, on the container or on a block.'; - const MIGRATE = 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + const MIGRATE = 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; const issuesOf = (r: { success: boolean; error?: { issues: ReadonlyArray<{ code: string; path: PropertyKey[]; message: string }> } }) => (r.error?.issues ?? []).map((i) => ({ code: i.code, path: i.path, message: i.message })); diff --git a/packages/spec/src/ui/report.zod.ts b/packages/spec/src/ui/report.zod.ts index fadd5361652..78dad298cd6 100644 --- a/packages/spec/src/ui/report.zod.ts +++ b/packages/spec/src/ui/report.zod.ts @@ -173,13 +173,13 @@ const JOINED_BLOCK_CHART_RETIRED = + 'block `chart`, so the chart parsed and nothing was plotted. Delete the key. A chart is drawn ' + 'from a non-joined report\'s own top-level `chart`: to plot one of these slices, give it a ' + 'report of its own with that `chart`. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; const JOINED_CONTAINER_CHART_REFUSED = 'a `joined` report draws no chart — it draws each block as a table and never reads `chart`, ' + 'on the container or on a block. Delete `chart`; to plot one of these slices, give it a ' + 'non-joined report of its own with that `chart`. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** * The refusal a `joined` report's block with no `dataset` earns, at diff --git a/packages/spec/src/ui/view-list-tabs-retirement.test.ts b/packages/spec/src/ui/view-list-tabs-retirement.test.ts index a2507b20a5e..dfc0b4a8694 100644 --- a/packages/spec/src/ui/view-list-tabs-retirement.test.ts +++ b/packages/spec/src/ui/view-list-tabs-retirement.test.ts @@ -138,7 +138,7 @@ describe('list-view tabs retirement — the tombstone, at every list-view door', const r = ListViewSchema.safeParse({ ...LIST, tabs: TABS }); const message = (r.error?.issues ?? []).map((i) => i.message).join('\n'); expect(message).toContain("the tab's `name` becomes the entry's key"); - expect(message).toContain('Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'); + expect(message).toContain('Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'); expect(message).not.toMatch(/#\d/); }); diff --git a/packages/spec/src/ui/view-union-retirement-prescription.test.ts b/packages/spec/src/ui/view-union-retirement-prescription.test.ts index 2b9730cc03b..71125883fa6 100644 --- a/packages/spec/src/ui/view-union-retirement-prescription.test.ts +++ b/packages/spec/src/ui/view-union-retirement-prescription.test.ts @@ -136,7 +136,7 @@ describe('§1 reach — every branch of the view union surfaces its own prescrip // its `os migrate meta` sentence is pinned class-wide by // `../shared/retired-key-migrate-sentence.test.ts`. A message this code // composed would be a second spelling of a pinned string. - expect(message).toContain('to list the mechanical edits for existing sources; apply them by hand.'); + expect(message).toContain('to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'); }); it('the lifted string is byte-identical to the nested issue it came from', () => { diff --git a/packages/spec/src/ui/view.test.ts b/packages/spec/src/ui/view.test.ts index 79049436bda..0ceb89ed0f1 100644 --- a/packages/spec/src/ui/view.test.ts +++ b/packages/spec/src/ui/view.test.ts @@ -3867,7 +3867,7 @@ describe('ListViewSchema — retired striped/bordered/virtualScroll (every reade ListViewSchema.parse({ type: 'grid', columns: ['name'], [key]: true }); } catch (e) { message = String((e as Error).message); } expect(message).toMatch(new RegExp(`view\\.${key}\` was removed`)); - expect(message).toMatch(/Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand\./); + expect(message).toMatch(/Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand\./); } }); it('accepts the live grid siblings byte-identically (rowHeight/selection/pagination/resizable)', () => { @@ -3937,7 +3937,7 @@ describe('ListViewSchema.exportOptions — object form + array lift + pdf retire expect(message).toMatch(/PDF export itself was declined as NOT PLANNED/); expect(message).not.toMatch(/#\d{3,5}\b/); expect(message).toMatch(/'csv', 'xlsx' and 'json'/); - expect(message).toMatch(/Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand\./); + expect(message).toMatch(/Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand\./); }); it("REJECTS 'pdf' in the object form's `formats` with the same prescription", () => { @@ -4449,7 +4449,7 @@ describe("ListViewSchema — the RETIRED `page` view type", () => { }; for (const body of [{ type: 'page', columns: [] }, { type: 'grid', pageName: 'p', columns: [] }]) { expect(collect(body)).toContain( - 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ); } }); diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index 484d2787aae..e60abeaa988 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -2408,7 +2408,7 @@ const LIST_VIEW_TYPE_PAGE_RETIRED = + 'that draws rows (`grid` and its siblings); to put a published page in front of users, give the ' + "app a navigation item instead — `{ type: 'page', pageName: '' }` under the app's " + '`navigation`, which is the page mount that has always rendered. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; const LIST_VIEW_PAGE_NAME_RETIRED = '`view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named ' @@ -2417,7 +2417,7 @@ const LIST_VIEW_PAGE_NAME_RETIRED = + "to put a published page in front of users, give the app a navigation item — `{ type: 'page', " + "pageName: '' }` under the app's `navigation` — which is a different key on a " + 'different surface and is the page mount that has always rendered. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** * [#17053] Prescription for the retired bare-string `sort` clause on the @@ -2457,7 +2457,7 @@ const LIST_VIEW_SORT_STRING_RETIRED = + '`sort: [{ field: \'created_at\', order: \'asc\' }]` — `order` is required on the entry and ' + 'is written out rather than omitted; a comma-separated clause becomes one array entry per ' + 'key, in the same order. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; const VIEW_CALENDAR_ALLOWED_NEEDS_START_DATE = "`appearance.allowedVisualizations` includes 'calendar', so end users can switch this view to a " @@ -2920,7 +2920,7 @@ const ListViewShapeSchema = lazySchema(() => strictObject({ + "the entry's `label`, and its `filter` rules join the view's own `filter` on that entry (copy the " + "view's `columns` too); a tab whose `view` already named a list view needs nothing more. Every " + '`listViews` entry renders as a tab in the switcher. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** Add Record (Airtable Interface parity) */ @@ -2947,13 +2947,13 @@ const ListViewShapeSchema = lazySchema(() => strictObject({ responsive: retiredKey( '`view.responsive` was removed in @objectstack/spec 17.0.0 (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.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), performance: retiredKey( '`view.performance` was removed in @objectstack/spec 17.0.0 (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.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), // `striped` / `bordered` / `virtualScroll` REMOVED (#7176, ADR-0049 @@ -2967,19 +2967,19 @@ const ListViewShapeSchema = lazySchema(() => strictObject({ '`view.striped` was removed in @objectstack/spec 17.0.0 (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.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), bordered: retiredKey( '`view.bordered` was removed in @objectstack/spec 17.0.0 (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.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), virtualScroll: retiredKey( '`view.virtualScroll` was removed in @objectstack/spec 17.0.0 (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.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), })); @@ -3059,7 +3059,7 @@ export const FormSelectOptionSchema = lazySchema(() => { + 'instead — field-level `defaultValue`, or `default: true` on that field\'s own ' + '`options` entry (both enforced there, with `defaultValue` winning when both are ' + 'declared). Run `os migrate meta --from 17` to list the mechanical edits for existing ' - + 'sources; apply them by hand.', + + 'sources; `--write` applies the ones it can prove, and you apply the rest by hand.', isDefault: '`isDefault` is an object-field spelling: an OBJECT field\'s option list answers it with ' + 'a rename to `default`, which is enforced there. Form-view options accept neither ' @@ -3162,7 +3162,7 @@ const FORM_FIELD_PUBLIC_PICKER_RETIRED = + '(`GET /forms/:slug/lookup/:field`) no longer exists. Delete the key (the whole `publicPicker` ' + 'block). To let a visitor choose from a fixed list, use a `select` field with static `options`; ' + 'to let them pick an existing record, put the form behind sign-in. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; const FormFieldBaseSchema = lazySchema(() => { const shape = { @@ -4181,13 +4181,13 @@ const FORM_VIEW_LAYOUT_RETIRED: ReadonlyMap = new Map([ + "form presentation folds it to 'vertical'. Write 'vertical', or omit `layout` ('vertical' " + 'is the renderer default); for a multi-column form set `columns` (e.g. `columns: 2`), which ' + 'the renderer honours under either layout. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'], + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'], ['inline', "'inline' was removed from the form view `layout` enum in @objectstack/spec 17.5.0 " + '(ADR-0049 enforce-or-remove) — no renderer ever gave it a behaviour of its own: every ' + "form presentation folds it to 'vertical', and a row of inline inputs is a toolbar / " + "filter-row pattern, not a record-form layout. Write 'vertical', or omit `layout` " + "('vertical' is the renderer default). " - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'], + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'], ]); /** @@ -4379,7 +4379,7 @@ export const FormViewSchema = lazySchema(() => strictObject({ '`form.defaultSort` was removed in @objectstack/spec 17.0.0 (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.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), /** Public form sharing configuration */ @@ -4519,7 +4519,7 @@ export const FormViewSchema = lazySchema(() => strictObject({ '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.', + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.', ), }).superRefine((view, ctx) => { // `section.pane` is split-only vocabulary. On any other form type it would @@ -4978,14 +4978,14 @@ const VIEW_ITEM_OWNER_RETIRED = + 'view marked as one user\'s was listed for every user who can read the object. Delete the key. ' + 'Nothing restricts a view item to one user today — per-user view scoping is a parked direction ' + '(ADR-0017), not a shipped mechanism — so a view item is visible to everyone who can read its object. ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; const VIEW_ITEM_HIDDEN_RETIRED = '`view.hidden` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it promised to ' + 'hide a view item from the switcher, and nothing ever read it: `GET /meta/view?object=` and the ' + 'console\'s view switcher list every item bound to the object, `hidden: true` included. Delete the key; ' + 'to take a view out of the switcher, delete the view item itself (or stop shipping it from source). ' - + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.'; /** * Fields shared by every independent view item, regardless of kind. Returned