diff --git a/.changeset/migrate-meta-sentence-house-form.md b/.changeset/migrate-meta-sentence-house-form.md new file mode 100644 index 0000000000..c38e4bf52e --- /dev/null +++ b/.changeset/migrate-meta-sentence-house-form.md @@ -0,0 +1,5 @@ +--- +'@objectstack/spec': patch +--- + +One house sentence for every `os migrate meta` prescription (#6856, maintainer-ruled route D). The 41 tombstone prescriptions that closed with `to rewrite it automatically.` / `to remove it.` / `to remove it automatically.` / `rewrites it for you.` now all close with exactly `Run 'os migrate meta --from 16' to rewrite existing sources automatically.` — the sentence states a property of the tool (it rewrites your source files), never the fate of the key, whose disposition stays in the body prose ("Delete the key…", "Rename the key to…"). The two partial conversions keep the precise two-clause form naming which part of the value converts (`dashboard.widgets[].compareTo.offset`, script `config.actionType`). The `agent.tools` prescription additionally states that the migration DELETES the key — not a rename — and each listed tool must be re-declared in a skill by hand. The rule is recorded in `shared/retired-key.ts`'s docblock and pinned class-wide by `retired-key-migrate-sentence.test.ts`. Acceptance is byte-identical everywhere: `retiredKey()` rejects with the guidance string regardless of its wording. diff --git a/content/docs/references/ai/agent.mdx b/content/docs/references/ai/agent.mdx index 7feb552bfd..06b62379dc 100644 --- a/content/docs/references/ai/agent.mdx +++ b/content/docs/references/ai/agent.mdx @@ -53,7 +53,7 @@ const result = AIModelConfigSchema.parse(data); | **lifecycle** | `{ id: string; description?: string; contextSchema?: Record; initial: string; … }` | optional | [EXPERIMENTAL — not enforced] State machine defining the agent conversation flow and constraints. Parsed but no runtime consumer yet (liveness #1878/#1893). | | **surface** | `Enum<'ask' \| 'build'>` | ✅ | 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 (#3894) — 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. Run `os migrate meta --from 16` to rewrite it automatically. | +| **tools** | `never` | optional | [REMOVED] `agent.tools` was removed in @objectstack/spec 17 (#3894) — 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. Run `os migrate meta --from 16` to rewrite existing sources automatically. | | **knowledge** | `never` | optional | [REMOVED] `agent.knowledge` was removed in @objectstack/spec 17.0.0 (#3896 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. | | **active** | `boolean` | ✅ | | | **access** | `string[]` | optional | Who can chat with this agent | diff --git a/content/docs/references/automation/control-flow.mdx b/content/docs/references/automation/control-flow.mdx index ed6f2c3ae9..b2bd72e50e 100644 --- a/content/docs/references/automation/control-flow.mdx +++ b/content/docs/references/automation/control-flow.mdx @@ -149,7 +149,7 @@ const result = FlowRegionSchema.parse(data); | **backoffMultiplier** | `number` | ✅ | Exponential backoff multiplier; 1 (the default) keeps the delay flat | | **maxRetryDelayMs** | `integer` | ✅ | Ceiling for a single backoff delay (ms) | | **jitter** | `boolean` | ✅ | Randomize each delay within [50%, 100%] of its computed value — spreads a thundering herd of simultaneous retries | -| **retryDelayMs** | `never` | optional | [REMOVED] `retryDelayMs` was removed in @objectstack/spec 17.0.0 (#4661, #4964) — the retry policy now has ONE spelling for its base delay across every surface that carries it: `job.retryPolicy`, a `try_catch` node's `retry` and `flow.errorHandling`. Rename the key to `backoffMs`; the value (milliseconds before the first retry) is unchanged. `os migrate meta --from 16` rewrites it for you. | +| **retryDelayMs** | `never` | optional | [REMOVED] `retryDelayMs` was removed in @objectstack/spec 17.0.0 (#4661, #4964) — the retry policy now has ONE spelling for its base delay across every surface that carries it: `job.retryPolicy`, a `try_catch` node's `retry` and `flow.errorHandling`. Rename the key to `backoffMs`; the value (milliseconds before the first retry) is unchanged. Run `os migrate meta --from 16` to rewrite existing sources automatically. | --- diff --git a/content/docs/references/automation/schemaless-node-config.mdx b/content/docs/references/automation/schemaless-node-config.mdx index f294928397..4686d99fcb 100644 --- a/content/docs/references/automation/schemaless-node-config.mdx +++ b/content/docs/references/automation/schemaless-node-config.mdx @@ -150,11 +150,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 (#4343) — 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 rewrite it automatically. | -| **template** | `never` | optional | [REMOVED] `script.config.template` was removed in @objectstack/spec 17 (#4343) — 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 rewrite it automatically. | -| **recipients** | `never` | optional | [REMOVED] `script.config.recipients` was removed in @objectstack/spec 17 (#4343) — 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 rewrite it automatically. | -| **variables** | `never` | optional | [REMOVED] `script.config.variables` was removed in @objectstack/spec 17 (#4343) — 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 rewrite it automatically. | -| **script** | `never` | optional | [REMOVED] `script.config.script` was removed in @objectstack/spec 17 (#4343) — 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 rewrite it automatically. | +| **actionType** | `never` | optional | [REMOVED] `script.config.actionType` was removed in @objectstack/spec 17 (#4343) — 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 rewrite the shorthand case into `config.function` automatically; the stub and marker values are removed. | +| **template** | `never` | optional | [REMOVED] `script.config.template` was removed in @objectstack/spec 17 (#4343) — 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 rewrite existing sources automatically. | +| **recipients** | `never` | optional | [REMOVED] `script.config.recipients` was removed in @objectstack/spec 17 (#4343) — 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 rewrite existing sources automatically. | +| **variables** | `never` | optional | [REMOVED] `script.config.variables` was removed in @objectstack/spec 17 (#4343) — 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 rewrite existing sources automatically. | +| **script** | `never` | optional | [REMOVED] `script.config.script` was removed in @objectstack/spec 17 (#4343) — 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 rewrite existing sources automatically. | --- diff --git a/content/docs/references/data/external-lookup.mdx b/content/docs/references/data/external-lookup.mdx index e875d4dd80..bb77e2ead6 100644 --- a/content/docs/references/data/external-lookup.mdx +++ b/content/docs/references/data/external-lookup.mdx @@ -67,7 +67,7 @@ const result = ExternalDataSourceSchema.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 (#5552, ADR-0049), and the whole `FieldMappingTransform` union went with it (`constant` / `cast` / `lookup` / `javascript` / `map`) — no runtime ever executed any of the five, and the `javascript` member advertised `dialect: "js"`, a dialect retired in #3278. Delete the key. The transform pipeline that IS enforced is the import mapping's: `mapping.fieldMapping[].transform` (a string enum — `none`/`constant`/`map`/`split`/`join`/`lookup` — with its settings in `params`), applied by the REST import path, which rejects `javascript` with a 400 rather than pretending to run it. Run `os migrate meta --from 16` to rewrite it automatically. | +| **transform** | `never` | optional | [REMOVED] `FieldMapping.transform` — authored as `connector.fieldMappings[].transform` and `externalLookup.fieldMappings[].transform` — was removed in @objectstack/spec 17.0.0 (#5552, ADR-0049), and the whole `FieldMappingTransform` union went with it (`constant` / `cast` / `lookup` / `javascript` / `map`) — no runtime ever executed any of the five, and the `javascript` member advertised `dialect: "js"`, a dialect retired in #3278. Delete the key. The transform pipeline that IS enforced is the import mapping's: `mapping.fieldMapping[].transform` (a string enum — `none`/`constant`/`map`/`split`/`join`/`lookup` — with its settings in `params`), applied by the REST import path, which rejects `javascript` with a 400 rather than pretending to run it. Run `os migrate meta --from 16` to rewrite existing sources automatically. | | **defaultValue** | `any` | optional | Default if source is null/undefined | | **type** | `string` | optional | Field type | | **readonly** | `boolean` | ✅ | Read-only field | diff --git a/content/docs/references/data/field.mdx b/content/docs/references/data/field.mdx index 766790c361..0e0374357d 100644 --- a/content/docs/references/data/field.mdx +++ b/content/docs/references/data/field.mdx @@ -119,7 +119,7 @@ const result = AddressSchema.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'` | | **requiredWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is required when TRUE. The only slot; the `conditionalRequired` alias was removed in protocol 17 (#3855). | -| **conditionalRequired** | `never` | optional | [REMOVED] `conditionalRequired` was removed in @objectstack/spec 17 (#3855) — use `requiredWhen`. Rename the key; the value (a CEL predicate) is unchanged. Run `os migrate meta --from 16` to rewrite it automatically. | +| **conditionalRequired** | `never` | optional | [REMOVED] `conditionalRequired` was removed in @objectstack/spec 17 (#3855) — use `requiredWhen`. Rename the key; the value (a CEL predicate) is unchanged. Run `os migrate meta --from 16` to rewrite existing sources automatically. | | **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 | Hidden from default UI | | **readonly** | `boolean` | optional | Read-only — never editable in forms, AND server-enforced on BOTH write paths: a non-system write to this field is silently dropped from the payload on UPDATE (#2948/#3003) and on INSERT (#3043; a create can no longer directly seed e.g. `approval_status: "approved"`), symmetric with `readonlyWhen`. A stripped INSERT field still falls back to its `defaultValue`. Exempt from the strip on BOTH paths: `isSystem` writes (seed replay, migration). Exempt on the UPDATE path ONLY: an opt-in "historical" import (`preserveAudit`, #3493) — which admits a whitelist (the audit/timestamp family plus author-declared business `readonly` fields). On INSERT the exemption does NOT apply (#6640): a non-system create that requests `preserveAudit` still has its readonly fields stripped, and is warned loudly that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. A normal (non-system) import is NOT system-context and still strips. | diff --git a/content/docs/references/data/object.mdx b/content/docs/references/data/object.mdx index 7c7265f943..9f4980b1e6 100644 --- a/content/docs/references/data/object.mdx +++ b/content/docs/references/data/object.mdx @@ -68,8 +68,8 @@ const result = ApiMethod.parse(data); | **name** | `string` | optional | Index name (auto-generated if not provided) | | **fields** | `string[]` | ✅ | Fields included in the index | | **unique** | `boolean \| 'global' \| 'organization'` | ✅ | Whether the index enforces uniqueness, and at which scope (ADR-0120). 'global' = materialized over exactly `fields`, no organization column injected — one holder across the whole installation; 'organization' = the driver prepends the NULL-safe organization key part (COALESCE(organization_id, '__global__')) at registration — one holder per organization; bare true = deprecated positional spelling of 'global' (warned in 17.x by lint unique/unscoped-declared-index, rejected at protocol 18, #5082) — state the scope. 'tenant'/'org' are rejected — the word is 'organization' | -| **type** | `never` | optional | [REMOVED] `indexes[].type` was removed in @objectstack/spec 17.0.0 (#5248, ADR-0049) — no driver ever read it. `SqlDriver.syncDeclaredIndexes` creates every declared index through knex's `table.index()` / `table.unique()`, which cannot express an access method, so the value changed no DDL; its `.default('btree')` merely made an inert knob show up in every parse output. Delete the key. The index method is the driver/dialect's decision (Postgres defaults to B-tree; `gin`/`gist`/`fulltext` are dialect-specific and are chosen by a database-layer migration when a workload actually needs one). Run `os migrate meta --from 16` to rewrite it automatically. | -| **partial** | `never` | optional | [REMOVED] `indexes[].partial` was removed in @objectstack/spec 17.0.0 (#5248, #4943, ADR-0049) — no driver ever emitted the `WHERE` clause, so a declared partial index was materialized as a FULL index and the predicate silently did nothing. Delete the key. Partial indexes are built at the database layer, not the declaration surface: issue `CREATE [UNIQUE] INDEX … WHERE ` from a runtime migration (this is what `metadata-protocol`'s `ensureOverlayIndex` already does for `sys_metadata`). Drift detection is unaffected — it reads partiality back from the database's own DDL, never from this key. Run `os migrate meta --from 16` to rewrite it automatically. | +| **type** | `never` | optional | [REMOVED] `indexes[].type` was removed in @objectstack/spec 17.0.0 (#5248, ADR-0049) — no driver ever read it. `SqlDriver.syncDeclaredIndexes` creates every declared index through knex's `table.index()` / `table.unique()`, which cannot express an access method, so the value changed no DDL; its `.default('btree')` merely made an inert knob show up in every parse output. Delete the key. The index method is the driver/dialect's decision (Postgres defaults to B-tree; `gin`/`gist`/`fulltext` are dialect-specific and are chosen by a database-layer migration when a workload actually needs one). Run `os migrate meta --from 16` to rewrite existing sources automatically. | +| **partial** | `never` | optional | [REMOVED] `indexes[].partial` was removed in @objectstack/spec 17.0.0 (#5248, #4943, ADR-0049) — no driver ever emitted the `WHERE` clause, so a declared partial index was materialized as a FULL index and the predicate silently did nothing. Delete the key. Partial indexes are built at the database layer, not the declaration surface: issue `CREATE [UNIQUE] INDEX … WHERE ` from a runtime migration (this is what `metadata-protocol`'s `ensureOverlayIndex` already does for `sys_metadata`). Drift detection is unaffected — it reads partiality back from the database's own DDL, never from this key. Run `os migrate meta --from 16` to rewrite existing sources automatically. | --- diff --git a/content/docs/references/integration/connector.mdx b/content/docs/references/integration/connector.mdx index b7ee9760ca..1b0c77a586 100644 --- a/content/docs/references/integration/connector.mdx +++ b/content/docs/references/integration/connector.mdx @@ -56,7 +56,8 @@ surface that runs it:** the import mapping's own `mapping.fieldMapping[].transfo (`data/mapping.zod.ts` — a string enum, `none`/`constant`/`map`/`split`/`join`/`lookup`, with its settings in `params`), applied row by row by the REST import path — or an ETL transformation step -(L2 above). Already authored the retired key? `os migrate meta --from 16` rewrites it. +(L2 above). Already authored the retired key? `os migrate meta --from 16` rewrites +existing sources automatically — the key itself is removed. ## Runtime contract — descriptor vs. registered connector (#2612) @@ -174,7 +175,7 @@ Circuit breaker configuration | **syncConfig** | `{ strategy?: Enum<'full' \| 'incremental' \| 'upsert' \| 'append_only'>; direction?: Enum<'import' \| 'export' \| 'bidirectional'>; schedule?: string \| object; realtimeSync?: boolean; … }` | optional | Data sync configuration | | **fieldMappings** | `{ source: string; target: string; defaultValue?: any; dataType?: Enum<'string' \| 'number' \| 'boolean' \| 'date' \| 'datetime' \| 'json' \| 'array'>; … }[]` | optional | Field mapping rules | | **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete' \| 'bulk_update' \| 'bulk_delete'>[]; … }[]` | optional | Webhook configurations (not yet enforced — never read at registration; see #3197) | -| **rateLimitConfig** | `never` | optional | [REMOVED] `connector.rateLimitConfig` was removed in @objectstack/spec 17.0.0 (#4911, 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 rewrite it automatically. | +| **rateLimitConfig** | `never` | optional | [REMOVED] `connector.rateLimitConfig` was removed in @objectstack/spec 17.0.0 (#4911, 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 rewrite existing sources automatically. | | **retryConfig** | `{ strategy?: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts?: number; initialDelayMs?: number; maxDelayMs?: number; … }` | optional | Retry configuration | | **connectionTimeoutMs** | `number` | optional | Connection timeout in ms | | **requestTimeoutMs** | `number` | optional | Request timeout in ms | @@ -262,7 +263,7 @@ Standard error category | :--- | :--- | :--- | :--- | | **source** | `string` | ✅ | Source field name | | **target** | `string` | ✅ | Target field name | -| **transform** | `never` | optional | [REMOVED] `FieldMapping.transform` — authored as `connector.fieldMappings[].transform` and `externalLookup.fieldMappings[].transform` — was removed in @objectstack/spec 17.0.0 (#5552, ADR-0049), and the whole `FieldMappingTransform` union went with it (`constant` / `cast` / `lookup` / `javascript` / `map`) — no runtime ever executed any of the five, and the `javascript` member advertised `dialect: "js"`, a dialect retired in #3278. Delete the key. The transform pipeline that IS enforced is the import mapping's: `mapping.fieldMapping[].transform` (a string enum — `none`/`constant`/`map`/`split`/`join`/`lookup` — with its settings in `params`), applied by the REST import path, which rejects `javascript` with a 400 rather than pretending to run it. Run `os migrate meta --from 16` to rewrite it automatically. | +| **transform** | `never` | optional | [REMOVED] `FieldMapping.transform` — authored as `connector.fieldMappings[].transform` and `externalLookup.fieldMappings[].transform` — was removed in @objectstack/spec 17.0.0 (#5552, ADR-0049), and the whole `FieldMappingTransform` union went with it (`constant` / `cast` / `lookup` / `javascript` / `map`) — no runtime ever executed any of the five, and the `javascript` member advertised `dialect: "js"`, a dialect retired in #3278. Delete the key. The transform pipeline that IS enforced is the import mapping's: `mapping.fieldMapping[].transform` (a string enum — `none`/`constant`/`map`/`split`/`join`/`lookup` — with its settings in `params`), applied by the REST import path, which rejects `javascript` with a 400 rather than pretending to run it. Run `os migrate meta --from 16` to rewrite existing sources automatically. | | **defaultValue** | `any` | optional | Default if source is null/undefined | | **dataType** | `Enum<'string' \| 'number' \| 'boolean' \| 'date' \| 'datetime' \| 'json' \| 'array'>` | optional | Target data type | | **required** | `boolean` | ✅ | Field is required | @@ -496,7 +497,7 @@ Connector type | **syncConfig** | `{ strategy?: Enum<'full' \| 'incremental' \| 'upsert' \| 'append_only'>; direction?: Enum<'import' \| 'export' \| 'bidirectional'>; schedule?: string \| object; realtimeSync?: boolean; … }` | optional | Data sync configuration | | **fieldMappings** | `{ source: string; target: string; defaultValue?: any; dataType?: Enum<'string' \| 'number' \| 'boolean' \| 'date' \| 'datetime' \| 'json' \| 'array'>; … }[]` | optional | Field mapping rules | | **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete' \| 'bulk_update' \| 'bulk_delete'>[]; … }[]` | optional | Webhook configurations (not yet enforced — never read at registration; see #3197) | -| **rateLimitConfig** | `never` | optional | [REMOVED] `connector.rateLimitConfig` was removed in @objectstack/spec 17.0.0 (#4911, 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 rewrite it automatically. | +| **rateLimitConfig** | `never` | optional | [REMOVED] `connector.rateLimitConfig` was removed in @objectstack/spec 17.0.0 (#4911, 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 rewrite existing sources automatically. | | **retryConfig** | `{ strategy?: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts?: number; initialDelayMs?: number; maxDelayMs?: number; … }` | optional | Retry configuration | | **connectionTimeoutMs** | `number` | optional | Connection timeout in ms | | **requestTimeoutMs** | `number` | optional | Request timeout in ms | diff --git a/content/docs/references/shared/mapping.mdx b/content/docs/references/shared/mapping.mdx index e4873d23e8..6b0e718ee4 100644 --- a/content/docs/references/shared/mapping.mdx +++ b/content/docs/references/shared/mapping.mdx @@ -73,7 +73,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 (#5552, ADR-0049), and the whole `FieldMappingTransform` union went with it (`constant` / `cast` / `lookup` / `javascript` / `map`) — no runtime ever executed any of the five, and the `javascript` member advertised `dialect: "js"`, a dialect retired in #3278. Delete the key. The transform pipeline that IS enforced is the import mapping's: `mapping.fieldMapping[].transform` (a string enum — `none`/`constant`/`map`/`split`/`join`/`lookup` — with its settings in `params`), applied by the REST import path, which rejects `javascript` with a 400 rather than pretending to run it. Run `os migrate meta --from 16` to rewrite it automatically. | +| **transform** | `never` | optional | [REMOVED] `FieldMapping.transform` — authored as `connector.fieldMappings[].transform` and `externalLookup.fieldMappings[].transform` — was removed in @objectstack/spec 17.0.0 (#5552, ADR-0049), and the whole `FieldMappingTransform` union went with it (`constant` / `cast` / `lookup` / `javascript` / `map`) — no runtime ever executed any of the five, and the `javascript` member advertised `dialect: "js"`, a dialect retired in #3278. Delete the key. The transform pipeline that IS enforced is the import mapping's: `mapping.fieldMapping[].transform` (a string enum — `none`/`constant`/`map`/`split`/`join`/`lookup` — with its settings in `params`), applied by the REST import path, which rejects `javascript` with a 400 rather than pretending to run it. Run `os migrate meta --from 16` to rewrite existing sources automatically. | | **defaultValue** | `any` | optional | Default if source is null/undefined | diff --git a/content/docs/references/system/job.mdx b/content/docs/references/system/job.mdx index a475f81e72..f60f4ddeba 100644 --- a/content/docs/references/system/job.mdx +++ b/content/docs/references/system/job.mdx @@ -125,7 +125,7 @@ const result = CronScheduleSchema.parse(data); | **backoffMultiplier** | `number` | ✅ | Exponential backoff multiplier; 1 (the default) keeps the delay flat | | **maxRetryDelayMs** | `integer` | ✅ | Ceiling for a single backoff delay (ms) | | **jitter** | `boolean` | ✅ | Randomize each delay within [50%, 100%] of its computed value — spreads a thundering herd of simultaneous retries | -| **retryDelayMs** | `never` | optional | [REMOVED] `retryDelayMs` was removed in @objectstack/spec 17.0.0 (#4661, #4964) — the retry policy now has ONE spelling for its base delay across every surface that carries it: `job.retryPolicy`, a `try_catch` node's `retry` and `flow.errorHandling`. Rename the key to `backoffMs`; the value (milliseconds before the first retry) is unchanged. `os migrate meta --from 16` rewrites it for you. | +| **retryDelayMs** | `never` | optional | [REMOVED] `retryDelayMs` was removed in @objectstack/spec 17.0.0 (#4661, #4964) — the retry policy now has ONE spelling for its base delay across every surface that carries it: `job.retryPolicy`, a `try_catch` node's `retry` and `flow.errorHandling`. Rename the key to `backoffMs`; the value (milliseconds before the first retry) is unchanged. Run `os migrate meta --from 16` to rewrite existing sources automatically. | --- diff --git a/content/docs/references/ui/action.mdx b/content/docs/references/ui/action.mdx index 19f042aaef..7d953b0919 100644 --- a/content/docs/references/ui/action.mdx +++ b/content/docs/references/ui/action.mdx @@ -70,7 +70,7 @@ const result = ActionSchema.parse(data); | **target** | `string` | optional | URL, Script Name, Flow ID, or API Endpoint. Supports $`{param.X}` and $`{ctx.X}` interpolation. | | **openIn** | `Enum<'self' \| 'new-tab'>` | optional | For type:'url' — where to open `target`. 'new-tab' opens a new browser tab; 'self' navigates in place. When omitted, external/absolute URLs open in a new tab and relative URLs navigate in place. Static execution option — keep it OUT of `params` (which is user-input-collection only). | | **body** | `{ language: 'expression'; source: string } \| { language: 'js'; source: string; capabilities?: Enum<'api.read' \| 'api.write' \| 'api.transaction' \| 'crypto.uuid' \| 'log'>[]; timeoutMs?: integer; … }` | optional | Action body — expression (L1) or sandboxed JS (L2). Only used when type is `script`. | -| **execute** | `never` | optional | [REMOVED] `execute` was removed in @objectstack/spec 17 (#3855) — use `target`. Rename the key; the value (a handler / flow / URL ref) is unchanged. Run `os migrate meta --from 16` to rewrite it automatically. | +| **execute** | `never` | optional | [REMOVED] `execute` was removed in @objectstack/spec 17 (#3855) — use `target`. Rename the key; the value (a handler / flow / URL ref) is unchanged. Run `os migrate meta --from 16` to rewrite existing sources automatically. | | **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. | diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index ed24735643..432eea9643 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -152,9 +152,9 @@ const result = AIChatWindowProps.parse(data); | **targetVariable** | `string` | optional | Page variable to bind selected record ID(s) | | **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 (#5775, 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 rewrite it automatically. | -| **searchFields** | `never` | optional | [REMOVED] `element:record_picker` property `searchFields` was removed in @objectstack/spec 17.0.0 (#5775, 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 remove it automatically. | -| **multiple** | `never` | optional | [REMOVED] `element:record_picker` property `multiple` was removed in @objectstack/spec 17.0.0 (#5775, ADR-0049) — the picker is a single-select `Select` and the bound page variable holds one record id, so `multiple: true` selected nothing extra and reported success. Delete the key; multi-record selection is not implemented on this element. Run `os migrate meta --from 16` to remove it automatically. | +| **displayField** | `never` | optional | [REMOVED] `element:record_picker` property `displayField` was removed in @objectstack/spec 17.0.0 (#5775, 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 rewrite existing sources automatically. | +| **searchFields** | `never` | optional | [REMOVED] `element:record_picker` property `searchFields` was removed in @objectstack/spec 17.0.0 (#5775, 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 rewrite existing sources automatically. | +| **multiple** | `never` | optional | [REMOVED] `element:record_picker` property `multiple` was removed in @objectstack/spec 17.0.0 (#5775, ADR-0049) — the picker is a single-select `Select` and the bound page variable holds one record id, so `multiple: true` selected nothing extra and reported success. Delete the key; multi-record selection is not implemented on this element. Run `os migrate meta --from 16` to rewrite existing sources automatically. | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | @@ -217,7 +217,7 @@ const result = AIChatWindowProps.parse(data); | **bordered** | `boolean` | ✅ | | | **actions** | `string[]` | optional | | | **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 (#5775, 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 rewrite it automatically. | +| **body** | `never` | optional | [REMOVED] `page:card` property `body` was removed in @objectstack/spec 17.0.0 (#5775, 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 rewrite existing sources automatically. | | **footer** | `any[]` | optional | Card footer components (slot) | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | @@ -261,7 +261,7 @@ const result = AIChatWindowProps.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **tabStyle** | `Enum<'line' \| 'card' \| 'pill'>` | optional | 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 (#6776, 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 rewrite it automatically. | +| **type** | `never` | optional | [REMOVED] `page:tabs` property `type` was removed in @objectstack/spec 17.0.0 (#6776, 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 rewrite existing sources automatically. | | **position** | `Enum<'top' \| 'left'>` | optional | | | **items** | `{ label: string \| Record; icon?: string; visibleWhen?: string \| object; value?: string; … }[]` | ✅ | | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | diff --git a/content/docs/references/ui/dashboard.mdx b/content/docs/references/ui/dashboard.mdx index c10166fff1..34877716e4 100644 --- a/content/docs/references/ui/dashboard.mdx +++ b/content/docs/references/ui/dashboard.mdx @@ -98,9 +98,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 (#5010, ADR-0049 enforce-or-remove) — a dashboard widget has NO action button, and never had one. No renderer draws per-widget chrome for it: every action the dashboard dispatches comes from `header.actions[]`. The three keys `actionUrl` / `actionType` / `actionIcon` went together; delete all three. Put the affordance on the dashboard header instead — `header: { actions: [{ label, actionUrl, actionType, icon }] }` — which IS dispatched (`DashboardHeaderAction`, same vocabulary, and `icon` is the header spelling of `actionIcon`). For a per-ROW affordance, the widget to reach for is a `table`/`pivot` bound to a dataset: its rows are clickable and drill through the semantic layer. Run `os migrate meta --from 16` to rewrite it automatically. | -| **actionType** | `never` | optional | [REMOVED] `dashboard.widgets[].actionType` was removed in @objectstack/spec 17.0.0 (#5010, ADR-0049 enforce-or-remove) — a dashboard widget has NO action button, and never had one. No renderer draws per-widget chrome for it: every action the dashboard dispatches comes from `header.actions[]`. The three keys `actionUrl` / `actionType` / `actionIcon` went together; delete all three. Put the affordance on the dashboard header instead — `header: { actions: [{ label, actionUrl, actionType, icon }] }` — which IS dispatched (`DashboardHeaderAction`, same vocabulary, and `icon` is the header spelling of `actionIcon`). For a per-ROW affordance, the widget to reach for is a `table`/`pivot` bound to a dataset: its rows are clickable and drill through the semantic layer. Run `os migrate meta --from 16` to rewrite it automatically. | -| **actionIcon** | `never` | optional | [REMOVED] `dashboard.widgets[].actionIcon` was removed in @objectstack/spec 17.0.0 (#5010, ADR-0049 enforce-or-remove) — a dashboard widget has NO action button, and never had one. No renderer draws per-widget chrome for it: every action the dashboard dispatches comes from `header.actions[]`. The three keys `actionUrl` / `actionType` / `actionIcon` went together; delete all three. Put the affordance on the dashboard header instead — `header: { actions: [{ label, actionUrl, actionType, icon }] }` — which IS dispatched (`DashboardHeaderAction`, same vocabulary, and `icon` is the header spelling of `actionIcon`). For a per-ROW affordance, the widget to reach for is a `table`/`pivot` bound to a dataset: its rows are clickable and drill through the semantic layer. Run `os migrate meta --from 16` to rewrite it automatically. | +| **actionUrl** | `never` | optional | [REMOVED] `dashboard.widgets[].actionUrl` was removed in @objectstack/spec 17.0.0 (#5010, ADR-0049 enforce-or-remove) — a dashboard widget has NO action button, and never had one. No renderer draws per-widget chrome for it: every action the dashboard dispatches comes from `header.actions[]`. The three keys `actionUrl` / `actionType` / `actionIcon` went together; delete all three. Put the affordance on the dashboard header instead — `header: { actions: [{ label, actionUrl, actionType, icon }] }` — which IS dispatched (`DashboardHeaderAction`, same vocabulary, and `icon` is the header spelling of `actionIcon`). For a per-ROW affordance, the widget to reach for is a `table`/`pivot` bound to a dataset: its rows are clickable and drill through the semantic layer. Run `os migrate meta --from 16` to rewrite existing sources automatically. | +| **actionType** | `never` | optional | [REMOVED] `dashboard.widgets[].actionType` was removed in @objectstack/spec 17.0.0 (#5010, ADR-0049 enforce-or-remove) — a dashboard widget has NO action button, and never had one. No renderer draws per-widget chrome for it: every action the dashboard dispatches comes from `header.actions[]`. The three keys `actionUrl` / `actionType` / `actionIcon` went together; delete all three. Put the affordance on the dashboard header instead — `header: { actions: [{ label, actionUrl, actionType, icon }] }` — which IS dispatched (`DashboardHeaderAction`, same vocabulary, and `icon` is the header spelling of `actionIcon`). For a per-ROW affordance, the widget to reach for is a `table`/`pivot` bound to a dataset: its rows are clickable and drill through the semantic layer. Run `os migrate meta --from 16` to rewrite existing sources automatically. | +| **actionIcon** | `never` | optional | [REMOVED] `dashboard.widgets[].actionIcon` was removed in @objectstack/spec 17.0.0 (#5010, ADR-0049 enforce-or-remove) — a dashboard widget has NO action button, and never had one. No renderer draws per-widget chrome for it: every action the dashboard dispatches comes from `header.actions[]`. The three keys `actionUrl` / `actionType` / `actionIcon` went together; delete all three. Put the affordance on the dashboard header instead — `header: { actions: [{ label, actionUrl, actionType, icon }] }` — which IS dispatched (`DashboardHeaderAction`, same vocabulary, and `icon` is the header spelling of `actionIcon`). For a per-ROW affordance, the widget to reach for is a `table`/`pivot` bound to a dataset: its rows are clickable and drill through the semantic layer. Run `os migrate meta --from 16` to rewrite existing sources automatically. | | **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) | @@ -110,8 +110,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 (#4876, ADR-0049 D2) — no renderer ever read it, so per-widget breakpoint overrides were never applied: the value parsed, validated, and then did nothing. The dashboard grid reflows by its own layout rules (`columns` + `gap` on the dashboard, the `layout` box on each widget). Delete the key. The shared `ResponsiveConfig` shape is NOT gone — it stays live on `page.components[].responsive`, which objectui `useResponsiveConfig` really does read; move the layout there if you need breakpoint behaviour today. Run `os migrate meta --from 16` to rewrite it automatically. | -| **aria** | `never` | optional | [REMOVED] `dashboard.widgets[].aria` was removed in @objectstack/spec 17.0.0 (#5010, ADR-0049 D2) — no renderer ever applied it, so ARIA attributes declared on a widget silently did not reach the DOM: the key promised accessibility compliance it did not deliver. This is the same removal the dashboard-level `aria` got in 17.0.0 (#3896). Delete the key. The dashboard renderer emits its own `aria-*` attributes for the widget grid; author a `title` (and `description`) on the widget instead — those ARE what the renderer labels the card with. The shared `AriaProps` shape is NOT gone: it stays live on `page.aria`, `page.components[].aria` and the list view `aria`. Run `os migrate meta --from 16` to remove it automatically. | +| **responsive** | `never` | optional | [REMOVED] `dashboard.widgets[].responsive` was removed in @objectstack/spec 17.0.0 (#4876, ADR-0049 D2) — no renderer ever read it, so per-widget breakpoint overrides were never applied: the value parsed, validated, and then did nothing. The dashboard grid reflows by its own layout rules (`columns` + `gap` on the dashboard, the `layout` box on each widget). Delete the key. The shared `ResponsiveConfig` shape is NOT gone — it stays live on `page.components[].responsive`, which objectui `useResponsiveConfig` really does read; move the layout there if you need breakpoint behaviour today. Run `os migrate meta --from 16` to rewrite existing sources automatically. | +| **aria** | `never` | optional | [REMOVED] `dashboard.widgets[].aria` was removed in @objectstack/spec 17.0.0 (#5010, ADR-0049 D2) — no renderer ever applied it, so ARIA attributes declared on a widget silently did not reach the DOM: the key promised accessibility compliance it did not deliver. This is the same removal the dashboard-level `aria` got in 17.0.0 (#3896). Delete the key. The dashboard renderer emits its own `aria-*` attributes for the widget grid; author a `title` (and `description`) on the widget instead — those ARE what the renderer labels the card with. The shared `AriaProps` shape is NOT gone: it stays live on `page.aria`, `page.components[].aria` and the list view `aria`. Run `os migrate meta --from 16` to rewrite existing sources automatically. | ### Allowed Values: `DashboardWidget.type` diff --git a/content/docs/references/ui/theme.mdx b/content/docs/references/ui/theme.mdx index be94799318..a9105b510a 100644 --- a/content/docs/references/ui/theme.mdx +++ b/content/docs/references/ui/theme.mdx @@ -101,8 +101,8 @@ const result = BorderRadiusSchema.parse(data); | **typography** | `{ fontFamily?: object }` | optional | Typography settings | | **borderRadius** | `{ none?: string; sm?: string; base?: string; md?: string; … }` | optional | Border radius scale | | **shadows** | `{ none?: string; sm?: string; base?: string; md?: string; … }` | optional | Box shadow effects | -| **animation** | `never` | optional | [REMOVED] `theme.animation` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — unlike the #3494 props above, the engine DID emit `--duration-*` and `--timing-*`, faithfully and for years; what never existed was a reader. No first-party component or stylesheet has ever consumed one, so every transition ran at the renderer default whatever you declared. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "duration-fast": "150ms", "timing-ease_in": "cubic-bezier(0.4, 0, 1, 1)" }` emits exactly the same properties). Run `os migrate meta --from 16` to rewrite it automatically. | -| **zIndex** | `never` | optional | [REMOVED] `theme.zIndex` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--z-base` … `--z-tooltip` and nothing read one, so an overlay you "lifted" still stacked by document order. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "z-modal": "1050" }` emits exactly the same `--z-modal`). Run `os migrate meta --from 16` to rewrite it automatically. | +| **animation** | `never` | optional | [REMOVED] `theme.animation` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — unlike the #3494 props above, the engine DID emit `--duration-*` and `--timing-*`, faithfully and for years; what never existed was a reader. No first-party component or stylesheet has ever consumed one, so every transition ran at the renderer default whatever you declared. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "duration-fast": "150ms", "timing-ease_in": "cubic-bezier(0.4, 0, 1, 1)" }` emits exactly the same properties). Run `os migrate meta --from 16` to rewrite existing sources automatically. | +| **zIndex** | `never` | optional | [REMOVED] `theme.zIndex` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--z-base` … `--z-tooltip` and nothing read one, so an overlay you "lifted" still stacked by document order. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "z-modal": "1050" }` emits exactly the same `--z-modal`). Run `os migrate meta --from 16` to rewrite existing sources automatically. | | **customVars** | `Record` | optional | Custom CSS variables (key-value pairs) | | **extends** | `string` | optional | Base theme to extend from | @@ -127,10 +127,10 @@ const result = BorderRadiusSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **fontFamily** | `{ base?: string }` | optional | | -| **fontSize** | `never` | optional | [REMOVED] `theme.typography.fontSize` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--font-size-xs` … `--font-size-4xl` faithfully and NO first-party component or stylesheet has ever read one, so a declared type scale was real CSS that styled nothing. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "font-size-lg": "1.125rem" }` emits exactly the same `--font-size-lg`). Run `os migrate meta --from 16` to rewrite it automatically. | -| **fontWeight** | `never` | optional | [REMOVED] `theme.typography.fontWeight` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--font-weight-*` and nothing read it, so text rendered at the inherited weight whatever you declared. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "font-weight-semibold": "600" }` emits exactly the same `--font-weight-semibold`). Run `os migrate meta --from 16` to rewrite it automatically. | -| **lineHeight** | `never` | optional | [REMOVED] `theme.typography.lineHeight` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--line-height-*` and nothing read it, so every block kept its inherited leading. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "line-height-relaxed": "1.75" }` emits exactly the same `--line-height-relaxed`). Run `os migrate meta --from 16` to rewrite it automatically. | -| **letterSpacing** | `never` | optional | [REMOVED] `theme.typography.letterSpacing` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--letter-spacing-*` and nothing read it, so tracking never moved. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "letter-spacing-wide": "0.025em" }` emits exactly the same `--letter-spacing-wide`). Run `os migrate meta --from 16` to rewrite it automatically. | +| **fontSize** | `never` | optional | [REMOVED] `theme.typography.fontSize` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--font-size-xs` … `--font-size-4xl` faithfully and NO first-party component or stylesheet has ever read one, so a declared type scale was real CSS that styled nothing. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "font-size-lg": "1.125rem" }` emits exactly the same `--font-size-lg`). Run `os migrate meta --from 16` to rewrite existing sources automatically. | +| **fontWeight** | `never` | optional | [REMOVED] `theme.typography.fontWeight` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--font-weight-*` and nothing read it, so text rendered at the inherited weight whatever you declared. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "font-weight-semibold": "600" }` emits exactly the same `--font-weight-semibold`). Run `os migrate meta --from 16` to rewrite existing sources automatically. | +| **lineHeight** | `never` | optional | [REMOVED] `theme.typography.lineHeight` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--line-height-*` and nothing read it, so every block kept its inherited leading. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "line-height-relaxed": "1.75" }` emits exactly the same `--line-height-relaxed`). Run `os migrate meta --from 16` to rewrite existing sources automatically. | +| **letterSpacing** | `never` | optional | [REMOVED] `theme.typography.letterSpacing` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--letter-spacing-*` and nothing read it, so tracking never moved. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "letter-spacing-wide": "0.025em" }` emits exactly the same `--letter-spacing-wide`). Run `os migrate meta --from 16` to rewrite existing sources automatically. | --- diff --git a/packages/spec/src/ai/agent.zod.ts b/packages/spec/src/ai/agent.zod.ts index 7275c2b7ae..bd598fa0f1 100644 --- a/packages/spec/src/ai/agent.zod.ts +++ b/packages/spec/src/ai/agent.zod.ts @@ -233,7 +233,10 @@ export const AgentSchema = lazySchema(() => strictObject({ '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. ' + - 'Run `os migrate meta --from 16` to rewrite it automatically.', + '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. ' + + 'Run `os migrate meta --from 16` to rewrite existing sources automatically.', ), /** Knowledge */ diff --git a/packages/spec/src/automation/schemaless-node-config.zod.ts b/packages/spec/src/automation/schemaless-node-config.zod.ts index c64530e912..d496a723c6 100644 --- a/packages/spec/src/automation/schemaless-node-config.zod.ts +++ b/packages/spec/src/automation/schemaless-node-config.zod.ts @@ -258,33 +258,34 @@ export const ScriptConfigSchema = lazySchema(() => strictObject({ + '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 rewrite it automatically.', + + 'name into `config.function`. Run `os migrate meta --from 16` to rewrite the shorthand ' + + 'case into `config.function` automatically; the stub and marker values are removed.', ), template: retiredKey( '`script.config.template` was removed in @objectstack/spec 17 (#4343) — 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 rewrite it automatically.', + + 'node. Run `os migrate meta --from 16` to rewrite existing sources automatically.', ), recipients: retiredKey( '`script.config.recipients` was removed in @objectstack/spec 17 (#4343) — 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 rewrite it automatically.', + + 'service for real. Run `os migrate meta --from 16` to rewrite existing sources automatically.', ), variables: retiredKey( '`script.config.variables` was removed in @objectstack/spec 17 (#4343) — 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 rewrite it automatically.', + + 'Run `os migrate meta --from 16` to rewrite existing sources automatically.', ), script: retiredKey( '`script.config.script` was removed in @objectstack/spec 17 (#4343) — 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 rewrite it automatically.', + + 'Run `os migrate meta --from 16` to rewrite existing sources automatically.', ), })); diff --git a/packages/spec/src/data/datasource.zod.ts b/packages/spec/src/data/datasource.zod.ts index 26f8cc4f4b..66895cea55 100644 --- a/packages/spec/src/data/datasource.zod.ts +++ b/packages/spec/src/data/datasource.zod.ts @@ -57,7 +57,7 @@ const RETIRED_CAPABILITIES: Record = { + 'Pushdown is decided by the runtime driver\'s own `supports.*` object, not by datasource ' + '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 rewrite it automatically.', + + 'read-only. Run `os migrate meta --from 16` to rewrite existing sources automatically.', readOnly: CAPABILITIES_REMOVED_PREFIX + '`readOnly` in particular NEVER made a datasource read-only: no write path consulted it, ' @@ -86,7 +86,7 @@ const RETIRED_DATASOURCE_BLOCKS: Record = { + 'CAREFUL — do NOT "fix" this by renaming keys: `hook.retryPolicy` and `job.retryPolicy` ARE ' + '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 remove it.', + + 'actually want that hook or job retried. Run `os migrate meta --from 16` to rewrite existing sources automatically.', healthCheck: '`datasource.healthCheck` was removed in @objectstack/spec 17.0.0 (#4583, ADR-0049) — no ' + 'health-check loop ever read it, so `enabled: true` scheduled nothing and the two timeouts ' @@ -94,18 +94,18 @@ 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 remove it.', + + 'Run `os migrate meta --from 16` to rewrite existing sources automatically.', externalLabel: '`external.label` was removed in @objectstack/spec 17.0.0 (#4583, 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 remove it.', + + 'Setup → Datasources actually renders. Run `os migrate meta --from 16` to rewrite existing sources automatically.', externalRequirePermission: '`external.requirePermission` was removed in @objectstack/spec 17.0.0 (#4583, ADR-0049) — no ' + 'authorization check ever consulted it, so a permission named here gated nothing. Access to ' + "a federated datasource's data is governed by the ordinary object permission sets and RLS, " + '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 remove it.', + + 'permissions instead. Run `os migrate meta --from 16` to rewrite existing sources automatically.', }; /** @@ -163,7 +163,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 rewrite it automatically.'; + + 'Run `os migrate meta --from 16` to rewrite existing sources automatically.'; export const DriverType = z.string().describe('Underlying driver identifier'); export type DriverType = z.input; diff --git a/packages/spec/src/data/field.zod.ts b/packages/spec/src/data/field.zod.ts index 03836f4491..b944b36d80 100644 --- a/packages/spec/src/data/field.zod.ts +++ b/packages/spec/src/data/field.zod.ts @@ -812,7 +812,7 @@ export const FieldSchema = lazySchema(() => strictObject({ conditionalRequired: retiredKey( '`conditionalRequired` was removed in @objectstack/spec 17 (#3855) — use `requiredWhen`. ' + 'Rename the key; the value (a CEL predicate) is unchanged. ' + - 'Run `os migrate meta --from 16` to rewrite it automatically.', + 'Run `os migrate meta --from 16` to rewrite existing sources automatically.', ), /** diff --git a/packages/spec/src/data/hook-body.zod.ts b/packages/spec/src/data/hook-body.zod.ts index 0c91131c08..c6eca26cbc 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 rewrite it automatically.'; + + 'Run `os migrate meta --from 16` to rewrite existing sources automatically.'; /** * Capability tokens a script body may request. diff --git a/packages/spec/src/data/object.zod.ts b/packages/spec/src/data/object.zod.ts index abe39c9bc9..cf4e2ffdd0 100644 --- a/packages/spec/src/data/object.zod.ts +++ b/packages/spec/src/data/object.zod.ts @@ -141,13 +141,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 at ' + '#3146 and, if built, returns as a live enforced flag (ADR-0049 prune-or-build). ' + - 'Run `os migrate meta --from 16` to rewrite it automatically.', + 'Run `os migrate meta --from 16` to rewrite existing sources automatically.', mru: '`enable.mru` was removed from @objectstack/spec in the 16.x line (#2377/#3207, ' + '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 rewrite it automatically.', + 'Run `os migrate meta --from 16` to rewrite existing sources automatically.', }; /** @@ -417,7 +417,7 @@ export const IndexSchema = lazySchema(() => z.object({ '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 rewrite it automatically.', + 'Run `os migrate meta --from 16` to rewrite existing sources automatically.', ), partial: retiredKey( '`indexes[].partial` was removed in @objectstack/spec 17.0.0 (#5248, #4943, ADR-0049) — no ' + @@ -427,7 +427,7 @@ export const IndexSchema = lazySchema(() => z.object({ '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 rewrite it automatically.', + 'Run `os migrate meta --from 16` to rewrite existing sources automatically.', ), })); @@ -1138,7 +1138,7 @@ const MANAGED_BY_SYSTEM_RETIRED = + 'and can be deleted; keep it only to NARROW. CSV `import` is deliberately NOT in that ' + 'default (#4671): 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 ' - + 'rewrite it automatically.'; + + 'rewrite existing sources automatically.'; /** * 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 24cd0307d9..712cf12660 100644 --- a/packages/spec/src/data/query.zod.ts +++ b/packages/spec/src/data/query.zod.ts @@ -81,7 +81,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 rewrite it automatically.'; + + 'Run `os migrate meta --from 16` to rewrite existing sources automatically.'; const ARRAY_AGG_RETIRED = '`array_agg`' + AGG_RETIRED_MIDDLE diff --git a/packages/spec/src/integration/connector.zod.ts b/packages/spec/src/integration/connector.zod.ts index d9c4315032..7d6f4e40ee 100644 --- a/packages/spec/src/integration/connector.zod.ts +++ b/packages/spec/src/integration/connector.zod.ts @@ -60,7 +60,8 @@ import { retiredKey } from '../shared/retired-key'; * (`data/mapping.zod.ts` — a string enum, * `none`/`constant`/`map`/`split`/`join`/`lookup`, with its settings in `params`), * applied row by row by the REST import path — or an ETL transformation step - * (L2 above). Already authored the retired key? `os migrate meta --from 16` rewrites it. + * (L2 above). Already authored the retired key? `os migrate meta --from 16` rewrites + * existing sources automatically — the key itself is removed. * * ## Runtime contract — descriptor vs. registered connector (#2612) * @@ -801,7 +802,7 @@ export const ConnectorSchema = 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 rewrite it automatically.', + 'Run `os migrate meta --from 16` to rewrite existing sources automatically.', ), /** diff --git a/packages/spec/src/shared/mapping.zod.ts b/packages/spec/src/shared/mapping.zod.ts index 3880a59585..073225c3ac 100644 --- a/packages/spec/src/shared/mapping.zod.ts +++ b/packages/spec/src/shared/mapping.zod.ts @@ -96,7 +96,7 @@ export const FieldMappingSchema = lazySchema(() => z.object({ + "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 rewrite it automatically.', + + 'pretending to run it. Run `os migrate meta --from 16` to rewrite existing sources automatically.', ), /** diff --git a/packages/spec/src/shared/retired-key-migrate-sentence.test.ts b/packages/spec/src/shared/retired-key-migrate-sentence.test.ts new file mode 100644 index 0000000000..48a33d892c --- /dev/null +++ b/packages/spec/src/shared/retired-key-migrate-sentence.test.ts @@ -0,0 +1,223 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#6856] 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): + * + * Run `os migrate meta --from ` to rewrite existing sources automatically. + * + * The sentence states a property of the TOOL (it rewrites your source files), + * 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…"). 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 rewrites only PART of the value keeps the + * two-clause form naming which part — + * "… to rewrite the case … automatically; ." (`ui/dashboard.zod.ts` `compareTo.offset` is the model; the script + * node's `config.actionType` is the other member.) + * + * 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 + * prescription. `migrations/registry.ts` is out of scope structurally — it is + * the migration LEDGER, whose `notes` are release prose over whole migrations, + * not tombstone prescriptions an author meets in a parse error. That is a + * scope bound on the corpus, not a per-site exemption: every prescription + * string in every schema file is judged, with no allowlist. + * + * What this pin deliberately does NOT check: a tombstone whose prescription + * carries no `os migrate meta` sentence at all (#6914's worklist) — absence of + * the marker means nothing is judged. The floor assertion below only guards + * the scanner itself against going blind. + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import url from 'node:url'; + +import { describe, expect, it } from 'vitest'; + +const HERE = path.dirname(url.fileURLToPath(import.meta.url)); +const SRC_ROOT = path.resolve(HERE, '..'); + +/** The migration ledger — release prose, not tombstone prescriptions (see module doc). */ +const OUT_OF_SCOPE = new Set([path.join('migrations', 'registry.ts')]); + +const MARKER = /(?:Run )?`os migrate meta --from \d+`/g; + +/** + * House form, anchored at the marker: the sentence must be the LAST sentence + * of its string literal (the char after the final period is the closing + * quote), so a prescription cannot bury the command mid-prose either. + */ +const HOUSE_AT_MARKER = + /^Run `os migrate meta --from \d+` to rewrite existing sources automatically\.['"]/; + +/** + * MIXED two-clause shape: clause one names the part of the value the tool + * rewrites, clause two says what it does with the rest. Shape, not sites. + */ +const MIXED_AT_MARKER = + /^Run `os migrate meta --from \d+` to rewrite the [^;'"]+ case[^;'"]* automatically; [^;'"]+\.['"]/; + +interface JudgedSite { + /** Path relative to `packages/spec/src`. */ + file: string; + /** 1-based line of the sentence's marker (best effort across concatenation). */ + line: number; + /** The sentence tail from the marker (for the failure message). */ + excerpt: string; + ok: boolean; +} + +/** + * Reconstruct judgeable text from one source file: drop comment lines, then + * merge string-concatenation seams (`'…' + '…'`, same line or across lines, + * single or double quotes) so a sentence split across literals is judged + * whole. Returns every marker occurrence with its verdict. + */ +export function judgeMigrateSentences(raw: string, file = ''): JudgedSite[] { + const kept: Array<{ text: string; line: number }> = []; + raw.split('\n').forEach((text, i) => { + const t = text.trim(); + if (t.startsWith('*') || t.startsWith('//') || t.startsWith('/*')) return; + kept.push({ text, line: i + 1 }); + }); + + let merged = ''; + const segments: Array<{ start: number; line: number }> = []; + for (const k of kept) { + // Same-line concatenation seam: '…' + '…' (either quote on either side). + let lineText = k.text.replace(/['"]\s*\+\s*['"]/g, ''); + // Cross-line seam: previous line ended a literal, this one reopens it. + const reopen = /^\s*\+\s*['"]/.exec(lineText); + if (reopen && /['"]\s*$/.test(merged)) { + merged = merged.replace(/['"]\s*$/, ''); + lineText = lineText.slice(reopen[0].length); + } + segments.push({ start: merged.length, line: k.line }); + merged += `${lineText}\n`; + } + + const judged: JudgedSite[] = []; + for (const m of merged.matchAll(MARKER)) { + const at = m.index ?? 0; + const rest = merged.slice(at); + const ok = HOUSE_AT_MARKER.test(rest) || MIXED_AT_MARKER.test(rest); + let line = 0; + for (const seg of segments) { + if (seg.start > at) break; + line = seg.line; + } + judged.push({ file, line, excerpt: rest.slice(0, 120).replace(/\n/g, ' '), ok }); + } + return judged; +} + +function* walk(dir: string): Generator { + for (const name of fs.readdirSync(dir)) { + const p = path.join(dir, name); + if (fs.statSync(p).isDirectory()) yield* walk(p); + else if (p.endsWith('.ts') && !p.endsWith('.test.ts') && !p.endsWith('.spec.ts')) yield p; + } +} + +function judgeTree(): JudgedSite[] { + const all: JudgedSite[] = []; + for (const file of walk(SRC_ROOT)) { + const rel = path.relative(SRC_ROOT, file); + if (OUT_OF_SCOPE.has(rel)) continue; + all.push(...judgeMigrateSentences(fs.readFileSync(file, 'utf8'), rel)); + } + return all; +} + +describe('retiredKey() `os migrate meta` sentences are the house sentence (#6856 route D)', () => { + it('every prescription sentence in packages/spec/src is house-form or MIXED two-clause', () => { + const judged = judgeTree(); + const violations = judged.filter((j) => !j.ok); + expect( + violations, + violations + .map((v) => `${v.file}:${v.line} — "${v.excerpt}"`) + .join('\n'), + ).toEqual([]); + }); + + it('anti-vacuity: the scanner actually judges the corpus (floor, not a census)', () => { + // 54 prescription sentences at the time of the sweep. The floor guards + // against the SCANNER going blind (a regex or comment-filter regression + // reporting an empty corpus as green), not against tombstones aging out — + // lower it deliberately, with the removal that shrinks the corpus, when + // that day comes. #6914's 35 pending sentences will only raise the count. + const judged = judgeTree(); + expect(judged.length).toBeGreaterThanOrEqual(50); + expect(judged.every((j) => j.ok)).toBe(true); + }); + + it('goes RED on the retired "rewrite it" spelling, naming the site', () => { + const planted = [ + "const X = retiredKey(", + " '`x.y` was removed in @objectstack/spec 17.0.0 (#0000) — nothing read it. Delete the key. '", + " + 'Run `os migrate meta --from 16` to rewrite it automatically.',", + ');', + ].join('\n'); + const judged = judgeMigrateSentences(planted, 'planted.zod.ts'); + expect(judged).toHaveLength(1); + expect(judged[0]!.ok).toBe(false); + expect(judged[0]!.file).toBe('planted.zod.ts'); + expect(judged[0]!.line).toBeGreaterThan(0); + }); + + it('goes RED on every other retired spelling and on an emptied sentence', () => { + const bad = [ + // The three other spellings the sweep retired. + "'Delete the key. Run `os migrate meta --from 16` to remove it.'", + "'Delete the key. Run `os migrate meta --from 16` to remove it automatically.'", + "'Rename the key to `b`; the value is unchanged. `os migrate meta --from 16` rewrites it for you.'", + // 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 rewrite existing sources automatically. Also do X.'", + ]; + for (const literal of bad) { + const judged = judgeMigrateSentences(`const s = ${literal};`, 'bad.zod.ts'); + expect(judged, literal).toHaveLength(1); + expect(judged[0]!.ok, literal).toBe(false); + } + }); + + 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 rewrite existing sources automatically.';", + // House, sentence split across a cross-line concatenation seam. + "const b = '`k` was removed (#1). Run `os migrate meta --from 16` to rewrite existing sources '\n + 'automatically.';", + // MIXED two-clause (the dashboard model). + "const c = 'Run `os migrate meta --from 16` to rewrite the `1y` case automatically; 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 rewrite the shorthand case into `config.function` automatically; the stub and marker values are removed.';", + ]; + for (const src of good) { + const judged = judgeMigrateSentences(src, 'good.zod.ts'); + expect(judged, src).toHaveLength(1); + expect(judged[0]!.ok, src).toBe(true); + } + }); + + it('does not judge comment prose — only string literals carry prescriptions', () => { + const commented = [ + '/**', + ' * Historical note: `os migrate meta --from 16` rewrites it for you.', + ' */', + '// and a line comment: Run `os migrate meta --from 16` to remove it.', + "const live = 'unrelated string';", + ].join('\n'); + expect(judgeMigrateSentences(commented, 'comments.zod.ts')).toHaveLength(0); + }); +}); diff --git a/packages/spec/src/shared/retired-key.ts b/packages/spec/src/shared/retired-key.ts index 07d16ff35c..7cdf14a6c5 100644 --- a/packages/spec/src/shared/retired-key.ts +++ b/packages/spec/src/shared/retired-key.ts @@ -31,6 +31,27 @@ * agent bumping `@objectstack/spec` sees THIS string, not our docs site. Write * it as an instruction, with the FROM → TO mapping and the one-line fix. * + * **The `os migrate meta` sentence is standardized — do not choose a verb.** + * 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): + * + * Run `os migrate meta --from ` to rewrite existing sources automatically. + * + * The sentence states a property of the TOOL — it rewrites your source + * files — 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 wrong reading promises a value conversion that + * never happens; "existing sources" has one antecedent. 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 rewrites only PART of the value + * keeps the two-clause form naming which part — "… to rewrite the case + * … automatically; ." (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 * it is archaeology rather than an upgrade (the history lives in CHANGELOG.md). @@ -47,13 +68,16 @@ import { z } from 'zod'; * * @param guidance - The upgrade prescription. State what replaced the key, the * version that removed it, and the one-line fix — this string IS the migration - * doc for anyone who hits it. + * doc for anyone who hits it. When an ADR-0087 conversion covers the surface, + * close with the house `os migrate meta` sentence (module docblock above — + * the wording is pinned, not a choice). * * @example * ```ts * conditionalRequired: retiredKey( * '`conditionalRequired` was removed in @objectstack/spec 17.0.0 (#3855). ' + - * 'Rename the key to `requiredWhen` — the value (a CEL predicate) is unchanged.', + * 'Rename the key to `requiredWhen` — the value (a CEL predicate) is unchanged. ' + + * 'Run `os migrate meta --from 16` to rewrite existing sources automatically.', * ), * ``` */ diff --git a/packages/spec/src/shared/retry-policy.zod.ts b/packages/spec/src/shared/retry-policy.zod.ts index 3b0725b760..cbedb72cc3 100644 --- a/packages/spec/src/shared/retry-policy.zod.ts +++ b/packages/spec/src/shared/retry-policy.zod.ts @@ -124,7 +124,7 @@ export function retryPolicyShape() { '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. `os migrate meta --from 16` rewrites it for you.', + 'is unchanged. Run `os migrate meta --from 16` to rewrite existing sources automatically.', ), }; } diff --git a/packages/spec/src/ui/action.zod.ts b/packages/spec/src/ui/action.zod.ts index 5b1fddc313..a2760d07e4 100644 --- a/packages/spec/src/ui/action.zod.ts +++ b/packages/spec/src/ui/action.zod.ts @@ -808,7 +808,7 @@ const actionObject = () => strictObject({ execute: retiredKey( '`execute` was removed in @objectstack/spec 17 (#3855) — use `target`. ' + 'Rename the key; the value (a handler / flow / URL ref) is unchanged. ' + - 'Run `os migrate meta --from 16` to rewrite it automatically.', + 'Run `os migrate meta --from 16` to rewrite existing sources automatically.', ), /** diff --git a/packages/spec/src/ui/aria-carrier-tombstones.test.ts b/packages/spec/src/ui/aria-carrier-tombstones.test.ts index ebeda329bb..c619dad536 100644 --- a/packages/spec/src/ui/aria-carrier-tombstones.test.ts +++ b/packages/spec/src/ui/aria-carrier-tombstones.test.ts @@ -95,10 +95,16 @@ describe('the `aria` tombstones name only live `AriaProps` carriers (#6756)', () // None of the above may be bought by weakening the prescription itself. expect(message).toContain('author a `title`'); - // The conversion STRIPS this key (`stripKeys(..., [... 'aria'])`); it does - // not relocate it. The verb has to say so. + // The conversion STRIPS this key (`stripKeys(..., [... 'aria'])`). The + // key's fate is the body's job ("Delete the key."), asserted above; the + // `os migrate meta` sentence states what the TOOL does and is the house + // sentence (#6856 route D — superseding the #6854 `to remove it` pin, + // an accepted cost of that ruling). Class-wide shape enforcement lives in + // `shared/retired-key-migrate-sentence.test.ts`; this pin holds the one + // site that already flipped once. + expect(message).toContain('Delete the key.'); expect(message).toContain('os migrate meta --from 16'); - expect(message).toMatch(/to remove it/); + expect(message).toMatch(/to rewrite existing sources automatically\.$/); }); it('the App.aria tombstone points at a page component, not at the retired widget surface', () => { diff --git a/packages/spec/src/ui/component.zod.ts b/packages/spec/src/ui/component.zod.ts index 158b713078..95daee61e3 100644 --- a/packages/spec/src/ui/component.zod.ts +++ b/packages/spec/src/ui/component.zod.ts @@ -304,7 +304,7 @@ export const PageTabsProps = z.object({ + '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 rewrite it automatically.', + + 'Run `os migrate meta --from 16` to rewrite existing sources automatically.', ), position: z.enum(['top', 'left']).default('top'), items: z.array(z.object({ @@ -369,7 +369,7 @@ export const PageCardProps = z.object({ '`page:card` property `body` was removed in @objectstack/spec 17.0.0 (#5775, 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 rewrite it automatically.', + + 'components) is unchanged. Run `os migrate meta --from 16` to rewrite existing sources automatically.', ), /** Slot for footer content */ footer: z.array(z.unknown()).optional().describe('Card footer components (slot)'), @@ -843,7 +843,7 @@ export const ElementRecordPickerPropsSchema = lazySchema(() => z.object({ + '(#5775, 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 rewrite it automatically.', + + 'Run `os migrate meta --from 16` to rewrite existing sources automatically.', ), /** * REMOVED (#5775). ADR-0049 enforce-or-remove: the control has no search @@ -854,7 +854,7 @@ export const ElementRecordPickerPropsSchema = lazySchema(() => z.object({ + '(#5775, 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 remove it automatically.', + + 'query path does apply. Run `os migrate meta --from 16` to rewrite existing sources automatically.', ), /** * REMOVED (#5775). ADR-0049 enforce-or-remove: the control is a single-select @@ -865,7 +865,7 @@ export const ElementRecordPickerPropsSchema = lazySchema(() => z.object({ + '(#5775, ADR-0049) — the picker is a single-select `Select` and the bound page variable ' + 'holds one record id, so `multiple: true` selected nothing extra and reported success. ' + 'Delete the key; multi-record selection is not implemented on this element. ' - + 'Run `os migrate meta --from 16` to remove it automatically.', + + 'Run `os migrate meta --from 16` to rewrite existing sources automatically.', ), /** ARIA accessibility */ aria: AriaPropsSchema.optional().describe('ARIA accessibility attributes'), diff --git a/packages/spec/src/ui/dashboard.zod.ts b/packages/spec/src/ui/dashboard.zod.ts index 8cd24371eb..0f53fc447b 100644 --- a/packages/spec/src/ui/dashboard.zod.ts +++ b/packages/spec/src/ui/dashboard.zod.ts @@ -291,7 +291,7 @@ const COMPARE_TO_STRING_RETIRED = (kind: 'previousPeriod' | 'previousYear') => + `Write \`compareTo: { kind: '${kind}' }\` instead — same comparison, spelled the way the ` + '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 rewrite it automatically.'; + + 'it. Run `os migrate meta --from 16` to rewrite existing sources automatically.'; // ── Per-widget action button prescriptions (#5010) ─────────────────────────── // @@ -314,7 +314,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 rewrite it automatically.'; + + 'Run `os migrate meta --from 16` to rewrite existing sources automatically.'; /** * Dashboard Widget Schema @@ -598,7 +598,7 @@ export const DashboardWidgetSchema = lazySchema(() => strictObject({ 'The shared `ResponsiveConfig` shape is NOT gone — it stays live on `page.components[].responsive`, ' + 'which objectui `useResponsiveConfig` really does read; move the layout there if you need ' + 'breakpoint behaviour today. ' + - 'Run `os migrate meta --from 16` to rewrite it automatically.', + 'Run `os migrate meta --from 16` to rewrite existing sources automatically.', ), // `aria` REMOVED (#5010, ADR-0049 D2): the same "false compliance" the @@ -626,7 +626,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 remove it automatically.', + 'Run `os migrate meta --from 16` to rewrite existing sources automatically.', ), // ADR-0021 single-form: every widget binds a `dataset` and selects `values` // (both required above) — there is no inline-query shape to disambiguate. diff --git a/packages/spec/src/ui/theme.zod.ts b/packages/spec/src/ui/theme.zod.ts index ea658fddca..250c7ab73d 100644 --- a/packages/spec/src/ui/theme.zod.ts +++ b/packages/spec/src/ui/theme.zod.ts @@ -221,25 +221,25 @@ export const TypographySchema = lazySchema(() => strictObject( base: z.string().optional().describe('Base font family (default: system fonts)'), heading: retiredKey( - '`theme.typography.fontFamily.heading` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — it emitted `--font-heading`, which no objectui component or stylesheet reads, so headings always rendered in the base font stack. `base` is the ONE font-family key with a live consumer (it emits `--font-sans`) and is unchanged. Delete the key; if your own CSS reads the variable, declare it under `customVars` (`{ "font-heading": "Georgia, serif" }` emits exactly the same `--font-heading`). Run `os migrate meta --from 16` to rewrite it automatically.', + '`theme.typography.fontFamily.heading` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — it emitted `--font-heading`, which no objectui component or stylesheet reads, so headings always rendered in the base font stack. `base` is the ONE font-family key with a live consumer (it emits `--font-sans`) and is unchanged. Delete the key; if your own CSS reads the variable, declare it under `customVars` (`{ "font-heading": "Georgia, serif" }` emits exactly the same `--font-heading`). Run `os migrate meta --from 16` to rewrite existing sources automatically.', ), mono: retiredKey( - '`theme.typography.fontFamily.mono` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — it emitted `--font-mono`, which no objectui component or stylesheet reads, so code always rendered in the browser default monospace. `base` is the ONE font-family key with a live consumer (it emits `--font-sans`) and is unchanged. Delete the key; if your own CSS reads the variable, declare it under `customVars` (`{ "font-mono": "ui-monospace, monospace" }` emits exactly the same `--font-mono`). Run `os migrate meta --from 16` to rewrite it automatically.', + '`theme.typography.fontFamily.mono` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — it emitted `--font-mono`, which no objectui component or stylesheet reads, so code always rendered in the browser default monospace. `base` is the ONE font-family key with a live consumer (it emits `--font-sans`) and is unchanged. Delete the key; if your own CSS reads the variable, declare it under `customVars` (`{ "font-mono": "ui-monospace, monospace" }` emits exactly the same `--font-mono`). Run `os migrate meta --from 16` to rewrite existing sources automatically.', ), }, ).optional(), fontSize: retiredKey( - '`theme.typography.fontSize` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--font-size-xs` … `--font-size-4xl` faithfully and NO first-party component or stylesheet has ever read one, so a declared type scale was real CSS that styled nothing. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "font-size-lg": "1.125rem" }` emits exactly the same `--font-size-lg`). Run `os migrate meta --from 16` to rewrite it automatically.', + '`theme.typography.fontSize` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--font-size-xs` … `--font-size-4xl` faithfully and NO first-party component or stylesheet has ever read one, so a declared type scale was real CSS that styled nothing. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "font-size-lg": "1.125rem" }` emits exactly the same `--font-size-lg`). Run `os migrate meta --from 16` to rewrite existing sources automatically.', ), fontWeight: retiredKey( - '`theme.typography.fontWeight` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--font-weight-*` and nothing read it, so text rendered at the inherited weight whatever you declared. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "font-weight-semibold": "600" }` emits exactly the same `--font-weight-semibold`). Run `os migrate meta --from 16` to rewrite it automatically.', + '`theme.typography.fontWeight` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--font-weight-*` and nothing read it, so text rendered at the inherited weight whatever you declared. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "font-weight-semibold": "600" }` emits exactly the same `--font-weight-semibold`). Run `os migrate meta --from 16` to rewrite existing sources automatically.', ), lineHeight: retiredKey( - '`theme.typography.lineHeight` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--line-height-*` and nothing read it, so every block kept its inherited leading. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "line-height-relaxed": "1.75" }` emits exactly the same `--line-height-relaxed`). Run `os migrate meta --from 16` to rewrite it automatically.', + '`theme.typography.lineHeight` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--line-height-*` and nothing read it, so every block kept its inherited leading. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "line-height-relaxed": "1.75" }` emits exactly the same `--line-height-relaxed`). Run `os migrate meta --from 16` to rewrite existing sources automatically.', ), letterSpacing: retiredKey( - '`theme.typography.letterSpacing` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--letter-spacing-*` and nothing read it, so tracking never moved. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "letter-spacing-wide": "0.025em" }` emits exactly the same `--letter-spacing-wide`). Run `os migrate meta --from 16` to rewrite it automatically.', + '`theme.typography.letterSpacing` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--letter-spacing-*` and nothing read it, so tracking never moved. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "letter-spacing-wide": "0.025em" }` emits exactly the same `--letter-spacing-wide`). Run `os migrate meta --from 16` to rewrite existing sources automatically.', ), }, )); @@ -352,10 +352,10 @@ const THEME_RETIRED_KEY_GUIDANCE: Readonly> = { * does not make the tombstone redundant. */ const THEME_ANIMATION_RETIRED = - '`theme.animation` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — unlike the #3494 props above, the engine DID emit `--duration-*` and `--timing-*`, faithfully and for years; what never existed was a reader. No first-party component or stylesheet has ever consumed one, so every transition ran at the renderer default whatever you declared. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "duration-fast": "150ms", "timing-ease_in": "cubic-bezier(0.4, 0, 1, 1)" }` emits exactly the same properties). Run `os migrate meta --from 16` to rewrite it automatically.'; + '`theme.animation` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — unlike the #3494 props above, the engine DID emit `--duration-*` and `--timing-*`, faithfully and for years; what never existed was a reader. No first-party component or stylesheet has ever consumed one, so every transition ran at the renderer default whatever you declared. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "duration-fast": "150ms", "timing-ease_in": "cubic-bezier(0.4, 0, 1, 1)" }` emits exactly the same properties). Run `os migrate meta --from 16` to rewrite existing sources automatically.'; const THEME_ZINDEX_RETIRED = - '`theme.zIndex` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--z-base` … `--z-tooltip` and nothing read one, so an overlay you "lifted" still stacked by document order. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "z-modal": "1050" }` emits exactly the same `--z-modal`). Run `os migrate meta --from 16` to rewrite it automatically.'; + '`theme.zIndex` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--z-base` … `--z-tooltip` and nothing read one, so an overlay you "lifted" still stacked by document order. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "z-modal": "1050" }` emits exactly the same `--z-modal`). Run `os migrate meta --from 16` to rewrite existing sources automatically.'; /** * Theme Configuration Schema