From 3047ef3f116ff9e53b0ca90c4fd282e42f2a4366 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 05:20:28 +0000 Subject: [PATCH 1/3] fix(spec): the protocol-17 conversion summaries and three descriptions state their decisions in words Stage 4 of the spec lane's runtime-string burn-down, class (b): the 56 toMajor-17 ADR-0087 conversion summaries (68 tracker ids) and three schema and route descriptions (3 ids) now state what each cited decision was, or drop a citation the sentence already explained. Text only: no conversion's id, surface, step, transform or order changes. Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude --- packages/spec/src/api/plugin-rest-api.zod.ts | 7 +- packages/spec/src/conversions/registry.ts | 120 +++++++++---------- packages/spec/src/data/field.zod.ts | 2 +- 3 files changed, 65 insertions(+), 64 deletions(-) diff --git a/packages/spec/src/api/plugin-rest-api.zod.ts b/packages/spec/src/api/plugin-rest-api.zod.ts index 158eb30fd9d..9e737451795 100644 --- a/packages/spec/src/api/plugin-rest-api.zod.ts +++ b/packages/spec/src/api/plugin-rest-api.zod.ts @@ -854,7 +854,7 @@ export const DEFAULT_METADATA_ROUTES: RestApiRouteRegistration = { + 'comparison: the packaged baseline, the tenant customization row, and the merged ' + 'result side by side. A DIFFERENT representation from `GET /:type/:name`, which ' + 'answers only the merged value under `item` — hence its own path and its own ' - + 'response schema (#5882). Reached until now only as `GET /:type/:name?layers=true`, ' + + 'response schema, since one route answers one shape. Reached until now only as `GET /:type/:name?layers=true`, ' + 'which still works during its deprecation window but is answered with ' + '`Deprecation` / `Link` headers pointing here.', tags: ['Metadata'], @@ -896,8 +896,9 @@ export const DEFAULT_METADATA_ROUTES: RestApiRouteRegistration = { + '`op=\'publish\'` history event. The sibling write door of `PUT /:type/:name` — the ' + 'ADR-0033 two-step spelling, where `?mode=draft` stages a body and this makes it live. ' + '404 `NO_DRAFT` when there is nothing to publish; 409 `metadata_conflict` when the ' - + 'published row advanced while the draft was held. Served since before #7294 with no ' - + 'declaration behind it — this entry is what makes its response contract nameable.', + + 'published row advanced while the draft was held. The route was served for a long time with no ' + + 'declaration behind it — this entry is what makes its response contract nameable, the same ' + + 'declared-equals-returned rule the save door follows.', tags: ['Metadata'], // No `requestSchema` (#3899): the body is optional and its only read key // is `message`, taken only when it is already a string and ignored diff --git a/packages/spec/src/conversions/registry.ts b/packages/spec/src/conversions/registry.ts index bf455aee494..87b8a85fb7b 100644 --- a/packages/spec/src/conversions/registry.ts +++ b/packages/spec/src/conversions/registry.ts @@ -724,7 +724,7 @@ const actionExecuteToTarget: MetadataConversion = { retiredFromLoadPath: true, retiredAfter: '16.1.0', surface: 'action.execute', - summary: "action key 'execute' → 'target' (the deprecated handler alias, #3713)", + summary: "action key 'execute' → 'target' (the deprecated handler alias; the spec and the renderer had resolved the pair in opposite directions, so one key now names the handler)", apply(stack, emit) { const renameOn = (action: Dict, path: string): Dict => { const renamed = renameKey(action, 'execute', 'target'); @@ -764,7 +764,7 @@ const fieldConditionalRequiredToRequiredWhen: MetadataConversion = { retiredFromLoadPath: true, retiredAfter: '16.1.0', surface: 'field.conditionalRequired', - summary: "field key 'conditionalRequired' → 'requiredWhen' (the deprecated predicate alias, #3754)", + summary: "field key 'conditionalRequired' → 'requiredWhen' (the deprecated predicate alias, folded into the canonical key so no reader picks its own precedence)", apply(stack, emit) { const withObjects = mapObjectFieldsKey(stack, 'objects', 'conditionalRequired', 'requiredWhen', emit); return mapObjectFieldsKey(withObjects, 'objectExtensions', 'conditionalRequired', 'requiredWhen', emit); @@ -819,7 +819,7 @@ const agentToolsToSkills: MetadataConversion = { retiredFromLoadPath: true, retiredAfter: '16.1.0', surface: 'agent.tools', - summary: "agent key 'tools' removed — declare capability in a skill (ADR-0064, #3894)", + summary: "agent key 'tools' removed — declare capability in a skill (ADR-0064: an agent's tools are exactly its skills' tools, and this inline slot resolved names against the whole registry with no surface check)", apply(stack, emit) { return mapCollection(stack, 'agents', (agent, path) => { if (!('tools' in agent) || agent.tools == null) return agent; @@ -878,7 +878,7 @@ const sharingRuleAccessLevelFullToEdit: MetadataConversion = { id: 'sharing-rule-access-level-full-to-edit', toMajor: 17, surface: 'sharingRule.accessLevel', - summary: "sharing-rule accessLevel 'full' → 'edit' (#3865 — `full` never granted more than `edit`)", + summary: "sharing-rule accessLevel 'full' → 'edit' (`full` never granted more than `edit`; a sharing rule grants read or edit, while delete and transfer come from object permissions and ownership)", apply(stack, emit) { return mapCollection(stack, 'sharingRules', (rule, path) => { if (rule.accessLevel !== 'full') return rule; @@ -951,7 +951,7 @@ const flowNodeCrudObjectAlias: MetadataConversion = { id: 'flow-node-crud-object-alias', toMajor: 17, surface: 'flow.node.config.objectName', - summary: "CRUD flow-node config key 'object' → 'objectName' (#3796 — `readAliasedConfig` shim graduation)", + summary: "CRUD flow-node config key 'object' → 'objectName' (the last alias in the executors' `readAliasedConfig` shim graduates into this layer, and the shim is deleted)", apply(stack, emit) { const crudTypes = new Set(['get_record', 'create_record', 'update_record', 'delete_record']); return renameFlowConfigAliases(stack, crudTypes, [['object', 'objectName']], emit); @@ -1074,8 +1074,8 @@ const flowNodeNotifyConfigAliases: MetadataConversion = { toMajor: 17, surface: 'flow.node.notify.config', summary: - "notify flow-node config keys 'to' → 'recipients', 'subject' → 'title', 'body' → 'message', 'url' → 'actionUrl' (#3796), " + - "and nested 'source: {object, id}' → 'sourceObject' / 'sourceId' (#4045)", + "notify flow-node config keys 'to' → 'recipients', 'subject' → 'title', 'body' → 'message', 'url' → 'actionUrl' (executor `??` fallbacks graduated into this layer; `actionUrl` is canonical because the notification chain downstream already uses it), " + + "and nested 'source: {object, id}' → 'sourceObject' / 'sourceId' (a shape the executor read that no config schema declared)", apply(stack, emit) { const renamed = renameFlowConfigAliases( stack, @@ -1286,7 +1286,7 @@ const flowNodeWaitEventConfigLift: MetadataConversion = { surface: 'flow.node.wait.waitEventConfig', summary: "wait flow-node loose config keys → the declared `waitEventConfig` block: 'eventType', " + - "'timerDuration'/'duration' → 'timerDuration', 'signalName'/'signal' → 'signalName', 'timeoutMs' (#4045)", + "'timerDuration'/'duration' → 'timerDuration', 'signalName'/'signal' → 'signalName', 'timeoutMs' (the executor also read these keys from the loose config, a second contract beside the declared block)", apply(stack, emit) { return liftWaitEventConfig(stack, emit); }, @@ -1361,7 +1361,7 @@ const flowNodeMapFlowAlias: MetadataConversion = { id: 'flow-node-map-flow-alias', toMajor: 17, surface: 'flow.node.map.config.flowName', - summary: "map flow-node config key 'flow' → 'flowName' (#4045 — undeclared executor fallback graduation)", + summary: "map flow-node config key 'flow' → 'flowName' (an undeclared spelling the executor accepted through a bare fallback; it graduates into this layer)", apply(stack, emit) { return renameFlowConfigAliases(stack, new Set(['map']), [['flow', 'flowName']], emit); }, @@ -1418,7 +1418,7 @@ const flowNodeSubflowFlowAlias: MetadataConversion = { id: 'flow-node-subflow-flow-alias', toMajor: 17, surface: 'flow.node.subflow.config.flowName', - summary: "subflow flow-node config key 'flow' → 'flowName' (#4278 — undeclared executor fallback graduation)", + summary: "subflow flow-node config key 'flow' → 'flowName' (an undeclared spelling the executor accepted through a bare fallback, found when the schemaless nodes were reconciled with their executors; it graduates into this layer)", apply(stack, emit) { return renameFlowConfigAliases(stack, new Set(['subflow']), [['flow', 'flowName']], emit); }, @@ -1527,7 +1527,7 @@ const flowNodeConnectorConfigLift: MetadataConversion = { surface: 'flow.node.connector_action.connectorConfig', summary: "connector_action flow-node loose config keys 'connectorId' / 'actionId' / 'input' → " + - 'the declared `connectorConfig` block (#4045)', + 'the declared `connectorConfig` block (the executor reads only that block; the published designer form had been writing these keys where nothing read them)', apply(stack, emit) { return liftConnectorConfigShape(stack, emit); }, @@ -1638,7 +1638,7 @@ const flowNodeScriptConfigAliases: MetadataConversion = { id: 'flow-node-script-config-aliases', toMajor: 17, surface: 'flow.node.script.config', - summary: "script flow-node config keys 'functionName' → 'function', 'input' → 'inputs' (#3796)", + summary: "script flow-node config keys 'functionName' → 'function', 'input' → 'inputs' (executor `??` fallbacks, graduated into this layer)", apply(stack, emit) { return renameFlowConfigAliases( stack, @@ -1739,7 +1739,7 @@ const appDeadAuthoringKeysRemoved: MetadataConversion = { 'app.version / app.aria / app.objects / app.apis / app.sharing / app.embed / ' + 'app.mobileNavigation / app.contextSelectors.includeAll / app.contextSelectors.placement / ' + 'app.homePageId / app.areas.order', - summary: "app keys 'version'/'aria'/'objects'/'apis'/'sharing'/'embed'/'mobileNavigation'/'homePageId' plus contextSelectors 'includeAll'/'placement' and areas 'order' removed (liveness audits #4001, #4509, #4667 — unread or wrongly encoded; sharing/embed declared a public surface no route enforced, mobileNavigation was fully unimplemented, includeAll was deliberately disobeyed because an 'All' row would clear a mandatory scope, homePageId WAS read by objectui's console before v17 but encoded the landing page as an ID cross-reference that silently fell back when it dangled — the landing page is the first nav item (premise corrected in #4709; the retirement stands), and no renderer ever sorted areas)", + summary: "app keys 'version'/'aria'/'objects'/'apis'/'sharing'/'embed'/'mobileNavigation'/'homePageId' plus contextSelectors 'includeAll'/'placement' and areas 'order' removed (liveness audits found each one unread or wrongly encoded; sharing/embed declared a public surface no route enforced, mobileNavigation was fully unimplemented, includeAll was deliberately disobeyed because an 'All' row would clear a mandatory scope, homePageId WAS read by objectui's console before v17 but encoded the landing page as an ID cross-reference that silently fell back when it dangled — the landing page is the first nav item (the first retirement record said nothing read it, a premise since corrected; the retirement stands), and no renderer ever sorted areas)", apply(stack, emit) { const RETIRED = [ 'version', 'aria', 'objects', 'apis', 'sharing', 'embed', 'mobileNavigation', @@ -1850,7 +1850,7 @@ const appAreaFailOpenGatesRemoved: MetadataConversion = { retiredFromLoadPath: true, retiredAfter: '16.1.0', surface: 'app.areas.visible / app.areas.requiredPermissions', - summary: "navigation-area keys 'visible'/'requiredPermissions' removed (#4651, ADR-0049 — FAIL-OPEN access gates: no layer ever read them, so a 'hidden' or permission-gated area was served and rendered to every user, while the identically named keys on a navigation ITEM and on the APP are enforced; gate the items inside the area, or gate the app)", + summary: "navigation-area keys 'visible'/'requiredPermissions' removed (ADR-0049 — FAIL-OPEN access gates: no layer ever read them, so a 'hidden' or permission-gated area was served and rendered to every user, while the identically named keys on a navigation ITEM and on the APP are enforced; gate the items inside the area, or gate the app)", apply(stack, emit) { const RETIRED_AREA_GATES = ['visible', 'requiredPermissions']; return mapCollection(stack, 'apps', (app, path) => { @@ -1952,7 +1952,7 @@ const permissionRlsPriorityRemoved: MetadataConversion = { retiredFromLoadPath: true, retiredAfter: '16.1.0', surface: 'permission.rowLevelSecurity.priority', - summary: "RLS-policy key 'priority' removed (#3896 audit — policies OR-combine, so the promised conflict-resolution semantics cannot exist; dropping it changes no outcome)", + summary: "RLS-policy key 'priority' removed (a security audit found no reader: policies OR-combine, so the promised conflict-resolution semantics cannot exist; dropping it changes no outcome)", apply(stack, emit) { return mapCollection(stack, 'permissions', (ps, path) => { const rls = (ps as { rowLevelSecurity?: unknown }).rowLevelSecurity; @@ -2021,7 +2021,7 @@ const toolInertAuthoringKeysRemoved: MetadataConversion = { retiredFromLoadPath: true, retiredAfter: '16.1.0', surface: 'tool.category / tool.permissions / tool.active / tool.builtIn', - summary: "tool keys 'category'/'permissions'/'active'/'builtIn' removed (#3896 close-out — authorable and inert; permissions gated nothing, active:false withdrew nothing)", + summary: "tool keys 'category'/'permissions'/'active'/'builtIn' removed (authorable and inert, so removed under ADR-0049 enforce-or-remove; permissions gated nothing, active:false withdrew nothing)", apply(stack, emit) { const RETIRED = ['category', 'permissions', 'active', 'builtIn'] as const; return mapCollection(stack, 'tools', (tool, path) => { @@ -2134,7 +2134,7 @@ const actionInertKeysRemoved: MetadataConversion = { retiredFromLoadPath: true, retiredAfter: '16.1.0', surface: 'action.shortcut / action.bulkEnabled', - summary: "action keys 'shortcut'/'bulkEnabled' removed (#3896 close-out — no keydown path dispatches shortcuts; the multi-select toolbar reads the view's bulkActions)", + summary: "action keys 'shortcut'/'bulkEnabled' removed (inert, removed under ADR-0049 enforce-or-remove: no keydown path dispatches shortcuts; the multi-select toolbar reads the view's bulkActions)", apply(stack, emit) { return mapCollection(stack, 'actions', (a, path) => stripKeys(a, ['shortcut', 'bulkEnabled'], emit, path)); }, @@ -2159,7 +2159,7 @@ const flowInertKeysRemoved: MetadataConversion = { retiredFromLoadPath: true, retiredAfter: '16.1.0', surface: 'flow.active / flow.template / flow.nodes[].outputSchema / flow.errorHandling.fallbackNodeId', - summary: "flow keys 'active'/'template', node 'outputSchema' and errorHandling 'fallbackNodeId' removed (#3896 close-out — active:false never stopped a flow; status is the enforced lifecycle)", + summary: "flow keys 'active'/'template', node 'outputSchema' and errorHandling 'fallbackNodeId' removed (inert, removed under ADR-0049 enforce-or-remove: active:false never stopped a flow; status is the enforced lifecycle)", apply(stack, emit) { let out = mapCollection(stack, 'flows', (f, path) => { let next = stripKeys(f, ['active', 'template'], emit, path); @@ -2218,7 +2218,7 @@ const viewInertKeysRemoved: MetadataConversion = { retiredFromLoadPath: true, retiredAfter: '16.1.0', surface: 'view.list.responsive / view.list.performance / view.form.defaultSort / view.form.aria', - summary: "view keys removed (#3896 close-out): list 'responsive'/'performance', form 'defaultSort'/'aria' — no renderer read them (list aria/data and form data stay live)", + summary: "view keys removed as inert (ADR-0049 enforce-or-remove): list 'responsive'/'performance', form 'defaultSort'/'aria' — no renderer read them (list aria/data and form data stay live)", apply(stack, emit) { const LIST_KEYS = ['responsive', 'performance'] as const; // NOT 'data': the sweep's removal attempt was refuted by the build — @@ -2264,7 +2264,7 @@ const viewListPassthroughKeysRemoved: MetadataConversion = { retiredFromLoadPath: true, retiredAfter: '16.1.0', surface: 'view.list.striped / view.list.bordered / view.list.virtualScroll', - summary: "view list keys removed (#7176): 'striped'/'bordered'/'virtualScroll' — every measured reader copied the key forward and none applied it (pass-through-only; ADR-0049 enforce-or-remove)", + summary: "view list keys removed: 'striped'/'bordered'/'virtualScroll' — every measured reader copied the key forward and none applied it (a key that is only passed through is dead in effect; ADR-0049 enforce-or-remove)", apply(stack, emit) { const LIST_KEYS = ['striped', 'bordered', 'virtualScroll'] as const; return mapViewPayloads(stack, (payload, kind, path) => @@ -2332,8 +2332,8 @@ const viewExportOptionsPdfRemoved: MetadataConversion = { retiredAfter: '16.1.0', surface: 'view.list.exportOptions / view.listViews.*.exportOptions', summary: - "list-view export format 'pdf' removed (#8010 — PDF export was declined as #1301 NOT_PLANNED; " - + 'ObjectGrid dropped the declared format from the menu with only a runtime console.warn)', + "list-view export format 'pdf' removed (PDF export was declined as not planned, and ObjectGrid " + + 'dropped the declared format from the menu with only a runtime console.warn; an honest enum replaces that warning)', apply(stack, emit) { const stripPdf = (slot: unknown, path: string): unknown => { if (!slot || typeof slot !== 'object' || Array.isArray(slot)) return slot; @@ -2401,7 +2401,7 @@ const dashboardInertKeysRemoved: MetadataConversion = { retiredFromLoadPath: true, retiredAfter: '16.1.0', surface: 'dashboard.aria / dashboard.performance / dashboard.widgets[].performance', - summary: "dashboard keys 'aria'/'performance' and widget 'performance' removed (#3896 close-out — no renderer applied any of them)", + summary: "dashboard keys 'aria'/'performance' and widget 'performance' removed (inert, removed under ADR-0049 enforce-or-remove: no renderer applied any of them)", apply(stack, emit) { return mapCollection(stack, 'dashboards', (d, path) => { let next = stripKeys(d, ['aria', 'performance'], emit, path); @@ -2471,7 +2471,7 @@ const dashboardWidgetResponsiveRemoved: MetadataConversion = { retiredFromLoadPath: true, retiredAfter: '16.1.0', surface: 'dashboard.widgets[].responsive', - summary: "dashboard widget key 'responsive' removed (#4876 — no renderer ever applied per-widget breakpoint overrides; the page.components[].responsive key this entry once deferred to was itself retired at protocol 18, #11027)", + summary: "dashboard widget key 'responsive' removed (no renderer ever applied per-widget breakpoint overrides; the page.components[].responsive key this entry once deferred to was measured equally unread and retired at protocol 18)", apply(stack, emit) { return mapCollection(stack, 'dashboards', (d, path) => { const widgets = d.widgets; @@ -2553,7 +2553,7 @@ const dashboardWidgetActionAriaRemoved: MetadataConversion = { + 'dashboard.widgets[].actionIcon / dashboard.widgets[].aria', summary: "dashboard widget keys 'actionUrl'/'actionType'/'actionIcon' and 'aria' removed " - + '(#5010 — no renderer ever drew a per-widget action button, and widget ARIA attributes never ' + + '(no renderer ever drew a per-widget action button, and widget ARIA attributes never ' + 'reached the DOM; use header.actions[] and the widget title/description)', apply(stack, emit) { return mapCollection(stack, 'dashboards', (d, path) => { @@ -2650,7 +2650,7 @@ const dashboardWidgetCompareToConverged: MetadataConversion = { surface: 'dashboard.widgets[].compareTo', summary: "dashboard widget 'compareTo' converged on the executor's { kind, dimension? } contract " - + "(#5011 — the bare strings and { offset: '1y' } rewrite mechanically; other { offset } " + + "(the shape the dataset executor implements; the bare strings and { offset: '1y' } rewrite mechanically; other { offset } " + 'durations have no faithful target and are reported, not guessed)', apply(stack, emit) { return mapCollection(stack, 'dashboards', (d, path) => { @@ -2723,7 +2723,7 @@ const agentKnowledgeRemoved: MetadataConversion = { retiredFromLoadPath: true, retiredAfter: '16.1.0', surface: 'agent.knowledge', - summary: "agent key 'knowledge' removed (#3896 close-out — declaring sources/indexes never scoped retrieval; restrict at the knowledge-service level)", + summary: "agent key 'knowledge' removed (inert, removed under ADR-0049 enforce-or-remove: declaring sources/indexes never scoped retrieval; restrict at the knowledge-service level)", apply(stack, emit) { return mapCollection(stack, 'agents', (a, path) => stripKeys(a, ['knowledge'], emit, path)); }, @@ -2741,7 +2741,7 @@ const skillTriggerPhrasesRemoved: MetadataConversion = { retiredFromLoadPath: true, retiredAfter: '16.1.0', surface: 'skill.triggerPhrases', - summary: "skill key 'triggerPhrases' removed (#3896 close-out — activation is triggerConditions + the agent's skills[] allowlist; phrases were a dead-end projection)", + summary: "skill key 'triggerPhrases' removed (inert, removed under ADR-0049 enforce-or-remove: activation is triggerConditions + the agent's skills[] allowlist; phrases were a dead-end projection)", apply(stack, emit) { return mapCollection(stack, 'skills', (sk, path) => stripKeys(sk, ['triggerPhrases'], emit, path)); }, @@ -2779,7 +2779,7 @@ const stackApiRequireAuthRemoved: MetadataConversion = { retiredFromLoadPath: true, retiredAfter: '16.1.0', surface: 'stack.api.requireAuth', - summary: "stack key 'api.requireAuth' removed — anonymous access is always denied; publish public surfaces by declaration (#3963)", + summary: "stack key 'api.requireAuth' removed — anonymous access is always denied; publish public surfaces by declaration (a public form, a share link or `book.audience: 'public'`), which replaced the deployment-wide opt-out", apply(stack, emit) { const api = stack.api; if (!isDict(api) || !('requireAuth' in api)) return stack; @@ -2862,7 +2862,7 @@ const flowNodeWaitTimeoutKeysRemoved: MetadataConversion = { surface: 'flow.node.waitEventConfig', summary: "waitEventConfig keys 'timeoutMs' (→ 'timerDuration', stringified — its only reader used it as the duration) " + - "and 'onTimeout' (removed — zero readers, so no timeout ever fired) (#4158)", + "and 'onTimeout' (removed — zero readers, so no timeout ever fired): wait never had a timeout, so its timeout contract is withdrawn rather than built", apply(stack, emit) { return removeWaitTimeoutKeys(stack, emit); }, @@ -2941,7 +2941,7 @@ const datasourceInertBlocksRemoved: MetadataConversion = { retiredFromLoadPath: true, retiredAfter: '16.1.0', surface: 'datasource.retryPolicy / datasource.healthCheck / datasource.external.label / datasource.external.requirePermission', - summary: "datasource keys 'retryPolicy'/'healthCheck' and external 'label'/'requirePermission' removed (#4583 — nothing retried, nothing probed on a schedule, and the federation label/permission were read by nobody)", + summary: "datasource keys 'retryPolicy'/'healthCheck' and external 'label'/'requirePermission' removed (nothing retried, nothing probed on a schedule, and the federation label/permission were read by nobody; each of those jobs already has a live mechanism)", apply(stack, emit) { return mapCollection(stack, 'datasources', (ds, path) => { const next = stripKeys(ds, ['retryPolicy', 'healthCheck'], emit, path); @@ -3029,7 +3029,7 @@ const mappingInertKeysRemoved: MetadataConversion = { retiredFromLoadPath: true, retiredAfter: '16.1.0', surface: 'mapping.extractQuery / mapping.errorPolicy / mapping.batchSize', - summary: "mapping keys 'extractQuery'/'errorPolicy'/'batchSize' removed (#4509 — no exporter reads a mapping, error handling belongs to the import request, and the write path sizes its own batches)", + summary: "mapping keys 'extractQuery'/'errorPolicy'/'batchSize' removed (no exporter reads a mapping, error handling belongs to the import request, and the write path sizes its own batches)", apply(stack, emit) { const RETIRED = ['extractQuery', 'errorPolicy', 'batchSize']; // Scoped to the `mappings` collection deliberately: `batchSize` is live on @@ -3084,7 +3084,7 @@ const bookTranslationsRemoved: MetadataConversion = { retiredFromLoadPath: true, retiredAfter: '16.1.0', surface: 'book.translations / book.groups.translations', - summary: "book keys 'translations' (book-level and group-level) removed (#4667 — no resolver read them; the tree endpoint and portal render labels verbatim, so a localized book served its authoring locale to everyone). Localize the docs instead: `doc.translations` is live", + summary: "book keys 'translations' (book-level and group-level) removed (no resolver read them; the tree endpoint and portal render labels verbatim, so a localized book served its authoring locale to everyone). Localize the docs instead: `doc.translations` is live", apply(stack, emit) { return mapCollection(stack, 'books', (book, path) => { const next = stripKeys(book, ['translations'], emit, path); @@ -3146,7 +3146,7 @@ const jobIdRemoved: MetadataConversion = { retiredFromLoadPath: true, retiredAfter: '16.1.0', surface: 'job.id', - summary: "job key 'id' removed (#4667 — nothing read it; `name` is the job's identity everywhere, so two jobs differing only in `id` were the same job, and the key's own description advertised an override that did not exist)", + summary: "job key 'id' removed (nothing read it; `name` is the job's identity everywhere, so two jobs differing only in `id` were the same job, and the key's own description advertised an override that did not exist)", apply(stack, emit) { return mapCollection(stack, 'jobs', (job, path) => stripKeys(job, ['id'], emit, path)); }, @@ -3203,7 +3203,7 @@ const translationValidationMessagesRemoved: MetadataConversion = { retiredFromLoadPath: true, retiredAfter: '16.1.0', surface: 'translation.validationMessages', - summary: "translation key 'validationMessages' removed (#4667 — no resolver read it, so a translated rule message was stored and never shown; #3778's migration table had been steering retired `errors:` authors into it). Author the message on the rule itself (`object.validations[].message`), and translate it under the object-scoped group `objects.._validations..message`, which the write path resolves (17.3.0, #14381)", + summary: "translation key 'validationMessages' removed (no resolver read it, so a translated rule message was stored and never shown; the legacy-key table of the translation-bundle migration had been steering retired `errors:` authors into it). Author the message on the rule itself (`object.validations[].message`), and translate it under the object-scoped group `objects.._validations..message`, which the write path resolves (17.3.0, a translation key shipped together with its reader)", apply(stack, emit) { return mapCollection(stack, 'translations', (t, path) => stripKeys(t, ['validationMessages'], emit, path)); @@ -3261,7 +3261,7 @@ const datasourceCapabilitiesRemoved: MetadataConversion = { retiredFromLoadPath: true, retiredAfter: '16.1.0', surface: 'datasource.capabilities', - summary: "datasource key 'capabilities' removed (#4583 — eleven flags no code read; pushdown comes from the driver's own supports.*, and `readOnly` never made anything read-only)", + summary: "datasource key 'capabilities' removed (eleven flags no code read; pushdown comes from the driver's own supports.*, and `readOnly` never made anything read-only)", apply(stack, emit) { return mapCollection(stack, 'datasources', (ds, path) => stripKeys(ds, ['capabilities'], emit, path)); }, @@ -3316,7 +3316,7 @@ const datasourceReadReplicasRemoved: MetadataConversion = { retiredFromLoadPath: true, retiredAfter: '16.1.0', surface: 'datasource.readReplicas', - summary: "datasource key 'readReplicas' removed (#4468 — no driver opened a replica connection and no query path splits reads from writes; front replicas behind one endpoint and point `config` at it)", + summary: "datasource key 'readReplicas' removed (no driver opened a replica connection and no query path splits reads from writes; front replicas behind one endpoint and point `config` at it)", apply(stack, emit) { return mapCollection(stack, 'datasources', (ds, path) => stripKeys(ds, ['readReplicas'], emit, path)); }, @@ -3418,7 +3418,7 @@ const datasourceConfigDriverKeyAliases: MetadataConversion = { summary: "datasource config keys → canonical per driver: sqlite 'file'/'database' → 'filename', " + "postgres/mysql 'connectionString' → 'url' and 'user' → 'username', mongo 'uri' → 'url' " - + "and 'user' → 'username' (#4456 — driver-factory `??` fallback graduation)", + + "and 'user' → 'username' (undeclared driver-factory `??` fallbacks, graduated into this layer and deleted from the reader)", apply(stack, emit) { return mapDatasources(stack, (ds, path) => { const kind = resolveDriverId(ds.driver); @@ -3664,7 +3664,7 @@ const flowNodeScriptBranchKeysRemoved: MetadataConversion = { summary: "script flow-node config keys 'actionType' (→ 'function' when it was shorthand for one; otherwise removed — " + "'email'/'slack' were logger-backed stubs that delivered nothing), plus 'template' / 'recipients' / " - + "'variables' (fed those stubs) and 'script' (inline JS the runtime never executed) (#4343)", + + "'variables' (fed those stubs) and 'script' (inline JS the runtime never executed); script is now a pure function-call node, the only path that ran real logic", apply(stack, emit) { return removeScriptBranchKeys(stack, emit); }, @@ -3759,7 +3759,7 @@ const objectManagedBySystemToSystemData: MetadataConversion = { retiredAfter: '16.1.0', surface: 'object.managedBy', summary: - "object managedBy 'system' → 'system-data' (#3355 — ADR-0103's residual bucket named the " + "object managedBy 'system' → 'system-data' (ADR-0103's residual bucket named the " + 'engine-owned half v16 had already moved out to `engine-owned`; the rename leaves the ' + 'name describing what the bucket actually holds: admin/user-writable platform data)', apply(stack, emit) { @@ -3826,7 +3826,7 @@ const objectEnableTrashMruRemoved: MetadataConversion = { retiredAfter: '16.1.0', surface: 'object.enable.trash / object.enable.mru', summary: - "object capability flags 'enable.trash'/'enable.mru' removed (#3207, #2377 close-out — no " + "object capability flags 'enable.trash'/'enable.mru' removed (the last slice of the dead author-facing property removals: no " + 'recycle bin and no MRU tracking ever ran; both default-true flags gated nothing)', apply(stack, emit) { return mapCollection(stack, 'objects', (obj, path) => { @@ -3909,7 +3909,7 @@ const objectIndexTypePartialRemoved: MetadataConversion = { retiredAfter: '16.1.0', surface: 'object.indexes[].type / object.indexes[].partial', summary: - "object index keys 'indexes[].type'/'indexes[].partial' removed (#5248, #4943 — no driver " + "object index keys 'indexes[].type'/'indexes[].partial' removed (no driver " + 'ever read either: the index method is the dialect\'s choice and a partial index is built ' + 'by a database-layer migration, not declared)', apply(stack, emit) { @@ -4068,7 +4068,7 @@ const retryPolicyConverged: MetadataConversion = { surface: 'flow.errorHandling.retryDelayMs / flow.node.config.retry.retryDelayMs / job.retryPolicy.maxRetries / job.retryPolicy.backoffMultiplier', summary: "retry policy unified across job.retryPolicy, try_catch retry and flow.errorHandling: base delay 'retryDelayMs' → 'backoffMs', " + - "and the pre-17 job defaults (maxRetries 3, backoffMultiplier 2) written out explicitly now that the merged default is 0 / 1 (#4661, #4964)", + "and the pre-17 job defaults (maxRetries 3, backoffMultiplier 2) written out explicitly now that the merged default is 0 / 1: two declarations that differed only by accident became one, and retry is opt-in because a retry replays whatever the attempt already did", apply(stack, emit) { // ── 0. flows: errorHandling.retryDelayMs → errorHandling.backoffMs ─ // @@ -4246,7 +4246,7 @@ const hookBodyCryptoHashRemoved: MetadataConversion = { retiredAfter: '16.1.0', surface: 'hook.body.capabilities / action.body.capabilities', summary: - "script-body capability token 'crypto.hash' removed (#4391 — the sandbox never installed " + "script-body capability token 'crypto.hash' removed (the sandbox never installed " + 'ctx.crypto.hash, so the token granted a call that always threw; the CLI inferred it too)', apply(stack, emit) { const stripToken = (item: Dict, path: string): Dict => { @@ -4416,7 +4416,7 @@ const datasetMeasureAggRemoved: MetadataConversion = { retiredAfter: '16.1.0', surface: 'dataset.measures[].aggregate', summary: - "dataset measure aggregates 'array_agg' / 'string_agg' removed (#6188 — no SQL backend " + "dataset measure aggregates 'array_agg' / 'string_agg' removed (no SQL backend " + 'compiled them and the v1 dataset runtime refused them by name, so a measure declaring ' + 'one never produced a value; the measure is dropped, and with it any derived measure ' + 'left referencing it)', @@ -4541,7 +4541,7 @@ const connectorRateLimitConfigRemoved: MetadataConversion = { retiredAfter: '16.1.0', surface: 'connector.rateLimitConfig', summary: - "connector key 'rateLimitConfig' removed (#4911 — no outbound rate-limiting engine exists; " + "connector key 'rateLimitConfig' removed (no outbound rate-limiting engine exists; " + "the runtime's only token bucket limits INBOUND requests, so every knob here was inert " + 'while reading like a configured cap. The whole ConnectorRateLimitConfig shape went with it)', apply(stack, emit) { @@ -4666,10 +4666,10 @@ const fieldMappingTransformRemoved: MetadataConversion = { retiredAfter: '16.1.0', surface: 'connector.fieldMappings[].transform / externalLookup.fieldMappings[].transform', summary: - "field-mapping key 'transform' removed (#5552 — the whole five-member " + "field-mapping key 'transform' removed (the whole five-member " + 'FieldMappingTransform union went with it: no runtime ever executed constant/cast/' - + 'lookup/javascript/map, and the javascript member advertised dialect="js", retired ' - + "in #3278. The enforced transform pipeline is the import mapping's string-enum " + + 'lookup/javascript/map, and the javascript member advertised dialect="js", a dialect ' + + "already retired because JavaScript belongs in a script body. The enforced transform pipeline is the import mapping's string-enum " + '`mapping.fieldMapping[].transform`, which is unaffected)', apply(stack, emit) { return mapCollection(stack, 'connectors', (c, path) => { @@ -4785,7 +4785,7 @@ const themeInertTokenScalesRemoved: MetadataConversion = { summary: "theme keys 'typography.fontSize'/'fontWeight'/'lineHeight'/'letterSpacing', " + "'typography.fontFamily.heading'/'mono', 'animation' and 'zIndex' removed " - + '(#5021, ADR-0049 — the engine emitted --font-size-*, --font-weight-*, --line-height-*, ' + + '(ADR-0049 — the engine emitted --font-size-*, --font-weight-*, --line-height-*, ' + '--letter-spacing-*, --duration-*, --timing-*, --z-*, --font-heading and --font-mono ' + 'faithfully, and no first-party component or stylesheet has ever read one. ' + 'Re-declare any variable you actually consume under customVars, which emits it verbatim)', @@ -4937,7 +4937,7 @@ const pageHeaderSubtitleAlias: MetadataConversion = { toMajor: 17, surface: 'page.component.page-header.description', summary: - "page-header component prop 'description' → 'subtitle' (objectui#3226 — the `subtitle ?? description` fallback retires)", + "page-header component prop 'description' → 'subtitle' (the off-spec spelling a renderer tolerated through a bare `subtitle ?? description` fallback; `subtitle` is the declared key, and the fallback retires)", apply(stack, emit) { return mapPageComponents(stack, (component, path) => { const type = component.type; @@ -5162,7 +5162,7 @@ const recordPickerDisplayFieldToLabelField: MetadataConversion = { retiredAfter: '16.1.0', surface: 'page.component.element:record_picker.displayField', summary: - "record-picker component prop 'displayField' → 'labelField' (#5775 — the required key no renderer read; `labelField ?? 'name'` is what renders the row)", + "record-picker component prop 'displayField' → 'labelField' (the required key no renderer read; `labelField ?? 'name'` is what renders the row, so the delivered spelling became the declared one)", apply(stack, emit) { return mapPageComponents(stack, (component, path) => { if (component.type !== RECORD_PICKER_COMPONENT_TYPE) return component; @@ -5284,7 +5284,7 @@ const recordPickerInertKeysRemoved: MetadataConversion = { retiredAfter: '16.1.0', surface: 'page.component.element:record_picker.searchFields / page.component.element:record_picker.multiple', summary: - "record-picker component props 'searchFields'/'multiple' removed (#5775 — the control is a plain single-select with no search box; neither key had a reader)", + "record-picker component props 'searchFields'/'multiple' removed (the control is a plain single-select with no search box; neither key had a reader)", apply(stack, emit) { return mapPageComponents(stack, (component, path) => { if (component.type !== RECORD_PICKER_COMPONENT_TYPE) return component; @@ -5411,7 +5411,7 @@ const pageCardBodyToChildren: MetadataConversion = { retiredAfter: '16.1.0', surface: 'page.component.page:card.body', summary: - "page:card component prop 'body' → 'children' (#5775 — one composition key across every container; the card renderer already reads both)", + "page:card component prop 'body' → 'children' (one composition key across every container; the card renderer already reads both)", apply(stack, emit) { return mapPageComponents(stack, (component, path) => { if (component.type !== 'page:card') return component; @@ -5579,7 +5579,7 @@ const inlineActionApiParamsToBodyExtra: MetadataConversion = { toMajor: 17, surface: 'page.component.element:button.action.params', summary: - "inline type:'api' action prop 'params' (object form) → 'bodyExtra' (#5777 — the payload gets its own key; `params` stays the ActionParam[] definition array)", + "inline type:'api' action prop 'params' (object form) → 'bodyExtra' (a static payload and a parameter definition are two things, so the payload gets its own key; `params` stays the ActionParam[] definition array)", apply(stack, emit) { return mapPageComponents(stack, (component, path) => { if (component.type !== 'element:button') return component; @@ -5844,7 +5844,7 @@ const pageTabsTypeToTabStyle: MetadataConversion = { retiredAfter: '16.1.0', surface: 'page.component.page:tabs.type', summary: - "page:tabs component prop 'type' → 'tabStyle' (#6776 — a props key named `type` collides with the node's dispatch key and is unauthorable in flat/JSX carriers; `tabStyle` is the spelling the renderer reads in all of them)", + "page:tabs component prop 'type' → 'tabStyle' (a props key named `type` collides with the node's dispatch key and is unauthorable in flat/JSX carriers; `tabStyle` is the spelling the renderer reads in all of them)", apply(stack, emit) { return mapPageComponents(stack, (component, path) => { if (component.type !== 'page:tabs') return component; @@ -6032,7 +6032,7 @@ const pageStructureInertKeysRemoved: MetadataConversion = { retiredAfter: '16.1.0', surface: 'page.component.page:header.icon / page.component.page:card.actions', summary: - "page:header prop 'icon' and page:card prop 'actions' removed (#6946 — neither has a renderer " + "page:header prop 'icon' and page:card prop 'actions' removed (neither has a renderer " + 'read point in objectui; the header resolves icons per action and the card renders ' + 'title/children/footer only)', apply(stack, emit) { @@ -6198,7 +6198,7 @@ const recordDetailsLayoutRemoved: MetadataConversion = { retiredAfter: '16.1.0', surface: 'page.component.record:details.layout', summary: - "record:details component prop 'layout' removed (#6946 — the declared auto|custom modes were " + "record:details component prop 'layout' removed (the declared auto|custom modes were " + 'never implemented; the renderer branches only on inline|compact, values the schema never ' + 'permitted, so both legal values selected nothing)', apply(stack, emit) { @@ -6340,7 +6340,7 @@ const appHiddenToUnpublished: MetadataConversion = { retiredAfter: '16.1.0', surface: 'app.hidden', summary: - "stored app publish gate 'hidden' → '_unpublished' (#4829, ADR-0045 amended — `hidden` carried BOTH the publish gate and 'keep out of the App Switcher', so the built-in Account app was withheld from every non-builder; the gate is now the machine-managed `_unpublished`, and `hidden` is navigation presentation only, never an access gate. Stored rows only — an authored `hidden: true` is left untouched)", + "stored app publish gate 'hidden' → '_unpublished' (ADR-0045 amended — `hidden` carried BOTH the publish gate and 'keep out of the App Switcher', so the built-in Account app was withheld from every non-builder; the gate is now the machine-managed `_unpublished`, and `hidden` is navigation presentation only, never an access gate. Stored rows only — an authored `hidden: true` is left untouched)", apply(stack, emit) { return mapCollection(stack, 'apps', (app, path) => { if (app.hidden !== true) return app; @@ -6453,7 +6453,7 @@ const actionGlobalNavLocationRemoved: MetadataConversion = { retiredAfter: '16.1.0', surface: 'action.locations[]', summary: - "action location 'global_nav' removed (#6888 — no running-app surface rendered it; the ⌘K " + "action location 'global_nav' removed (no running-app surface rendered it; the ⌘K " + 'palette reads no action metadata, while the Studio designer previewed a command-palette ' + 'frame for it. The value is stripped and the key kept, so an action left with no location ' + 'becomes the documented headless shape `locations: []`)', diff --git a/packages/spec/src/data/field.zod.ts b/packages/spec/src/data/field.zod.ts index ea9313c7687..17002393fb6 100644 --- a/packages/spec/src/data/field.zod.ts +++ b/packages/spec/src/data/field.zod.ts @@ -2052,7 +2052,7 @@ export const FieldSchema = lazySchema(() => { * on `autonumber`, so no other type's parse output moves. */ autonumberFormat: z.string().optional().meta({ - description: 'Auto-number format: literal text + {0000} counter, {YYYY}/{MM}/{DD}/{YYYYMMDD} date tokens (business tz), and {field_name} interpolation. Counter resets per rendered prefix (e.g. AD{YYYYMMDD}{0000} resets daily). Omitted on an `autonumber` field ⇒ the contract default `{0000}` (#6555).', + description: 'Auto-number format: literal text + {0000} counter, {YYYY}/{MM}/{DD}/{YYYYMMDD} date tokens (business tz), and {field_name} interpolation. Counter resets per rendered prefix (e.g. AD{YYYYMMDD}{0000} resets daily). Omitted on an `autonumber` field ⇒ the contract default `{0000}`, which every driver and the engine fallback read, so one field numbers alike on every backend.', default: DEFAULT_AUTONUMBER_FORMAT, }), // `index` (field-level bool) removed in the 16.x line (#2377, ADR-0049): the From 8c9513fd12be99fa39e7ba1800fb08102c146dbc Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 05:26:59 +0000 Subject: [PATCH 2/3] chore(spec): regenerate the upgrade guide, spec-changes and field references from the reworded text check:generated --fix regenerated exactly the three artifacts it proved stale: docs/protocol-upgrade-guide.md, packages/spec/spec-changes.json and the autonumberFormat rows of the generated reference pages. Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude --- content/docs/references/data/field.mdx | 2 +- content/docs/references/data/object.mdx | 4 +- content/docs/references/system/migration.mdx | 4 +- docs/protocol-upgrade-guide.md | 112 +++++----- packages/spec/spec-changes.json | 224 +++++++++---------- 5 files changed, 173 insertions(+), 173 deletions(-) diff --git a/content/docs/references/data/field.mdx b/content/docs/references/data/field.mdx index 17cfbf52eb8..82013aa975c 100644 --- a/content/docs/references/data/field.mdx +++ b/content/docs/references/data/field.mdx @@ -118,7 +118,7 @@ const result = CurrencyConfigSchema.parse(data); | **sortable** | `boolean` | optional (default: `true`) | Whether field is sortable in list views | | **inlineHelpText** | `string` | optional | Help text displayed below the field in forms | | **placeholder** | `string` | optional | Placeholder text rendered inside the empty input (the HTML placeholder attribute); disappears once a value is entered. Distinct from `inlineHelpText` (always-visible help rendered beside/under the input) and `description` (tooltip/developer documentation). | -| **autonumberFormat** | `string` | optional (default: `"{0000}"`) | Auto-number format: literal text + `{0000}` counter, `{YYYY}`/`{MM}`/`{DD}`/`{YYYYMMDD}` date tokens (business tz), and `{field_name}` interpolation. Counter resets per rendered prefix (e.g. AD`{YYYYMMDD}``{0000}` resets daily). Omitted on an `autonumber` field ⇒ the contract default `{0000}` (#6555). | +| **autonumberFormat** | `string` | optional (default: `"{0000}"`) | Auto-number format: literal text + `{0000}` counter, `{YYYY}`/`{MM}`/`{DD}`/`{YYYYMMDD}` date tokens (business tz), and `{field_name}` interpolation. Counter resets per rendered prefix (e.g. AD`{YYYYMMDD}``{0000}` resets daily). Omitted on an `autonumber` field ⇒ the contract default `{0000}`, which every driver and the engine fallback read, so one field numbers alike on every backend. | | **externalId** | `boolean` | optional (default: `false`) | Is external ID for upsert operations | | **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | | **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. | diff --git a/content/docs/references/data/object.mdx b/content/docs/references/data/object.mdx index 9d06ef8a500..3b6bd03d616 100644 --- a/content/docs/references/data/object.mdx +++ b/content/docs/references/data/object.mdx @@ -282,7 +282,7 @@ const result = ApiMethod.parse(data); | **sortable** | `boolean` | optional (default: `true`) | Whether field is sortable in list views | | **inlineHelpText** | `string` | optional | Help text displayed below the field in forms | | **placeholder** | `string` | optional | Placeholder text rendered inside the empty input (the HTML placeholder attribute); disappears once a value is entered. Distinct from `inlineHelpText` (always-visible help rendered beside/under the input) and `description` (tooltip/developer documentation). | -| **autonumberFormat** | `string` | optional (default: `"{0000}"`) | Auto-number format: literal text + `{0000}` counter, `{YYYY}`/`{MM}`/`{DD}`/`{YYYYMMDD}` date tokens (business tz), and `{field_name}` interpolation. Counter resets per rendered prefix (e.g. AD`{YYYYMMDD}``{0000}` resets daily). Omitted on an `autonumber` field ⇒ the contract default `{0000}` (#6555). | +| **autonumberFormat** | `string` | optional (default: `"{0000}"`) | Auto-number format: literal text + `{0000}` counter, `{YYYY}`/`{MM}`/`{DD}`/`{YYYYMMDD}` date tokens (business tz), and `{field_name}` interpolation. Counter resets per rendered prefix (e.g. AD`{YYYYMMDD}``{0000}` resets daily). Omitted on an `autonumber` field ⇒ the contract default `{0000}`, which every driver and the engine fallback read, so one field numbers alike on every backend. | | **externalId** | `boolean` | optional (default: `false`) | Is external ID for upsert operations | | **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | | **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. | @@ -616,7 +616,7 @@ const result = ApiMethod.parse(data); | **sortable** | `boolean` | optional (default: `true`) | Whether field is sortable in list views | | **inlineHelpText** | `string` | optional | Help text displayed below the field in forms | | **placeholder** | `string` | optional | Placeholder text rendered inside the empty input (the HTML placeholder attribute); disappears once a value is entered. Distinct from `inlineHelpText` (always-visible help rendered beside/under the input) and `description` (tooltip/developer documentation). | -| **autonumberFormat** | `string` | optional (default: `"{0000}"`) | Auto-number format: literal text + `{0000}` counter, `{YYYY}`/`{MM}`/`{DD}`/`{YYYYMMDD}` date tokens (business tz), and `{field_name}` interpolation. Counter resets per rendered prefix (e.g. AD`{YYYYMMDD}``{0000}` resets daily). Omitted on an `autonumber` field ⇒ the contract default `{0000}` (#6555). | +| **autonumberFormat** | `string` | optional (default: `"{0000}"`) | Auto-number format: literal text + `{0000}` counter, `{YYYY}`/`{MM}`/`{DD}`/`{YYYYMMDD}` date tokens (business tz), and `{field_name}` interpolation. Counter resets per rendered prefix (e.g. AD`{YYYYMMDD}``{0000}` resets daily). Omitted on an `autonumber` field ⇒ the contract default `{0000}`, which every driver and the engine fallback read, so one field numbers alike on every backend. | | **externalId** | `boolean` | optional (default: `false`) | Is external ID for upsert operations | | **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | | **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. | diff --git a/content/docs/references/system/migration.mdx b/content/docs/references/system/migration.mdx index 7f3f105d69e..3b7e1dd01a2 100644 --- a/content/docs/references/system/migration.mdx +++ b/content/docs/references/system/migration.mdx @@ -119,7 +119,7 @@ Add a new field to an existing object | **sortable** | `boolean` | optional (default: `true`) | Whether field is sortable in list views | | **inlineHelpText** | `string` | optional | Help text displayed below the field in forms | | **placeholder** | `string` | optional | Placeholder text rendered inside the empty input (the HTML placeholder attribute); disappears once a value is entered. Distinct from `inlineHelpText` (always-visible help rendered beside/under the input) and `description` (tooltip/developer documentation). | -| **autonumberFormat** | `string` | optional (default: `"{0000}"`) | Auto-number format: literal text + `{0000}` counter, `{YYYY}`/`{MM}`/`{DD}`/`{YYYYMMDD}` date tokens (business tz), and `{field_name}` interpolation. Counter resets per rendered prefix (e.g. AD`{YYYYMMDD}``{0000}` resets daily). Omitted on an `autonumber` field ⇒ the contract default `{0000}` (#6555). | +| **autonumberFormat** | `string` | optional (default: `"{0000}"`) | Auto-number format: literal text + `{0000}` counter, `{YYYY}`/`{MM}`/`{DD}`/`{YYYYMMDD}` date tokens (business tz), and `{field_name}` interpolation. Counter resets per rendered prefix (e.g. AD`{YYYYMMDD}``{0000}` resets daily). Omitted on an `autonumber` field ⇒ the contract default `{0000}`, which every driver and the engine fallback read, so one field numbers alike on every backend. | | **externalId** | `boolean` | optional (default: `false`) | Is external ID for upsert operations | | **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | | **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. | @@ -541,7 +541,7 @@ Add a new field to an existing object | **sortable** | `boolean` | optional (default: `true`) | Whether field is sortable in list views | | **inlineHelpText** | `string` | optional | Help text displayed below the field in forms | | **placeholder** | `string` | optional | Placeholder text rendered inside the empty input (the HTML placeholder attribute); disappears once a value is entered. Distinct from `inlineHelpText` (always-visible help rendered beside/under the input) and `description` (tooltip/developer documentation). | -| **autonumberFormat** | `string` | optional (default: `"{0000}"`) | Auto-number format: literal text + `{0000}` counter, `{YYYY}`/`{MM}`/`{DD}`/`{YYYYMMDD}` date tokens (business tz), and `{field_name}` interpolation. Counter resets per rendered prefix (e.g. AD`{YYYYMMDD}``{0000}` resets daily). Omitted on an `autonumber` field ⇒ the contract default `{0000}` (#6555). | +| **autonumberFormat** | `string` | optional (default: `"{0000}"`) | Auto-number format: literal text + `{0000}` counter, `{YYYY}`/`{MM}`/`{DD}`/`{YYYYMMDD}` date tokens (business tz), and `{field_name}` interpolation. Counter resets per rendered prefix (e.g. AD`{YYYYMMDD}``{0000}` resets daily). Omitted on an `autonumber` field ⇒ the contract default `{0000}`, which every driver and the engine fallback read, so one field numbers alike on every backend. | | **externalId** | `boolean` | optional (default: `false`) | Is external ID for upsert operations | | **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | | **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. | diff --git a/docs/protocol-upgrade-guide.md b/docs/protocol-upgrade-guide.md index 2a819dfd101..945b3c37c5a 100644 --- a/docs/protocol-upgrade-guide.md +++ b/docs/protocol-upgrade-guide.md @@ -149,63 +149,63 @@ Finally it removes the 'pdf' member of `view.exportOptions` formats (#8010, main | Conversion | Surface | Change | Load window | |---|---|---|---| -| `action-execute-to-target` | `action.execute` | action key 'execute' → 'target' (the deprecated handler alias, #3713) | retired — `migrate meta` only | -| `field-conditionalRequired-to-requiredWhen` | `field.conditionalRequired` | field key 'conditionalRequired' → 'requiredWhen' (the deprecated predicate alias, #3754) | retired — `migrate meta` only | -| `agent-tools-to-skills` | `agent.tools` | agent key 'tools' removed — declare capability in a skill (ADR-0064, #3894) | retired — `migrate meta` only | -| `sharing-rule-access-level-full-to-edit` | `sharingRule.accessLevel` | sharing-rule accessLevel 'full' → 'edit' (#3865 — `full` never granted more than `edit`) | live — protocol 17 loader accepts the old shape | -| `flow-node-crud-object-alias` | `flow.node.config.objectName` | CRUD flow-node config key 'object' → 'objectName' (#3796 — `readAliasedConfig` shim graduation) | live — protocol 17 loader accepts the old shape | -| `flow-node-notify-config-aliases` | `flow.node.notify.config` | notify flow-node config keys 'to' → 'recipients', 'subject' → 'title', 'body' → 'message', 'url' → 'actionUrl' (#3796), and nested 'source: {object, id}' → 'sourceObject' / 'sourceId' (#4045) | live — protocol 17 loader accepts the old shape | -| `flow-node-wait-event-config-lift` | `flow.node.wait.waitEventConfig` | wait flow-node loose config keys → the declared `waitEventConfig` block: 'eventType', 'timerDuration'/'duration' → 'timerDuration', 'signalName'/'signal' → 'signalName', 'timeoutMs' (#4045) | live — protocol 17 loader accepts the old shape | -| `flow-node-connector-config-lift` | `flow.node.connector_action.connectorConfig` | connector_action flow-node loose config keys 'connectorId' / 'actionId' / 'input' → the declared `connectorConfig` block (#4045) | live — protocol 17 loader accepts the old shape | -| `flow-node-map-flow-alias` | `flow.node.map.config.flowName` | map flow-node config key 'flow' → 'flowName' (#4045 — undeclared executor fallback graduation) | live — protocol 17 loader accepts the old shape | -| `flow-node-subflow-flow-alias` | `flow.node.subflow.config.flowName` | subflow flow-node config key 'flow' → 'flowName' (#4278 — undeclared executor fallback graduation) | live — protocol 17 loader accepts the old shape | -| `flow-node-script-config-aliases` | `flow.node.script.config` | script flow-node config keys 'functionName' → 'function', 'input' → 'inputs' (#3796) | live — protocol 17 loader accepts the old shape | -| `permission-rls-priority-removed` | `permission.rowLevelSecurity.priority` | RLS-policy key 'priority' removed (#3896 audit — policies OR-combine, so the promised conflict-resolution semantics cannot exist; dropping it changes no outcome) | retired — `migrate meta` only | -| `tool-inert-authoring-keys-removed` | `tool.category / tool.permissions / tool.active / tool.builtIn` | tool keys 'category'/'permissions'/'active'/'builtIn' removed (#3896 close-out — authorable and inert; permissions gated nothing, active:false withdrew nothing) | retired — `migrate meta` only | -| `app-dead-authoring-keys-removed` | `app.version / app.aria / app.objects / app.apis / app.sharing / app.embed / app.mobileNavigation / app.contextSelectors.includeAll / app.contextSelectors.placement / app.homePageId / app.areas.order` | app keys 'version'/'aria'/'objects'/'apis'/'sharing'/'embed'/'mobileNavigation'/'homePageId' plus contextSelectors 'includeAll'/'placement' and areas 'order' removed (liveness audits #4001, #4509, #4667 — unread or wrongly encoded; sharing/embed declared a public surface no route enforced, mobileNavigation was fully unimplemented, includeAll was deliberately disobeyed because an 'All' row would clear a mandatory scope, homePageId WAS read by objectui's console before v17 but encoded the landing page as an ID cross-reference that silently fell back when it dangled — the landing page is the first nav item (premise corrected in #4709; the retirement stands), and no renderer ever sorted areas) | retired — `migrate meta` only | -| `app-area-fail-open-gates-removed` | `app.areas.visible / app.areas.requiredPermissions` | navigation-area keys 'visible'/'requiredPermissions' removed (#4651, ADR-0049 — FAIL-OPEN access gates: no layer ever read them, so a 'hidden' or permission-gated area was served and rendered to every user, while the identically named keys on a navigation ITEM and on the APP are enforced; gate the items inside the area, or gate the app) | retired — `migrate meta` only | -| `action-inert-keys-removed` | `action.shortcut / action.bulkEnabled` | action keys 'shortcut'/'bulkEnabled' removed (#3896 close-out — no keydown path dispatches shortcuts; the multi-select toolbar reads the view's bulkActions) | retired — `migrate meta` only | -| `flow-inert-keys-removed` | `flow.active / flow.template / flow.nodes[].outputSchema / flow.errorHandling.fallbackNodeId` | flow keys 'active'/'template', node 'outputSchema' and errorHandling 'fallbackNodeId' removed (#3896 close-out — active:false never stopped a flow; status is the enforced lifecycle) | retired — `migrate meta` only | -| `view-inert-keys-removed` | `view.list.responsive / view.list.performance / view.form.defaultSort / view.form.aria` | view keys removed (#3896 close-out): list 'responsive'/'performance', form 'defaultSort'/'aria' — no renderer read them (list aria/data and form data stay live) | retired — `migrate meta` only | -| `view-list-passthrough-keys-removed` | `view.list.striped / view.list.bordered / view.list.virtualScroll` | view list keys removed (#7176): 'striped'/'bordered'/'virtualScroll' — every measured reader copied the key forward and none applied it (pass-through-only; ADR-0049 enforce-or-remove) | retired — `migrate meta` only | -| `view-export-options-pdf-removed` | `view.list.exportOptions / view.listViews.*.exportOptions` | list-view export format 'pdf' removed (#8010 — PDF export was declined as #1301 NOT_PLANNED; ObjectGrid dropped the declared format from the menu with only a runtime console.warn) | retired — `migrate meta` only | -| `dashboard-inert-keys-removed` | `dashboard.aria / dashboard.performance / dashboard.widgets[].performance` | dashboard keys 'aria'/'performance' and widget 'performance' removed (#3896 close-out — no renderer applied any of them) | retired — `migrate meta` only | -| `dashboard-widget-responsive-removed` | `dashboard.widgets[].responsive` | dashboard widget key 'responsive' removed (#4876 — no renderer ever applied per-widget breakpoint overrides; the page.components[].responsive key this entry once deferred to was itself retired at protocol 18, #11027) | retired — `migrate meta` only | -| `dashboard-widget-action-aria-removed` | `dashboard.widgets[].actionUrl / dashboard.widgets[].actionType / dashboard.widgets[].actionIcon / dashboard.widgets[].aria` | dashboard widget keys 'actionUrl'/'actionType'/'actionIcon' and 'aria' removed (#5010 — no renderer ever drew a per-widget action button, and widget ARIA attributes never reached the DOM; use header.actions[] and the widget title/description) | retired — `migrate meta` only | -| `dashboard-widget-compareto-converged` | `dashboard.widgets[].compareTo` | dashboard widget 'compareTo' converged on the executor's { kind, dimension? } contract (#5011 — the bare strings and { offset: '1y' } rewrite mechanically; other { offset } durations have no faithful target and are reported, not guessed) | retired — `migrate meta` only | -| `agent-knowledge-removed` | `agent.knowledge` | agent key 'knowledge' removed (#3896 close-out — declaring sources/indexes never scoped retrieval; restrict at the knowledge-service level) | retired — `migrate meta` only | -| `skill-trigger-phrases-removed` | `skill.triggerPhrases` | skill key 'triggerPhrases' removed (#3896 close-out — activation is triggerConditions + the agent's skills[] allowlist; phrases were a dead-end projection) | retired — `migrate meta` only | -| `stack-api-require-auth-removed` | `stack.api.requireAuth` | stack key 'api.requireAuth' removed — anonymous access is always denied; publish public surfaces by declaration (#3963) | retired — `migrate meta` only | -| `flow-node-wait-timeout-keys-removed` | `flow.node.waitEventConfig` | waitEventConfig keys 'timeoutMs' (→ 'timerDuration', stringified — its only reader used it as the duration) and 'onTimeout' (removed — zero readers, so no timeout ever fired) (#4158) | retired — `migrate meta` only | -| `datasource-read-replicas-removed` | `datasource.readReplicas` | datasource key 'readReplicas' removed (#4468 — no driver opened a replica connection and no query path splits reads from writes; front replicas behind one endpoint and point `config` at it) | retired — `migrate meta` only | -| `datasource-capabilities-removed` | `datasource.capabilities` | datasource key 'capabilities' removed (#4583 — eleven flags no code read; pushdown comes from the driver's own supports.*, and `readOnly` never made anything read-only) | retired — `migrate meta` only | -| `datasource-inert-blocks-removed` | `datasource.retryPolicy / datasource.healthCheck / datasource.external.label / datasource.external.requirePermission` | datasource keys 'retryPolicy'/'healthCheck' and external 'label'/'requirePermission' removed (#4583 — nothing retried, nothing probed on a schedule, and the federation label/permission were read by nobody) | retired — `migrate meta` only | -| `mapping-inert-keys-removed` | `mapping.extractQuery / mapping.errorPolicy / mapping.batchSize` | mapping keys 'extractQuery'/'errorPolicy'/'batchSize' removed (#4509 — no exporter reads a mapping, error handling belongs to the import request, and the write path sizes its own batches) | retired — `migrate meta` only | -| `book-translations-removed` | `book.translations / book.groups.translations` | book keys 'translations' (book-level and group-level) removed (#4667 — no resolver read them; the tree endpoint and portal render labels verbatim, so a localized book served its authoring locale to everyone). Localize the docs instead: `doc.translations` is live | retired — `migrate meta` only | -| `job-id-removed` | `job.id` | job key 'id' removed (#4667 — nothing read it; `name` is the job's identity everywhere, so two jobs differing only in `id` were the same job, and the key's own description advertised an override that did not exist) | retired — `migrate meta` only | -| `translation-validation-messages-removed` | `translation.validationMessages` | translation key 'validationMessages' removed (#4667 — no resolver read it, so a translated rule message was stored and never shown; #3778's migration table had been steering retired `errors:` authors into it). Author the message on the rule itself (`object.validations[].message`), and translate it under the object-scoped group `objects.._validations..message`, which the write path resolves (17.3.0, #14381) | retired — `migrate meta` only | -| `datasource-config-driver-key-aliases` | `datasource.config` | datasource config keys → canonical per driver: sqlite 'file'/'database' → 'filename', postgres/mysql 'connectionString' → 'url' and 'user' → 'username', mongo 'uri' → 'url' and 'user' → 'username' (#4456 — driver-factory `??` fallback graduation) | retired — `migrate meta` only | +| `action-execute-to-target` | `action.execute` | action key 'execute' → 'target' (the deprecated handler alias; the spec and the renderer had resolved the pair in opposite directions, so one key now names the handler) | retired — `migrate meta` only | +| `field-conditionalRequired-to-requiredWhen` | `field.conditionalRequired` | field key 'conditionalRequired' → 'requiredWhen' (the deprecated predicate alias, folded into the canonical key so no reader picks its own precedence) | retired — `migrate meta` only | +| `agent-tools-to-skills` | `agent.tools` | agent key 'tools' removed — declare capability in a skill (ADR-0064: an agent's tools are exactly its skills' tools, and this inline slot resolved names against the whole registry with no surface check) | retired — `migrate meta` only | +| `sharing-rule-access-level-full-to-edit` | `sharingRule.accessLevel` | sharing-rule accessLevel 'full' → 'edit' (`full` never granted more than `edit`; a sharing rule grants read or edit, while delete and transfer come from object permissions and ownership) | live — protocol 17 loader accepts the old shape | +| `flow-node-crud-object-alias` | `flow.node.config.objectName` | CRUD flow-node config key 'object' → 'objectName' (the last alias in the executors' `readAliasedConfig` shim graduates into this layer, and the shim is deleted) | live — protocol 17 loader accepts the old shape | +| `flow-node-notify-config-aliases` | `flow.node.notify.config` | notify flow-node config keys 'to' → 'recipients', 'subject' → 'title', 'body' → 'message', 'url' → 'actionUrl' (executor `??` fallbacks graduated into this layer; `actionUrl` is canonical because the notification chain downstream already uses it), and nested 'source: {object, id}' → 'sourceObject' / 'sourceId' (a shape the executor read that no config schema declared) | live — protocol 17 loader accepts the old shape | +| `flow-node-wait-event-config-lift` | `flow.node.wait.waitEventConfig` | wait flow-node loose config keys → the declared `waitEventConfig` block: 'eventType', 'timerDuration'/'duration' → 'timerDuration', 'signalName'/'signal' → 'signalName', 'timeoutMs' (the executor also read these keys from the loose config, a second contract beside the declared block) | live — protocol 17 loader accepts the old shape | +| `flow-node-connector-config-lift` | `flow.node.connector_action.connectorConfig` | connector_action flow-node loose config keys 'connectorId' / 'actionId' / 'input' → the declared `connectorConfig` block (the executor reads only that block; the published designer form had been writing these keys where nothing read them) | live — protocol 17 loader accepts the old shape | +| `flow-node-map-flow-alias` | `flow.node.map.config.flowName` | map flow-node config key 'flow' → 'flowName' (an undeclared spelling the executor accepted through a bare fallback; it graduates into this layer) | live — protocol 17 loader accepts the old shape | +| `flow-node-subflow-flow-alias` | `flow.node.subflow.config.flowName` | subflow flow-node config key 'flow' → 'flowName' (an undeclared spelling the executor accepted through a bare fallback, found when the schemaless nodes were reconciled with their executors; it graduates into this layer) | live — protocol 17 loader accepts the old shape | +| `flow-node-script-config-aliases` | `flow.node.script.config` | script flow-node config keys 'functionName' → 'function', 'input' → 'inputs' (executor `??` fallbacks, graduated into this layer) | live — protocol 17 loader accepts the old shape | +| `permission-rls-priority-removed` | `permission.rowLevelSecurity.priority` | RLS-policy key 'priority' removed (a security audit found no reader: policies OR-combine, so the promised conflict-resolution semantics cannot exist; dropping it changes no outcome) | retired — `migrate meta` only | +| `tool-inert-authoring-keys-removed` | `tool.category / tool.permissions / tool.active / tool.builtIn` | tool keys 'category'/'permissions'/'active'/'builtIn' removed (authorable and inert, so removed under ADR-0049 enforce-or-remove; permissions gated nothing, active:false withdrew nothing) | retired — `migrate meta` only | +| `app-dead-authoring-keys-removed` | `app.version / app.aria / app.objects / app.apis / app.sharing / app.embed / app.mobileNavigation / app.contextSelectors.includeAll / app.contextSelectors.placement / app.homePageId / app.areas.order` | app keys 'version'/'aria'/'objects'/'apis'/'sharing'/'embed'/'mobileNavigation'/'homePageId' plus contextSelectors 'includeAll'/'placement' and areas 'order' removed (liveness audits found each one unread or wrongly encoded; sharing/embed declared a public surface no route enforced, mobileNavigation was fully unimplemented, includeAll was deliberately disobeyed because an 'All' row would clear a mandatory scope, homePageId WAS read by objectui's console before v17 but encoded the landing page as an ID cross-reference that silently fell back when it dangled — the landing page is the first nav item (the first retirement record said nothing read it, a premise since corrected; the retirement stands), and no renderer ever sorted areas) | retired — `migrate meta` only | +| `app-area-fail-open-gates-removed` | `app.areas.visible / app.areas.requiredPermissions` | navigation-area keys 'visible'/'requiredPermissions' removed (ADR-0049 — FAIL-OPEN access gates: no layer ever read them, so a 'hidden' or permission-gated area was served and rendered to every user, while the identically named keys on a navigation ITEM and on the APP are enforced; gate the items inside the area, or gate the app) | retired — `migrate meta` only | +| `action-inert-keys-removed` | `action.shortcut / action.bulkEnabled` | action keys 'shortcut'/'bulkEnabled' removed (inert, removed under ADR-0049 enforce-or-remove: no keydown path dispatches shortcuts; the multi-select toolbar reads the view's bulkActions) | retired — `migrate meta` only | +| `flow-inert-keys-removed` | `flow.active / flow.template / flow.nodes[].outputSchema / flow.errorHandling.fallbackNodeId` | flow keys 'active'/'template', node 'outputSchema' and errorHandling 'fallbackNodeId' removed (inert, removed under ADR-0049 enforce-or-remove: active:false never stopped a flow; status is the enforced lifecycle) | retired — `migrate meta` only | +| `view-inert-keys-removed` | `view.list.responsive / view.list.performance / view.form.defaultSort / view.form.aria` | view keys removed as inert (ADR-0049 enforce-or-remove): list 'responsive'/'performance', form 'defaultSort'/'aria' — no renderer read them (list aria/data and form data stay live) | retired — `migrate meta` only | +| `view-list-passthrough-keys-removed` | `view.list.striped / view.list.bordered / view.list.virtualScroll` | view list keys removed: 'striped'/'bordered'/'virtualScroll' — every measured reader copied the key forward and none applied it (a key that is only passed through is dead in effect; ADR-0049 enforce-or-remove) | retired — `migrate meta` only | +| `view-export-options-pdf-removed` | `view.list.exportOptions / view.listViews.*.exportOptions` | list-view export format 'pdf' removed (PDF export was declined as not planned, and ObjectGrid dropped the declared format from the menu with only a runtime console.warn; an honest enum replaces that warning) | retired — `migrate meta` only | +| `dashboard-inert-keys-removed` | `dashboard.aria / dashboard.performance / dashboard.widgets[].performance` | dashboard keys 'aria'/'performance' and widget 'performance' removed (inert, removed under ADR-0049 enforce-or-remove: no renderer applied any of them) | retired — `migrate meta` only | +| `dashboard-widget-responsive-removed` | `dashboard.widgets[].responsive` | dashboard widget key 'responsive' removed (no renderer ever applied per-widget breakpoint overrides; the page.components[].responsive key this entry once deferred to was measured equally unread and retired at protocol 18) | retired — `migrate meta` only | +| `dashboard-widget-action-aria-removed` | `dashboard.widgets[].actionUrl / dashboard.widgets[].actionType / dashboard.widgets[].actionIcon / dashboard.widgets[].aria` | dashboard widget keys 'actionUrl'/'actionType'/'actionIcon' and 'aria' removed (no renderer ever drew a per-widget action button, and widget ARIA attributes never reached the DOM; use header.actions[] and the widget title/description) | retired — `migrate meta` only | +| `dashboard-widget-compareto-converged` | `dashboard.widgets[].compareTo` | dashboard widget 'compareTo' converged on the executor's { kind, dimension? } contract (the shape the dataset executor implements; the bare strings and { offset: '1y' } rewrite mechanically; other { offset } durations have no faithful target and are reported, not guessed) | retired — `migrate meta` only | +| `agent-knowledge-removed` | `agent.knowledge` | agent key 'knowledge' removed (inert, removed under ADR-0049 enforce-or-remove: declaring sources/indexes never scoped retrieval; restrict at the knowledge-service level) | retired — `migrate meta` only | +| `skill-trigger-phrases-removed` | `skill.triggerPhrases` | skill key 'triggerPhrases' removed (inert, removed under ADR-0049 enforce-or-remove: activation is triggerConditions + the agent's skills[] allowlist; phrases were a dead-end projection) | retired — `migrate meta` only | +| `stack-api-require-auth-removed` | `stack.api.requireAuth` | stack key 'api.requireAuth' removed — anonymous access is always denied; publish public surfaces by declaration (a public form, a share link or `book.audience: 'public'`), which replaced the deployment-wide opt-out | retired — `migrate meta` only | +| `flow-node-wait-timeout-keys-removed` | `flow.node.waitEventConfig` | waitEventConfig keys 'timeoutMs' (→ 'timerDuration', stringified — its only reader used it as the duration) and 'onTimeout' (removed — zero readers, so no timeout ever fired): wait never had a timeout, so its timeout contract is withdrawn rather than built | retired — `migrate meta` only | +| `datasource-read-replicas-removed` | `datasource.readReplicas` | datasource key 'readReplicas' removed (no driver opened a replica connection and no query path splits reads from writes; front replicas behind one endpoint and point `config` at it) | retired — `migrate meta` only | +| `datasource-capabilities-removed` | `datasource.capabilities` | datasource key 'capabilities' removed (eleven flags no code read; pushdown comes from the driver's own supports.*, and `readOnly` never made anything read-only) | retired — `migrate meta` only | +| `datasource-inert-blocks-removed` | `datasource.retryPolicy / datasource.healthCheck / datasource.external.label / datasource.external.requirePermission` | datasource keys 'retryPolicy'/'healthCheck' and external 'label'/'requirePermission' removed (nothing retried, nothing probed on a schedule, and the federation label/permission were read by nobody; each of those jobs already has a live mechanism) | retired — `migrate meta` only | +| `mapping-inert-keys-removed` | `mapping.extractQuery / mapping.errorPolicy / mapping.batchSize` | mapping keys 'extractQuery'/'errorPolicy'/'batchSize' removed (no exporter reads a mapping, error handling belongs to the import request, and the write path sizes its own batches) | retired — `migrate meta` only | +| `book-translations-removed` | `book.translations / book.groups.translations` | book keys 'translations' (book-level and group-level) removed (no resolver read them; the tree endpoint and portal render labels verbatim, so a localized book served its authoring locale to everyone). Localize the docs instead: `doc.translations` is live | retired — `migrate meta` only | +| `job-id-removed` | `job.id` | job key 'id' removed (nothing read it; `name` is the job's identity everywhere, so two jobs differing only in `id` were the same job, and the key's own description advertised an override that did not exist) | retired — `migrate meta` only | +| `translation-validation-messages-removed` | `translation.validationMessages` | translation key 'validationMessages' removed (no resolver read it, so a translated rule message was stored and never shown; the legacy-key table of the translation-bundle migration had been steering retired `errors:` authors into it). Author the message on the rule itself (`object.validations[].message`), and translate it under the object-scoped group `objects.._validations..message`, which the write path resolves (17.3.0, a translation key shipped together with its reader) | retired — `migrate meta` only | +| `datasource-config-driver-key-aliases` | `datasource.config` | datasource config keys → canonical per driver: sqlite 'file'/'database' → 'filename', postgres/mysql 'connectionString' → 'url' and 'user' → 'username', mongo 'uri' → 'url' and 'user' → 'username' (undeclared driver-factory `??` fallbacks, graduated into this layer and deleted from the reader) | retired — `migrate meta` only | | `datasource-driver-mongo-to-mongodb` | `datasource.driver` | datasource driver id 'mongo' → 'mongodb' — the canonical id both boot hosts, the driver package and the published DRIVER_CATALOG already used, so the id that selects a driver and the id that selects its config contract are one string with no mapping between them | live — protocol 17 loader accepts the old shape | -| `flow-node-script-branch-keys-removed` | `flow.node.script.config.actionType / flow.node.script.config.template / flow.node.script.config.recipients / flow.node.script.config.variables / flow.node.script.config.script` | script flow-node config keys 'actionType' (→ 'function' when it was shorthand for one; otherwise removed — 'email'/'slack' were logger-backed stubs that delivered nothing), plus 'template' / 'recipients' / 'variables' (fed those stubs) and 'script' (inline JS the runtime never executed) (#4343) | retired — `migrate meta` only | -| `retry-policy-converged` | `flow.errorHandling.retryDelayMs / flow.node.config.retry.retryDelayMs / job.retryPolicy.maxRetries / job.retryPolicy.backoffMultiplier` | retry policy unified across job.retryPolicy, try_catch retry and flow.errorHandling: base delay 'retryDelayMs' → 'backoffMs', and the pre-17 job defaults (maxRetries 3, backoffMultiplier 2) written out explicitly now that the merged default is 0 / 1 (#4661, #4964) | live — protocol 17 loader accepts the old shape | -| `object-managed-by-system-to-system-data` | `object.managedBy` | object managedBy 'system' → 'system-data' (#3355 — ADR-0103's residual bucket named the engine-owned half v16 had already moved out to `engine-owned`; the rename leaves the name describing what the bucket actually holds: admin/user-writable platform data) | retired — `migrate meta` only | -| `object-enable-trash-mru-removed` | `object.enable.trash / object.enable.mru` | object capability flags 'enable.trash'/'enable.mru' removed (#3207, #2377 close-out — no recycle bin and no MRU tracking ever ran; both default-true flags gated nothing) | retired — `migrate meta` only | -| `hook-body-crypto-hash-removed` | `hook.body.capabilities / action.body.capabilities` | script-body capability token 'crypto.hash' removed (#4391 — the sandbox never installed ctx.crypto.hash, so the token granted a call that always threw; the CLI inferred it too) | retired — `migrate meta` only | -| `dataset-measure-array-string-agg-removed` | `dataset.measures[].aggregate` | dataset measure aggregates 'array_agg' / 'string_agg' removed (#6188 — no SQL backend compiled them and the v1 dataset runtime refused them by name, so a measure declaring one never produced a value; the measure is dropped, and with it any derived measure left referencing it) | retired — `migrate meta` only | -| `connector-rate-limit-config-removed` | `connector.rateLimitConfig` | connector key 'rateLimitConfig' removed (#4911 — no outbound rate-limiting engine exists; the runtime's only token bucket limits INBOUND requests, so every knob here was inert while reading like a configured cap. The whole ConnectorRateLimitConfig shape went with it) | retired — `migrate meta` only | -| `field-mapping-transform-removed` | `connector.fieldMappings[].transform / externalLookup.fieldMappings[].transform` | field-mapping key 'transform' removed (#5552 — the whole five-member FieldMappingTransform union went with it: no runtime ever executed constant/cast/lookup/javascript/map, and the javascript member advertised dialect="js", retired in #3278. The enforced transform pipeline is the import mapping's string-enum `mapping.fieldMapping[].transform`, which is unaffected) | retired — `migrate meta` only | -| `theme-inert-token-scales-removed` | `theme.typography.fontSize / theme.typography.fontWeight / theme.typography.lineHeight / theme.typography.letterSpacing / theme.typography.fontFamily.heading / theme.typography.fontFamily.mono / theme.animation / theme.zIndex` | theme keys 'typography.fontSize'/'fontWeight'/'lineHeight'/'letterSpacing', 'typography.fontFamily.heading'/'mono', 'animation' and 'zIndex' removed (#5021, ADR-0049 — the engine emitted --font-size-*, --font-weight-*, --line-height-*, --letter-spacing-*, --duration-*, --timing-*, --z-*, --font-heading and --font-mono faithfully, and no first-party component or stylesheet has ever read one. Re-declare any variable you actually consume under customVars, which emits it verbatim) | retired — `migrate meta` only | -| `page-header-subtitle-alias` | `page.component.page-header.description` | page-header component prop 'description' → 'subtitle' (objectui#3226 — the `subtitle ?? description` fallback retires) | live — protocol 17 loader accepts the old shape | -| `object-index-type-partial-removed` | `object.indexes[].type / object.indexes[].partial` | object index keys 'indexes[].type'/'indexes[].partial' removed (#5248, #4943 — no driver ever read either: the index method is the dialect's choice and a partial index is built by a database-layer migration, not declared) | retired — `migrate meta` only | -| `record-picker-display-field-to-label-field` | `page.component.element:record_picker.displayField` | record-picker component prop 'displayField' → 'labelField' (#5775 — the required key no renderer read; `labelField ?? 'name'` is what renders the row) | retired — `migrate meta` only | -| `record-picker-inert-keys-removed` | `page.component.element:record_picker.searchFields / page.component.element:record_picker.multiple` | record-picker component props 'searchFields'/'multiple' removed (#5775 — the control is a plain single-select with no search box; neither key had a reader) | retired — `migrate meta` only | -| `page-card-body-to-children` | `page.component.page:card.body` | page:card component prop 'body' → 'children' (#5775 — one composition key across every container; the card renderer already reads both) | retired — `migrate meta` only | -| `inline-action-api-params-to-body-extra` | `page.component.element:button.action.params` | inline type:'api' action prop 'params' (object form) → 'bodyExtra' (#5777 — the payload gets its own key; `params` stays the ActionParam[] definition array) | live — protocol 17 loader accepts the old shape | -| `page-tabs-type-to-tab-style` | `page.component.page:tabs.type` | page:tabs component prop 'type' → 'tabStyle' (#6776 — a props key named `type` collides with the node's dispatch key and is unauthorable in flat/JSX carriers; `tabStyle` is the spelling the renderer reads in all of them) | retired — `migrate meta` only | -| `page-structure-inert-keys-removed` | `page.component.page:header.icon / page.component.page:card.actions` | page:header prop 'icon' and page:card prop 'actions' removed (#6946 — neither has a renderer read point in objectui; the header resolves icons per action and the card renders title/children/footer only) | retired — `migrate meta` only | -| `record-details-layout-removed` | `page.component.record:details.layout` | record:details component prop 'layout' removed (#6946 — the declared auto|custom modes were never implemented; the renderer branches only on inline|compact, values the schema never permitted, so both legal values selected nothing) | retired — `migrate meta` only | -| `app-hidden-to-unpublished` | `app.hidden` | stored app publish gate 'hidden' → '_unpublished' (#4829, ADR-0045 amended — `hidden` carried BOTH the publish gate and 'keep out of the App Switcher', so the built-in Account app was withheld from every non-builder; the gate is now the machine-managed `_unpublished`, and `hidden` is navigation presentation only, never an access gate. Stored rows only — an authored `hidden: true` is left untouched) | retired — `migrate meta` only | -| `action-global-nav-location-removed` | `action.locations[]` | action location 'global_nav' removed (#6888 — no running-app surface rendered it; the ⌘K palette reads no action metadata, while the Studio designer previewed a command-palette frame for it. The value is stripped and the key kept, so an action left with no location becomes the documented headless shape `locations: []`) | retired — `migrate meta` only | +| `flow-node-script-branch-keys-removed` | `flow.node.script.config.actionType / flow.node.script.config.template / flow.node.script.config.recipients / flow.node.script.config.variables / flow.node.script.config.script` | script flow-node config keys 'actionType' (→ 'function' when it was shorthand for one; otherwise removed — 'email'/'slack' were logger-backed stubs that delivered nothing), plus 'template' / 'recipients' / 'variables' (fed those stubs) and 'script' (inline JS the runtime never executed); script is now a pure function-call node, the only path that ran real logic | retired — `migrate meta` only | +| `retry-policy-converged` | `flow.errorHandling.retryDelayMs / flow.node.config.retry.retryDelayMs / job.retryPolicy.maxRetries / job.retryPolicy.backoffMultiplier` | retry policy unified across job.retryPolicy, try_catch retry and flow.errorHandling: base delay 'retryDelayMs' → 'backoffMs', and the pre-17 job defaults (maxRetries 3, backoffMultiplier 2) written out explicitly now that the merged default is 0 / 1: two declarations that differed only by accident became one, and retry is opt-in because a retry replays whatever the attempt already did | live — protocol 17 loader accepts the old shape | +| `object-managed-by-system-to-system-data` | `object.managedBy` | object managedBy 'system' → 'system-data' (ADR-0103's residual bucket named the engine-owned half v16 had already moved out to `engine-owned`; the rename leaves the name describing what the bucket actually holds: admin/user-writable platform data) | retired — `migrate meta` only | +| `object-enable-trash-mru-removed` | `object.enable.trash / object.enable.mru` | object capability flags 'enable.trash'/'enable.mru' removed (the last slice of the dead author-facing property removals: no recycle bin and no MRU tracking ever ran; both default-true flags gated nothing) | retired — `migrate meta` only | +| `hook-body-crypto-hash-removed` | `hook.body.capabilities / action.body.capabilities` | script-body capability token 'crypto.hash' removed (the sandbox never installed ctx.crypto.hash, so the token granted a call that always threw; the CLI inferred it too) | retired — `migrate meta` only | +| `dataset-measure-array-string-agg-removed` | `dataset.measures[].aggregate` | dataset measure aggregates 'array_agg' / 'string_agg' removed (no SQL backend compiled them and the v1 dataset runtime refused them by name, so a measure declaring one never produced a value; the measure is dropped, and with it any derived measure left referencing it) | retired — `migrate meta` only | +| `connector-rate-limit-config-removed` | `connector.rateLimitConfig` | connector key 'rateLimitConfig' removed (no outbound rate-limiting engine exists; the runtime's only token bucket limits INBOUND requests, so every knob here was inert while reading like a configured cap. The whole ConnectorRateLimitConfig shape went with it) | retired — `migrate meta` only | +| `field-mapping-transform-removed` | `connector.fieldMappings[].transform / externalLookup.fieldMappings[].transform` | field-mapping key 'transform' removed (the whole five-member FieldMappingTransform union went with it: no runtime ever executed constant/cast/lookup/javascript/map, and the javascript member advertised dialect="js", a dialect already retired because JavaScript belongs in a script body. The enforced transform pipeline is the import mapping's string-enum `mapping.fieldMapping[].transform`, which is unaffected) | retired — `migrate meta` only | +| `theme-inert-token-scales-removed` | `theme.typography.fontSize / theme.typography.fontWeight / theme.typography.lineHeight / theme.typography.letterSpacing / theme.typography.fontFamily.heading / theme.typography.fontFamily.mono / theme.animation / theme.zIndex` | theme keys 'typography.fontSize'/'fontWeight'/'lineHeight'/'letterSpacing', 'typography.fontFamily.heading'/'mono', 'animation' and 'zIndex' removed (ADR-0049 — the engine emitted --font-size-*, --font-weight-*, --line-height-*, --letter-spacing-*, --duration-*, --timing-*, --z-*, --font-heading and --font-mono faithfully, and no first-party component or stylesheet has ever read one. Re-declare any variable you actually consume under customVars, which emits it verbatim) | retired — `migrate meta` only | +| `page-header-subtitle-alias` | `page.component.page-header.description` | page-header component prop 'description' → 'subtitle' (the off-spec spelling a renderer tolerated through a bare `subtitle ?? description` fallback; `subtitle` is the declared key, and the fallback retires) | live — protocol 17 loader accepts the old shape | +| `object-index-type-partial-removed` | `object.indexes[].type / object.indexes[].partial` | object index keys 'indexes[].type'/'indexes[].partial' removed (no driver ever read either: the index method is the dialect's choice and a partial index is built by a database-layer migration, not declared) | retired — `migrate meta` only | +| `record-picker-display-field-to-label-field` | `page.component.element:record_picker.displayField` | record-picker component prop 'displayField' → 'labelField' (the required key no renderer read; `labelField ?? 'name'` is what renders the row, so the delivered spelling became the declared one) | retired — `migrate meta` only | +| `record-picker-inert-keys-removed` | `page.component.element:record_picker.searchFields / page.component.element:record_picker.multiple` | record-picker component props 'searchFields'/'multiple' removed (the control is a plain single-select with no search box; neither key had a reader) | retired — `migrate meta` only | +| `page-card-body-to-children` | `page.component.page:card.body` | page:card component prop 'body' → 'children' (one composition key across every container; the card renderer already reads both) | retired — `migrate meta` only | +| `inline-action-api-params-to-body-extra` | `page.component.element:button.action.params` | inline type:'api' action prop 'params' (object form) → 'bodyExtra' (a static payload and a parameter definition are two things, so the payload gets its own key; `params` stays the ActionParam[] definition array) | live — protocol 17 loader accepts the old shape | +| `page-tabs-type-to-tab-style` | `page.component.page:tabs.type` | page:tabs component prop 'type' → 'tabStyle' (a props key named `type` collides with the node's dispatch key and is unauthorable in flat/JSX carriers; `tabStyle` is the spelling the renderer reads in all of them) | retired — `migrate meta` only | +| `page-structure-inert-keys-removed` | `page.component.page:header.icon / page.component.page:card.actions` | page:header prop 'icon' and page:card prop 'actions' removed (neither has a renderer read point in objectui; the header resolves icons per action and the card renders title/children/footer only) | retired — `migrate meta` only | +| `record-details-layout-removed` | `page.component.record:details.layout` | record:details component prop 'layout' removed (the declared auto|custom modes were never implemented; the renderer branches only on inline|compact, values the schema never permitted, so both legal values selected nothing) | retired — `migrate meta` only | +| `app-hidden-to-unpublished` | `app.hidden` | stored app publish gate 'hidden' → '_unpublished' (ADR-0045 amended — `hidden` carried BOTH the publish gate and 'keep out of the App Switcher', so the built-in Account app was withheld from every non-builder; the gate is now the machine-managed `_unpublished`, and `hidden` is navigation presentation only, never an access gate. Stored rows only — an authored `hidden: true` is left untouched) | retired — `migrate meta` only | +| `action-global-nav-location-removed` | `action.locations[]` | action location 'global_nav' removed (no running-app surface rendered it; the ⌘K palette reads no action metadata, while the Studio designer previewed a command-palette frame for it. The value is stripped and the key kept, so an action left with no location becomes the documented headless shape `locations: []`) | retired — `migrate meta` only | ### Semantic (delegated to you, with acceptance criteria) diff --git a/packages/spec/spec-changes.json b/packages/spec/spec-changes.json index 1998bcef160..14b08dbbd9c 100644 --- a/packages/spec/spec-changes.json +++ b/packages/spec/spec-changes.json @@ -10,217 +10,217 @@ "converted": [ { "surface": "action.execute", - "to": "action key 'execute' → 'target' (the deprecated handler alias, #3713)", + "to": "action key 'execute' → 'target' (the deprecated handler alias; the spec and the renderer had resolved the pair in opposite directions, so one key now names the handler)", "conversionId": "action-execute-to-target", "toMajor": 17 }, { "surface": "field.conditionalRequired", - "to": "field key 'conditionalRequired' → 'requiredWhen' (the deprecated predicate alias, #3754)", + "to": "field key 'conditionalRequired' → 'requiredWhen' (the deprecated predicate alias, folded into the canonical key so no reader picks its own precedence)", "conversionId": "field-conditionalRequired-to-requiredWhen", "toMajor": 17 }, { "surface": "agent.tools", - "to": "agent key 'tools' removed — declare capability in a skill (ADR-0064, #3894)", + "to": "agent key 'tools' removed — declare capability in a skill (ADR-0064: an agent's tools are exactly its skills' tools, and this inline slot resolved names against the whole registry with no surface check)", "conversionId": "agent-tools-to-skills", "toMajor": 17 }, { "surface": "sharingRule.accessLevel", - "to": "sharing-rule accessLevel 'full' → 'edit' (#3865 — `full` never granted more than `edit`)", + "to": "sharing-rule accessLevel 'full' → 'edit' (`full` never granted more than `edit`; a sharing rule grants read or edit, while delete and transfer come from object permissions and ownership)", "conversionId": "sharing-rule-access-level-full-to-edit", "toMajor": 17 }, { "surface": "flow.node.config.objectName", - "to": "CRUD flow-node config key 'object' → 'objectName' (#3796 — `readAliasedConfig` shim graduation)", + "to": "CRUD flow-node config key 'object' → 'objectName' (the last alias in the executors' `readAliasedConfig` shim graduates into this layer, and the shim is deleted)", "conversionId": "flow-node-crud-object-alias", "toMajor": 17 }, { "surface": "flow.node.notify.config", - "to": "notify flow-node config keys 'to' → 'recipients', 'subject' → 'title', 'body' → 'message', 'url' → 'actionUrl' (#3796), and nested 'source: {object, id}' → 'sourceObject' / 'sourceId' (#4045)", + "to": "notify flow-node config keys 'to' → 'recipients', 'subject' → 'title', 'body' → 'message', 'url' → 'actionUrl' (executor `??` fallbacks graduated into this layer; `actionUrl` is canonical because the notification chain downstream already uses it), and nested 'source: {object, id}' → 'sourceObject' / 'sourceId' (a shape the executor read that no config schema declared)", "conversionId": "flow-node-notify-config-aliases", "toMajor": 17 }, { "surface": "flow.node.wait.waitEventConfig", - "to": "wait flow-node loose config keys → the declared `waitEventConfig` block: 'eventType', 'timerDuration'/'duration' → 'timerDuration', 'signalName'/'signal' → 'signalName', 'timeoutMs' (#4045)", + "to": "wait flow-node loose config keys → the declared `waitEventConfig` block: 'eventType', 'timerDuration'/'duration' → 'timerDuration', 'signalName'/'signal' → 'signalName', 'timeoutMs' (the executor also read these keys from the loose config, a second contract beside the declared block)", "conversionId": "flow-node-wait-event-config-lift", "toMajor": 17 }, { "surface": "flow.node.connector_action.connectorConfig", - "to": "connector_action flow-node loose config keys 'connectorId' / 'actionId' / 'input' → the declared `connectorConfig` block (#4045)", + "to": "connector_action flow-node loose config keys 'connectorId' / 'actionId' / 'input' → the declared `connectorConfig` block (the executor reads only that block; the published designer form had been writing these keys where nothing read them)", "conversionId": "flow-node-connector-config-lift", "toMajor": 17 }, { "surface": "flow.node.map.config.flowName", - "to": "map flow-node config key 'flow' → 'flowName' (#4045 — undeclared executor fallback graduation)", + "to": "map flow-node config key 'flow' → 'flowName' (an undeclared spelling the executor accepted through a bare fallback; it graduates into this layer)", "conversionId": "flow-node-map-flow-alias", "toMajor": 17 }, { "surface": "flow.node.subflow.config.flowName", - "to": "subflow flow-node config key 'flow' → 'flowName' (#4278 — undeclared executor fallback graduation)", + "to": "subflow flow-node config key 'flow' → 'flowName' (an undeclared spelling the executor accepted through a bare fallback, found when the schemaless nodes were reconciled with their executors; it graduates into this layer)", "conversionId": "flow-node-subflow-flow-alias", "toMajor": 17 }, { "surface": "flow.node.script.config", - "to": "script flow-node config keys 'functionName' → 'function', 'input' → 'inputs' (#3796)", + "to": "script flow-node config keys 'functionName' → 'function', 'input' → 'inputs' (executor `??` fallbacks, graduated into this layer)", "conversionId": "flow-node-script-config-aliases", "toMajor": 17 }, { "surface": "permission.rowLevelSecurity.priority", - "to": "RLS-policy key 'priority' removed (#3896 audit — policies OR-combine, so the promised conflict-resolution semantics cannot exist; dropping it changes no outcome)", + "to": "RLS-policy key 'priority' removed (a security audit found no reader: policies OR-combine, so the promised conflict-resolution semantics cannot exist; dropping it changes no outcome)", "conversionId": "permission-rls-priority-removed", "toMajor": 17 }, { "surface": "tool.category / tool.permissions / tool.active / tool.builtIn", - "to": "tool keys 'category'/'permissions'/'active'/'builtIn' removed (#3896 close-out — authorable and inert; permissions gated nothing, active:false withdrew nothing)", + "to": "tool keys 'category'/'permissions'/'active'/'builtIn' removed (authorable and inert, so removed under ADR-0049 enforce-or-remove; permissions gated nothing, active:false withdrew nothing)", "conversionId": "tool-inert-authoring-keys-removed", "toMajor": 17 }, { "surface": "app.version / app.aria / app.objects / app.apis / app.sharing / app.embed / app.mobileNavigation / app.contextSelectors.includeAll / app.contextSelectors.placement / app.homePageId / app.areas.order", - "to": "app keys 'version'/'aria'/'objects'/'apis'/'sharing'/'embed'/'mobileNavigation'/'homePageId' plus contextSelectors 'includeAll'/'placement' and areas 'order' removed (liveness audits #4001, #4509, #4667 — unread or wrongly encoded; sharing/embed declared a public surface no route enforced, mobileNavigation was fully unimplemented, includeAll was deliberately disobeyed because an 'All' row would clear a mandatory scope, homePageId WAS read by objectui's console before v17 but encoded the landing page as an ID cross-reference that silently fell back when it dangled — the landing page is the first nav item (premise corrected in #4709; the retirement stands), and no renderer ever sorted areas)", + "to": "app keys 'version'/'aria'/'objects'/'apis'/'sharing'/'embed'/'mobileNavigation'/'homePageId' plus contextSelectors 'includeAll'/'placement' and areas 'order' removed (liveness audits found each one unread or wrongly encoded; sharing/embed declared a public surface no route enforced, mobileNavigation was fully unimplemented, includeAll was deliberately disobeyed because an 'All' row would clear a mandatory scope, homePageId WAS read by objectui's console before v17 but encoded the landing page as an ID cross-reference that silently fell back when it dangled — the landing page is the first nav item (the first retirement record said nothing read it, a premise since corrected; the retirement stands), and no renderer ever sorted areas)", "conversionId": "app-dead-authoring-keys-removed", "toMajor": 17 }, { "surface": "app.areas.visible / app.areas.requiredPermissions", - "to": "navigation-area keys 'visible'/'requiredPermissions' removed (#4651, ADR-0049 — FAIL-OPEN access gates: no layer ever read them, so a 'hidden' or permission-gated area was served and rendered to every user, while the identically named keys on a navigation ITEM and on the APP are enforced; gate the items inside the area, or gate the app)", + "to": "navigation-area keys 'visible'/'requiredPermissions' removed (ADR-0049 — FAIL-OPEN access gates: no layer ever read them, so a 'hidden' or permission-gated area was served and rendered to every user, while the identically named keys on a navigation ITEM and on the APP are enforced; gate the items inside the area, or gate the app)", "conversionId": "app-area-fail-open-gates-removed", "toMajor": 17 }, { "surface": "action.shortcut / action.bulkEnabled", - "to": "action keys 'shortcut'/'bulkEnabled' removed (#3896 close-out — no keydown path dispatches shortcuts; the multi-select toolbar reads the view's bulkActions)", + "to": "action keys 'shortcut'/'bulkEnabled' removed (inert, removed under ADR-0049 enforce-or-remove: no keydown path dispatches shortcuts; the multi-select toolbar reads the view's bulkActions)", "conversionId": "action-inert-keys-removed", "toMajor": 17 }, { "surface": "flow.active / flow.template / flow.nodes[].outputSchema / flow.errorHandling.fallbackNodeId", - "to": "flow keys 'active'/'template', node 'outputSchema' and errorHandling 'fallbackNodeId' removed (#3896 close-out — active:false never stopped a flow; status is the enforced lifecycle)", + "to": "flow keys 'active'/'template', node 'outputSchema' and errorHandling 'fallbackNodeId' removed (inert, removed under ADR-0049 enforce-or-remove: active:false never stopped a flow; status is the enforced lifecycle)", "conversionId": "flow-inert-keys-removed", "toMajor": 17 }, { "surface": "view.list.responsive / view.list.performance / view.form.defaultSort / view.form.aria", - "to": "view keys removed (#3896 close-out): list 'responsive'/'performance', form 'defaultSort'/'aria' — no renderer read them (list aria/data and form data stay live)", + "to": "view keys removed as inert (ADR-0049 enforce-or-remove): list 'responsive'/'performance', form 'defaultSort'/'aria' — no renderer read them (list aria/data and form data stay live)", "conversionId": "view-inert-keys-removed", "toMajor": 17 }, { "surface": "view.list.striped / view.list.bordered / view.list.virtualScroll", - "to": "view list keys removed (#7176): 'striped'/'bordered'/'virtualScroll' — every measured reader copied the key forward and none applied it (pass-through-only; ADR-0049 enforce-or-remove)", + "to": "view list keys removed: 'striped'/'bordered'/'virtualScroll' — every measured reader copied the key forward and none applied it (a key that is only passed through is dead in effect; ADR-0049 enforce-or-remove)", "conversionId": "view-list-passthrough-keys-removed", "toMajor": 17 }, { "surface": "view.list.exportOptions / view.listViews.*.exportOptions", - "to": "list-view export format 'pdf' removed (#8010 — PDF export was declined as #1301 NOT_PLANNED; ObjectGrid dropped the declared format from the menu with only a runtime console.warn)", + "to": "list-view export format 'pdf' removed (PDF export was declined as not planned, and ObjectGrid dropped the declared format from the menu with only a runtime console.warn; an honest enum replaces that warning)", "conversionId": "view-export-options-pdf-removed", "toMajor": 17 }, { "surface": "dashboard.aria / dashboard.performance / dashboard.widgets[].performance", - "to": "dashboard keys 'aria'/'performance' and widget 'performance' removed (#3896 close-out — no renderer applied any of them)", + "to": "dashboard keys 'aria'/'performance' and widget 'performance' removed (inert, removed under ADR-0049 enforce-or-remove: no renderer applied any of them)", "conversionId": "dashboard-inert-keys-removed", "toMajor": 17 }, { "surface": "dashboard.widgets[].responsive", - "to": "dashboard widget key 'responsive' removed (#4876 — no renderer ever applied per-widget breakpoint overrides; the page.components[].responsive key this entry once deferred to was itself retired at protocol 18, #11027)", + "to": "dashboard widget key 'responsive' removed (no renderer ever applied per-widget breakpoint overrides; the page.components[].responsive key this entry once deferred to was measured equally unread and retired at protocol 18)", "conversionId": "dashboard-widget-responsive-removed", "toMajor": 17 }, { "surface": "dashboard.widgets[].actionUrl / dashboard.widgets[].actionType / dashboard.widgets[].actionIcon / dashboard.widgets[].aria", - "to": "dashboard widget keys 'actionUrl'/'actionType'/'actionIcon' and 'aria' removed (#5010 — no renderer ever drew a per-widget action button, and widget ARIA attributes never reached the DOM; use header.actions[] and the widget title/description)", + "to": "dashboard widget keys 'actionUrl'/'actionType'/'actionIcon' and 'aria' removed (no renderer ever drew a per-widget action button, and widget ARIA attributes never reached the DOM; use header.actions[] and the widget title/description)", "conversionId": "dashboard-widget-action-aria-removed", "toMajor": 17 }, { "surface": "dashboard.widgets[].compareTo", - "to": "dashboard widget 'compareTo' converged on the executor's { kind, dimension? } contract (#5011 — the bare strings and { offset: '1y' } rewrite mechanically; other { offset } durations have no faithful target and are reported, not guessed)", + "to": "dashboard widget 'compareTo' converged on the executor's { kind, dimension? } contract (the shape the dataset executor implements; the bare strings and { offset: '1y' } rewrite mechanically; other { offset } durations have no faithful target and are reported, not guessed)", "conversionId": "dashboard-widget-compareto-converged", "toMajor": 17 }, { "surface": "agent.knowledge", - "to": "agent key 'knowledge' removed (#3896 close-out — declaring sources/indexes never scoped retrieval; restrict at the knowledge-service level)", + "to": "agent key 'knowledge' removed (inert, removed under ADR-0049 enforce-or-remove: declaring sources/indexes never scoped retrieval; restrict at the knowledge-service level)", "conversionId": "agent-knowledge-removed", "toMajor": 17 }, { "surface": "skill.triggerPhrases", - "to": "skill key 'triggerPhrases' removed (#3896 close-out — activation is triggerConditions + the agent's skills[] allowlist; phrases were a dead-end projection)", + "to": "skill key 'triggerPhrases' removed (inert, removed under ADR-0049 enforce-or-remove: activation is triggerConditions + the agent's skills[] allowlist; phrases were a dead-end projection)", "conversionId": "skill-trigger-phrases-removed", "toMajor": 17 }, { "surface": "stack.api.requireAuth", - "to": "stack key 'api.requireAuth' removed — anonymous access is always denied; publish public surfaces by declaration (#3963)", + "to": "stack key 'api.requireAuth' removed — anonymous access is always denied; publish public surfaces by declaration (a public form, a share link or `book.audience: 'public'`), which replaced the deployment-wide opt-out", "conversionId": "stack-api-require-auth-removed", "toMajor": 17 }, { "surface": "flow.node.waitEventConfig", - "to": "waitEventConfig keys 'timeoutMs' (→ 'timerDuration', stringified — its only reader used it as the duration) and 'onTimeout' (removed — zero readers, so no timeout ever fired) (#4158)", + "to": "waitEventConfig keys 'timeoutMs' (→ 'timerDuration', stringified — its only reader used it as the duration) and 'onTimeout' (removed — zero readers, so no timeout ever fired): wait never had a timeout, so its timeout contract is withdrawn rather than built", "conversionId": "flow-node-wait-timeout-keys-removed", "toMajor": 17 }, { "surface": "datasource.readReplicas", - "to": "datasource key 'readReplicas' removed (#4468 — no driver opened a replica connection and no query path splits reads from writes; front replicas behind one endpoint and point `config` at it)", + "to": "datasource key 'readReplicas' removed (no driver opened a replica connection and no query path splits reads from writes; front replicas behind one endpoint and point `config` at it)", "conversionId": "datasource-read-replicas-removed", "toMajor": 17 }, { "surface": "datasource.capabilities", - "to": "datasource key 'capabilities' removed (#4583 — eleven flags no code read; pushdown comes from the driver's own supports.*, and `readOnly` never made anything read-only)", + "to": "datasource key 'capabilities' removed (eleven flags no code read; pushdown comes from the driver's own supports.*, and `readOnly` never made anything read-only)", "conversionId": "datasource-capabilities-removed", "toMajor": 17 }, { "surface": "datasource.retryPolicy / datasource.healthCheck / datasource.external.label / datasource.external.requirePermission", - "to": "datasource keys 'retryPolicy'/'healthCheck' and external 'label'/'requirePermission' removed (#4583 — nothing retried, nothing probed on a schedule, and the federation label/permission were read by nobody)", + "to": "datasource keys 'retryPolicy'/'healthCheck' and external 'label'/'requirePermission' removed (nothing retried, nothing probed on a schedule, and the federation label/permission were read by nobody; each of those jobs already has a live mechanism)", "conversionId": "datasource-inert-blocks-removed", "toMajor": 17 }, { "surface": "mapping.extractQuery / mapping.errorPolicy / mapping.batchSize", - "to": "mapping keys 'extractQuery'/'errorPolicy'/'batchSize' removed (#4509 — no exporter reads a mapping, error handling belongs to the import request, and the write path sizes its own batches)", + "to": "mapping keys 'extractQuery'/'errorPolicy'/'batchSize' removed (no exporter reads a mapping, error handling belongs to the import request, and the write path sizes its own batches)", "conversionId": "mapping-inert-keys-removed", "toMajor": 17 }, { "surface": "book.translations / book.groups.translations", - "to": "book keys 'translations' (book-level and group-level) removed (#4667 — no resolver read them; the tree endpoint and portal render labels verbatim, so a localized book served its authoring locale to everyone). Localize the docs instead: `doc.translations` is live", + "to": "book keys 'translations' (book-level and group-level) removed (no resolver read them; the tree endpoint and portal render labels verbatim, so a localized book served its authoring locale to everyone). Localize the docs instead: `doc.translations` is live", "conversionId": "book-translations-removed", "toMajor": 17 }, { "surface": "job.id", - "to": "job key 'id' removed (#4667 — nothing read it; `name` is the job's identity everywhere, so two jobs differing only in `id` were the same job, and the key's own description advertised an override that did not exist)", + "to": "job key 'id' removed (nothing read it; `name` is the job's identity everywhere, so two jobs differing only in `id` were the same job, and the key's own description advertised an override that did not exist)", "conversionId": "job-id-removed", "toMajor": 17 }, { "surface": "translation.validationMessages", - "to": "translation key 'validationMessages' removed (#4667 — no resolver read it, so a translated rule message was stored and never shown; #3778's migration table had been steering retired `errors:` authors into it). Author the message on the rule itself (`object.validations[].message`), and translate it under the object-scoped group `objects.._validations..message`, which the write path resolves (17.3.0, #14381)", + "to": "translation key 'validationMessages' removed (no resolver read it, so a translated rule message was stored and never shown; the legacy-key table of the translation-bundle migration had been steering retired `errors:` authors into it). Author the message on the rule itself (`object.validations[].message`), and translate it under the object-scoped group `objects.._validations..message`, which the write path resolves (17.3.0, a translation key shipped together with its reader)", "conversionId": "translation-validation-messages-removed", "toMajor": 17 }, { "surface": "datasource.config", - "to": "datasource config keys → canonical per driver: sqlite 'file'/'database' → 'filename', postgres/mysql 'connectionString' → 'url' and 'user' → 'username', mongo 'uri' → 'url' and 'user' → 'username' (#4456 — driver-factory `??` fallback graduation)", + "to": "datasource config keys → canonical per driver: sqlite 'file'/'database' → 'filename', postgres/mysql 'connectionString' → 'url' and 'user' → 'username', mongo 'uri' → 'url' and 'user' → 'username' (undeclared driver-factory `??` fallbacks, graduated into this layer and deleted from the reader)", "conversionId": "datasource-config-driver-key-aliases", "toMajor": 17 }, @@ -232,121 +232,121 @@ }, { "surface": "flow.node.script.config.actionType / flow.node.script.config.template / flow.node.script.config.recipients / flow.node.script.config.variables / flow.node.script.config.script", - "to": "script flow-node config keys 'actionType' (→ 'function' when it was shorthand for one; otherwise removed — 'email'/'slack' were logger-backed stubs that delivered nothing), plus 'template' / 'recipients' / 'variables' (fed those stubs) and 'script' (inline JS the runtime never executed) (#4343)", + "to": "script flow-node config keys 'actionType' (→ 'function' when it was shorthand for one; otherwise removed — 'email'/'slack' were logger-backed stubs that delivered nothing), plus 'template' / 'recipients' / 'variables' (fed those stubs) and 'script' (inline JS the runtime never executed); script is now a pure function-call node, the only path that ran real logic", "conversionId": "flow-node-script-branch-keys-removed", "toMajor": 17 }, { "surface": "flow.errorHandling.retryDelayMs / flow.node.config.retry.retryDelayMs / job.retryPolicy.maxRetries / job.retryPolicy.backoffMultiplier", - "to": "retry policy unified across job.retryPolicy, try_catch retry and flow.errorHandling: base delay 'retryDelayMs' → 'backoffMs', and the pre-17 job defaults (maxRetries 3, backoffMultiplier 2) written out explicitly now that the merged default is 0 / 1 (#4661, #4964)", + "to": "retry policy unified across job.retryPolicy, try_catch retry and flow.errorHandling: base delay 'retryDelayMs' → 'backoffMs', and the pre-17 job defaults (maxRetries 3, backoffMultiplier 2) written out explicitly now that the merged default is 0 / 1: two declarations that differed only by accident became one, and retry is opt-in because a retry replays whatever the attempt already did", "conversionId": "retry-policy-converged", "toMajor": 17 }, { "surface": "object.managedBy", - "to": "object managedBy 'system' → 'system-data' (#3355 — ADR-0103's residual bucket named the engine-owned half v16 had already moved out to `engine-owned`; the rename leaves the name describing what the bucket actually holds: admin/user-writable platform data)", + "to": "object managedBy 'system' → 'system-data' (ADR-0103's residual bucket named the engine-owned half v16 had already moved out to `engine-owned`; the rename leaves the name describing what the bucket actually holds: admin/user-writable platform data)", "conversionId": "object-managed-by-system-to-system-data", "toMajor": 17 }, { "surface": "object.enable.trash / object.enable.mru", - "to": "object capability flags 'enable.trash'/'enable.mru' removed (#3207, #2377 close-out — no recycle bin and no MRU tracking ever ran; both default-true flags gated nothing)", + "to": "object capability flags 'enable.trash'/'enable.mru' removed (the last slice of the dead author-facing property removals: no recycle bin and no MRU tracking ever ran; both default-true flags gated nothing)", "conversionId": "object-enable-trash-mru-removed", "toMajor": 17 }, { "surface": "hook.body.capabilities / action.body.capabilities", - "to": "script-body capability token 'crypto.hash' removed (#4391 — the sandbox never installed ctx.crypto.hash, so the token granted a call that always threw; the CLI inferred it too)", + "to": "script-body capability token 'crypto.hash' removed (the sandbox never installed ctx.crypto.hash, so the token granted a call that always threw; the CLI inferred it too)", "conversionId": "hook-body-crypto-hash-removed", "toMajor": 17 }, { "surface": "dataset.measures[].aggregate", - "to": "dataset measure aggregates 'array_agg' / 'string_agg' removed (#6188 — no SQL backend compiled them and the v1 dataset runtime refused them by name, so a measure declaring one never produced a value; the measure is dropped, and with it any derived measure left referencing it)", + "to": "dataset measure aggregates 'array_agg' / 'string_agg' removed (no SQL backend compiled them and the v1 dataset runtime refused them by name, so a measure declaring one never produced a value; the measure is dropped, and with it any derived measure left referencing it)", "conversionId": "dataset-measure-array-string-agg-removed", "toMajor": 17 }, { "surface": "connector.rateLimitConfig", - "to": "connector key 'rateLimitConfig' removed (#4911 — no outbound rate-limiting engine exists; the runtime's only token bucket limits INBOUND requests, so every knob here was inert while reading like a configured cap. The whole ConnectorRateLimitConfig shape went with it)", + "to": "connector key 'rateLimitConfig' removed (no outbound rate-limiting engine exists; the runtime's only token bucket limits INBOUND requests, so every knob here was inert while reading like a configured cap. The whole ConnectorRateLimitConfig shape went with it)", "conversionId": "connector-rate-limit-config-removed", "toMajor": 17 }, { "surface": "connector.fieldMappings[].transform / externalLookup.fieldMappings[].transform", - "to": "field-mapping key 'transform' removed (#5552 — the whole five-member FieldMappingTransform union went with it: no runtime ever executed constant/cast/lookup/javascript/map, and the javascript member advertised dialect=\"js\", retired in #3278. The enforced transform pipeline is the import mapping's string-enum `mapping.fieldMapping[].transform`, which is unaffected)", + "to": "field-mapping key 'transform' removed (the whole five-member FieldMappingTransform union went with it: no runtime ever executed constant/cast/lookup/javascript/map, and the javascript member advertised dialect=\"js\", a dialect already retired because JavaScript belongs in a script body. The enforced transform pipeline is the import mapping's string-enum `mapping.fieldMapping[].transform`, which is unaffected)", "conversionId": "field-mapping-transform-removed", "toMajor": 17 }, { "surface": "theme.typography.fontSize / theme.typography.fontWeight / theme.typography.lineHeight / theme.typography.letterSpacing / theme.typography.fontFamily.heading / theme.typography.fontFamily.mono / theme.animation / theme.zIndex", - "to": "theme keys 'typography.fontSize'/'fontWeight'/'lineHeight'/'letterSpacing', 'typography.fontFamily.heading'/'mono', 'animation' and 'zIndex' removed (#5021, ADR-0049 — the engine emitted --font-size-*, --font-weight-*, --line-height-*, --letter-spacing-*, --duration-*, --timing-*, --z-*, --font-heading and --font-mono faithfully, and no first-party component or stylesheet has ever read one. Re-declare any variable you actually consume under customVars, which emits it verbatim)", + "to": "theme keys 'typography.fontSize'/'fontWeight'/'lineHeight'/'letterSpacing', 'typography.fontFamily.heading'/'mono', 'animation' and 'zIndex' removed (ADR-0049 — the engine emitted --font-size-*, --font-weight-*, --line-height-*, --letter-spacing-*, --duration-*, --timing-*, --z-*, --font-heading and --font-mono faithfully, and no first-party component or stylesheet has ever read one. Re-declare any variable you actually consume under customVars, which emits it verbatim)", "conversionId": "theme-inert-token-scales-removed", "toMajor": 17 }, { "surface": "page.component.page-header.description", - "to": "page-header component prop 'description' → 'subtitle' (objectui#3226 — the `subtitle ?? description` fallback retires)", + "to": "page-header component prop 'description' → 'subtitle' (the off-spec spelling a renderer tolerated through a bare `subtitle ?? description` fallback; `subtitle` is the declared key, and the fallback retires)", "conversionId": "page-header-subtitle-alias", "toMajor": 17 }, { "surface": "object.indexes[].type / object.indexes[].partial", - "to": "object index keys 'indexes[].type'/'indexes[].partial' removed (#5248, #4943 — no driver ever read either: the index method is the dialect's choice and a partial index is built by a database-layer migration, not declared)", + "to": "object index keys 'indexes[].type'/'indexes[].partial' removed (no driver ever read either: the index method is the dialect's choice and a partial index is built by a database-layer migration, not declared)", "conversionId": "object-index-type-partial-removed", "toMajor": 17 }, { "surface": "page.component.element:record_picker.displayField", - "to": "record-picker component prop 'displayField' → 'labelField' (#5775 — the required key no renderer read; `labelField ?? 'name'` is what renders the row)", + "to": "record-picker component prop 'displayField' → 'labelField' (the required key no renderer read; `labelField ?? 'name'` is what renders the row, so the delivered spelling became the declared one)", "conversionId": "record-picker-display-field-to-label-field", "toMajor": 17 }, { "surface": "page.component.element:record_picker.searchFields / page.component.element:record_picker.multiple", - "to": "record-picker component props 'searchFields'/'multiple' removed (#5775 — the control is a plain single-select with no search box; neither key had a reader)", + "to": "record-picker component props 'searchFields'/'multiple' removed (the control is a plain single-select with no search box; neither key had a reader)", "conversionId": "record-picker-inert-keys-removed", "toMajor": 17 }, { "surface": "page.component.page:card.body", - "to": "page:card component prop 'body' → 'children' (#5775 — one composition key across every container; the card renderer already reads both)", + "to": "page:card component prop 'body' → 'children' (one composition key across every container; the card renderer already reads both)", "conversionId": "page-card-body-to-children", "toMajor": 17 }, { "surface": "page.component.element:button.action.params", - "to": "inline type:'api' action prop 'params' (object form) → 'bodyExtra' (#5777 — the payload gets its own key; `params` stays the ActionParam[] definition array)", + "to": "inline type:'api' action prop 'params' (object form) → 'bodyExtra' (a static payload and a parameter definition are two things, so the payload gets its own key; `params` stays the ActionParam[] definition array)", "conversionId": "inline-action-api-params-to-body-extra", "toMajor": 17 }, { "surface": "page.component.page:tabs.type", - "to": "page:tabs component prop 'type' → 'tabStyle' (#6776 — a props key named `type` collides with the node's dispatch key and is unauthorable in flat/JSX carriers; `tabStyle` is the spelling the renderer reads in all of them)", + "to": "page:tabs component prop 'type' → 'tabStyle' (a props key named `type` collides with the node's dispatch key and is unauthorable in flat/JSX carriers; `tabStyle` is the spelling the renderer reads in all of them)", "conversionId": "page-tabs-type-to-tab-style", "toMajor": 17 }, { "surface": "page.component.page:header.icon / page.component.page:card.actions", - "to": "page:header prop 'icon' and page:card prop 'actions' removed (#6946 — neither has a renderer read point in objectui; the header resolves icons per action and the card renders title/children/footer only)", + "to": "page:header prop 'icon' and page:card prop 'actions' removed (neither has a renderer read point in objectui; the header resolves icons per action and the card renders title/children/footer only)", "conversionId": "page-structure-inert-keys-removed", "toMajor": 17 }, { "surface": "page.component.record:details.layout", - "to": "record:details component prop 'layout' removed (#6946 — the declared auto|custom modes were never implemented; the renderer branches only on inline|compact, values the schema never permitted, so both legal values selected nothing)", + "to": "record:details component prop 'layout' removed (the declared auto|custom modes were never implemented; the renderer branches only on inline|compact, values the schema never permitted, so both legal values selected nothing)", "conversionId": "record-details-layout-removed", "toMajor": 17 }, { "surface": "app.hidden", - "to": "stored app publish gate 'hidden' → '_unpublished' (#4829, ADR-0045 amended — `hidden` carried BOTH the publish gate and 'keep out of the App Switcher', so the built-in Account app was withheld from every non-builder; the gate is now the machine-managed `_unpublished`, and `hidden` is navigation presentation only, never an access gate. Stored rows only — an authored `hidden: true` is left untouched)", + "to": "stored app publish gate 'hidden' → '_unpublished' (ADR-0045 amended — `hidden` carried BOTH the publish gate and 'keep out of the App Switcher', so the built-in Account app was withheld from every non-builder; the gate is now the machine-managed `_unpublished`, and `hidden` is navigation presentation only, never an access gate. Stored rows only — an authored `hidden: true` is left untouched)", "conversionId": "app-hidden-to-unpublished", "toMajor": 17 }, { "surface": "action.locations[]", - "to": "action location 'global_nav' removed (#6888 — no running-app surface rendered it; the ⌘K palette reads no action metadata, while the Studio designer previewed a command-palette frame for it. The value is stripped and the key kept, so an action left with no location becomes the documented headless shape `locations: []`)", + "to": "action location 'global_nav' removed (no running-app surface rendered it; the ⌘K palette reads no action metadata, while the Studio designer previewed a command-palette frame for it. The value is stripped and the key kept, so an action left with no location becomes the documented headless shape `locations: []`)", "conversionId": "action-global-nav-location-removed", "toMajor": 17 } @@ -902,217 +902,217 @@ "converted": [ { "surface": "action.execute", - "to": "action key 'execute' → 'target' (the deprecated handler alias, #3713)", + "to": "action key 'execute' → 'target' (the deprecated handler alias; the spec and the renderer had resolved the pair in opposite directions, so one key now names the handler)", "conversionId": "action-execute-to-target", "toMajor": 17 }, { "surface": "field.conditionalRequired", - "to": "field key 'conditionalRequired' → 'requiredWhen' (the deprecated predicate alias, #3754)", + "to": "field key 'conditionalRequired' → 'requiredWhen' (the deprecated predicate alias, folded into the canonical key so no reader picks its own precedence)", "conversionId": "field-conditionalRequired-to-requiredWhen", "toMajor": 17 }, { "surface": "agent.tools", - "to": "agent key 'tools' removed — declare capability in a skill (ADR-0064, #3894)", + "to": "agent key 'tools' removed — declare capability in a skill (ADR-0064: an agent's tools are exactly its skills' tools, and this inline slot resolved names against the whole registry with no surface check)", "conversionId": "agent-tools-to-skills", "toMajor": 17 }, { "surface": "sharingRule.accessLevel", - "to": "sharing-rule accessLevel 'full' → 'edit' (#3865 — `full` never granted more than `edit`)", + "to": "sharing-rule accessLevel 'full' → 'edit' (`full` never granted more than `edit`; a sharing rule grants read or edit, while delete and transfer come from object permissions and ownership)", "conversionId": "sharing-rule-access-level-full-to-edit", "toMajor": 17 }, { "surface": "flow.node.config.objectName", - "to": "CRUD flow-node config key 'object' → 'objectName' (#3796 — `readAliasedConfig` shim graduation)", + "to": "CRUD flow-node config key 'object' → 'objectName' (the last alias in the executors' `readAliasedConfig` shim graduates into this layer, and the shim is deleted)", "conversionId": "flow-node-crud-object-alias", "toMajor": 17 }, { "surface": "flow.node.notify.config", - "to": "notify flow-node config keys 'to' → 'recipients', 'subject' → 'title', 'body' → 'message', 'url' → 'actionUrl' (#3796), and nested 'source: {object, id}' → 'sourceObject' / 'sourceId' (#4045)", + "to": "notify flow-node config keys 'to' → 'recipients', 'subject' → 'title', 'body' → 'message', 'url' → 'actionUrl' (executor `??` fallbacks graduated into this layer; `actionUrl` is canonical because the notification chain downstream already uses it), and nested 'source: {object, id}' → 'sourceObject' / 'sourceId' (a shape the executor read that no config schema declared)", "conversionId": "flow-node-notify-config-aliases", "toMajor": 17 }, { "surface": "flow.node.wait.waitEventConfig", - "to": "wait flow-node loose config keys → the declared `waitEventConfig` block: 'eventType', 'timerDuration'/'duration' → 'timerDuration', 'signalName'/'signal' → 'signalName', 'timeoutMs' (#4045)", + "to": "wait flow-node loose config keys → the declared `waitEventConfig` block: 'eventType', 'timerDuration'/'duration' → 'timerDuration', 'signalName'/'signal' → 'signalName', 'timeoutMs' (the executor also read these keys from the loose config, a second contract beside the declared block)", "conversionId": "flow-node-wait-event-config-lift", "toMajor": 17 }, { "surface": "flow.node.connector_action.connectorConfig", - "to": "connector_action flow-node loose config keys 'connectorId' / 'actionId' / 'input' → the declared `connectorConfig` block (#4045)", + "to": "connector_action flow-node loose config keys 'connectorId' / 'actionId' / 'input' → the declared `connectorConfig` block (the executor reads only that block; the published designer form had been writing these keys where nothing read them)", "conversionId": "flow-node-connector-config-lift", "toMajor": 17 }, { "surface": "flow.node.map.config.flowName", - "to": "map flow-node config key 'flow' → 'flowName' (#4045 — undeclared executor fallback graduation)", + "to": "map flow-node config key 'flow' → 'flowName' (an undeclared spelling the executor accepted through a bare fallback; it graduates into this layer)", "conversionId": "flow-node-map-flow-alias", "toMajor": 17 }, { "surface": "flow.node.subflow.config.flowName", - "to": "subflow flow-node config key 'flow' → 'flowName' (#4278 — undeclared executor fallback graduation)", + "to": "subflow flow-node config key 'flow' → 'flowName' (an undeclared spelling the executor accepted through a bare fallback, found when the schemaless nodes were reconciled with their executors; it graduates into this layer)", "conversionId": "flow-node-subflow-flow-alias", "toMajor": 17 }, { "surface": "flow.node.script.config", - "to": "script flow-node config keys 'functionName' → 'function', 'input' → 'inputs' (#3796)", + "to": "script flow-node config keys 'functionName' → 'function', 'input' → 'inputs' (executor `??` fallbacks, graduated into this layer)", "conversionId": "flow-node-script-config-aliases", "toMajor": 17 }, { "surface": "permission.rowLevelSecurity.priority", - "to": "RLS-policy key 'priority' removed (#3896 audit — policies OR-combine, so the promised conflict-resolution semantics cannot exist; dropping it changes no outcome)", + "to": "RLS-policy key 'priority' removed (a security audit found no reader: policies OR-combine, so the promised conflict-resolution semantics cannot exist; dropping it changes no outcome)", "conversionId": "permission-rls-priority-removed", "toMajor": 17 }, { "surface": "tool.category / tool.permissions / tool.active / tool.builtIn", - "to": "tool keys 'category'/'permissions'/'active'/'builtIn' removed (#3896 close-out — authorable and inert; permissions gated nothing, active:false withdrew nothing)", + "to": "tool keys 'category'/'permissions'/'active'/'builtIn' removed (authorable and inert, so removed under ADR-0049 enforce-or-remove; permissions gated nothing, active:false withdrew nothing)", "conversionId": "tool-inert-authoring-keys-removed", "toMajor": 17 }, { "surface": "app.version / app.aria / app.objects / app.apis / app.sharing / app.embed / app.mobileNavigation / app.contextSelectors.includeAll / app.contextSelectors.placement / app.homePageId / app.areas.order", - "to": "app keys 'version'/'aria'/'objects'/'apis'/'sharing'/'embed'/'mobileNavigation'/'homePageId' plus contextSelectors 'includeAll'/'placement' and areas 'order' removed (liveness audits #4001, #4509, #4667 — unread or wrongly encoded; sharing/embed declared a public surface no route enforced, mobileNavigation was fully unimplemented, includeAll was deliberately disobeyed because an 'All' row would clear a mandatory scope, homePageId WAS read by objectui's console before v17 but encoded the landing page as an ID cross-reference that silently fell back when it dangled — the landing page is the first nav item (premise corrected in #4709; the retirement stands), and no renderer ever sorted areas)", + "to": "app keys 'version'/'aria'/'objects'/'apis'/'sharing'/'embed'/'mobileNavigation'/'homePageId' plus contextSelectors 'includeAll'/'placement' and areas 'order' removed (liveness audits found each one unread or wrongly encoded; sharing/embed declared a public surface no route enforced, mobileNavigation was fully unimplemented, includeAll was deliberately disobeyed because an 'All' row would clear a mandatory scope, homePageId WAS read by objectui's console before v17 but encoded the landing page as an ID cross-reference that silently fell back when it dangled — the landing page is the first nav item (the first retirement record said nothing read it, a premise since corrected; the retirement stands), and no renderer ever sorted areas)", "conversionId": "app-dead-authoring-keys-removed", "toMajor": 17 }, { "surface": "app.areas.visible / app.areas.requiredPermissions", - "to": "navigation-area keys 'visible'/'requiredPermissions' removed (#4651, ADR-0049 — FAIL-OPEN access gates: no layer ever read them, so a 'hidden' or permission-gated area was served and rendered to every user, while the identically named keys on a navigation ITEM and on the APP are enforced; gate the items inside the area, or gate the app)", + "to": "navigation-area keys 'visible'/'requiredPermissions' removed (ADR-0049 — FAIL-OPEN access gates: no layer ever read them, so a 'hidden' or permission-gated area was served and rendered to every user, while the identically named keys on a navigation ITEM and on the APP are enforced; gate the items inside the area, or gate the app)", "conversionId": "app-area-fail-open-gates-removed", "toMajor": 17 }, { "surface": "action.shortcut / action.bulkEnabled", - "to": "action keys 'shortcut'/'bulkEnabled' removed (#3896 close-out — no keydown path dispatches shortcuts; the multi-select toolbar reads the view's bulkActions)", + "to": "action keys 'shortcut'/'bulkEnabled' removed (inert, removed under ADR-0049 enforce-or-remove: no keydown path dispatches shortcuts; the multi-select toolbar reads the view's bulkActions)", "conversionId": "action-inert-keys-removed", "toMajor": 17 }, { "surface": "flow.active / flow.template / flow.nodes[].outputSchema / flow.errorHandling.fallbackNodeId", - "to": "flow keys 'active'/'template', node 'outputSchema' and errorHandling 'fallbackNodeId' removed (#3896 close-out — active:false never stopped a flow; status is the enforced lifecycle)", + "to": "flow keys 'active'/'template', node 'outputSchema' and errorHandling 'fallbackNodeId' removed (inert, removed under ADR-0049 enforce-or-remove: active:false never stopped a flow; status is the enforced lifecycle)", "conversionId": "flow-inert-keys-removed", "toMajor": 17 }, { "surface": "view.list.responsive / view.list.performance / view.form.defaultSort / view.form.aria", - "to": "view keys removed (#3896 close-out): list 'responsive'/'performance', form 'defaultSort'/'aria' — no renderer read them (list aria/data and form data stay live)", + "to": "view keys removed as inert (ADR-0049 enforce-or-remove): list 'responsive'/'performance', form 'defaultSort'/'aria' — no renderer read them (list aria/data and form data stay live)", "conversionId": "view-inert-keys-removed", "toMajor": 17 }, { "surface": "view.list.striped / view.list.bordered / view.list.virtualScroll", - "to": "view list keys removed (#7176): 'striped'/'bordered'/'virtualScroll' — every measured reader copied the key forward and none applied it (pass-through-only; ADR-0049 enforce-or-remove)", + "to": "view list keys removed: 'striped'/'bordered'/'virtualScroll' — every measured reader copied the key forward and none applied it (a key that is only passed through is dead in effect; ADR-0049 enforce-or-remove)", "conversionId": "view-list-passthrough-keys-removed", "toMajor": 17 }, { "surface": "view.list.exportOptions / view.listViews.*.exportOptions", - "to": "list-view export format 'pdf' removed (#8010 — PDF export was declined as #1301 NOT_PLANNED; ObjectGrid dropped the declared format from the menu with only a runtime console.warn)", + "to": "list-view export format 'pdf' removed (PDF export was declined as not planned, and ObjectGrid dropped the declared format from the menu with only a runtime console.warn; an honest enum replaces that warning)", "conversionId": "view-export-options-pdf-removed", "toMajor": 17 }, { "surface": "dashboard.aria / dashboard.performance / dashboard.widgets[].performance", - "to": "dashboard keys 'aria'/'performance' and widget 'performance' removed (#3896 close-out — no renderer applied any of them)", + "to": "dashboard keys 'aria'/'performance' and widget 'performance' removed (inert, removed under ADR-0049 enforce-or-remove: no renderer applied any of them)", "conversionId": "dashboard-inert-keys-removed", "toMajor": 17 }, { "surface": "dashboard.widgets[].responsive", - "to": "dashboard widget key 'responsive' removed (#4876 — no renderer ever applied per-widget breakpoint overrides; the page.components[].responsive key this entry once deferred to was itself retired at protocol 18, #11027)", + "to": "dashboard widget key 'responsive' removed (no renderer ever applied per-widget breakpoint overrides; the page.components[].responsive key this entry once deferred to was measured equally unread and retired at protocol 18)", "conversionId": "dashboard-widget-responsive-removed", "toMajor": 17 }, { "surface": "dashboard.widgets[].actionUrl / dashboard.widgets[].actionType / dashboard.widgets[].actionIcon / dashboard.widgets[].aria", - "to": "dashboard widget keys 'actionUrl'/'actionType'/'actionIcon' and 'aria' removed (#5010 — no renderer ever drew a per-widget action button, and widget ARIA attributes never reached the DOM; use header.actions[] and the widget title/description)", + "to": "dashboard widget keys 'actionUrl'/'actionType'/'actionIcon' and 'aria' removed (no renderer ever drew a per-widget action button, and widget ARIA attributes never reached the DOM; use header.actions[] and the widget title/description)", "conversionId": "dashboard-widget-action-aria-removed", "toMajor": 17 }, { "surface": "dashboard.widgets[].compareTo", - "to": "dashboard widget 'compareTo' converged on the executor's { kind, dimension? } contract (#5011 — the bare strings and { offset: '1y' } rewrite mechanically; other { offset } durations have no faithful target and are reported, not guessed)", + "to": "dashboard widget 'compareTo' converged on the executor's { kind, dimension? } contract (the shape the dataset executor implements; the bare strings and { offset: '1y' } rewrite mechanically; other { offset } durations have no faithful target and are reported, not guessed)", "conversionId": "dashboard-widget-compareto-converged", "toMajor": 17 }, { "surface": "agent.knowledge", - "to": "agent key 'knowledge' removed (#3896 close-out — declaring sources/indexes never scoped retrieval; restrict at the knowledge-service level)", + "to": "agent key 'knowledge' removed (inert, removed under ADR-0049 enforce-or-remove: declaring sources/indexes never scoped retrieval; restrict at the knowledge-service level)", "conversionId": "agent-knowledge-removed", "toMajor": 17 }, { "surface": "skill.triggerPhrases", - "to": "skill key 'triggerPhrases' removed (#3896 close-out — activation is triggerConditions + the agent's skills[] allowlist; phrases were a dead-end projection)", + "to": "skill key 'triggerPhrases' removed (inert, removed under ADR-0049 enforce-or-remove: activation is triggerConditions + the agent's skills[] allowlist; phrases were a dead-end projection)", "conversionId": "skill-trigger-phrases-removed", "toMajor": 17 }, { "surface": "stack.api.requireAuth", - "to": "stack key 'api.requireAuth' removed — anonymous access is always denied; publish public surfaces by declaration (#3963)", + "to": "stack key 'api.requireAuth' removed — anonymous access is always denied; publish public surfaces by declaration (a public form, a share link or `book.audience: 'public'`), which replaced the deployment-wide opt-out", "conversionId": "stack-api-require-auth-removed", "toMajor": 17 }, { "surface": "flow.node.waitEventConfig", - "to": "waitEventConfig keys 'timeoutMs' (→ 'timerDuration', stringified — its only reader used it as the duration) and 'onTimeout' (removed — zero readers, so no timeout ever fired) (#4158)", + "to": "waitEventConfig keys 'timeoutMs' (→ 'timerDuration', stringified — its only reader used it as the duration) and 'onTimeout' (removed — zero readers, so no timeout ever fired): wait never had a timeout, so its timeout contract is withdrawn rather than built", "conversionId": "flow-node-wait-timeout-keys-removed", "toMajor": 17 }, { "surface": "datasource.readReplicas", - "to": "datasource key 'readReplicas' removed (#4468 — no driver opened a replica connection and no query path splits reads from writes; front replicas behind one endpoint and point `config` at it)", + "to": "datasource key 'readReplicas' removed (no driver opened a replica connection and no query path splits reads from writes; front replicas behind one endpoint and point `config` at it)", "conversionId": "datasource-read-replicas-removed", "toMajor": 17 }, { "surface": "datasource.capabilities", - "to": "datasource key 'capabilities' removed (#4583 — eleven flags no code read; pushdown comes from the driver's own supports.*, and `readOnly` never made anything read-only)", + "to": "datasource key 'capabilities' removed (eleven flags no code read; pushdown comes from the driver's own supports.*, and `readOnly` never made anything read-only)", "conversionId": "datasource-capabilities-removed", "toMajor": 17 }, { "surface": "datasource.retryPolicy / datasource.healthCheck / datasource.external.label / datasource.external.requirePermission", - "to": "datasource keys 'retryPolicy'/'healthCheck' and external 'label'/'requirePermission' removed (#4583 — nothing retried, nothing probed on a schedule, and the federation label/permission were read by nobody)", + "to": "datasource keys 'retryPolicy'/'healthCheck' and external 'label'/'requirePermission' removed (nothing retried, nothing probed on a schedule, and the federation label/permission were read by nobody; each of those jobs already has a live mechanism)", "conversionId": "datasource-inert-blocks-removed", "toMajor": 17 }, { "surface": "mapping.extractQuery / mapping.errorPolicy / mapping.batchSize", - "to": "mapping keys 'extractQuery'/'errorPolicy'/'batchSize' removed (#4509 — no exporter reads a mapping, error handling belongs to the import request, and the write path sizes its own batches)", + "to": "mapping keys 'extractQuery'/'errorPolicy'/'batchSize' removed (no exporter reads a mapping, error handling belongs to the import request, and the write path sizes its own batches)", "conversionId": "mapping-inert-keys-removed", "toMajor": 17 }, { "surface": "book.translations / book.groups.translations", - "to": "book keys 'translations' (book-level and group-level) removed (#4667 — no resolver read them; the tree endpoint and portal render labels verbatim, so a localized book served its authoring locale to everyone). Localize the docs instead: `doc.translations` is live", + "to": "book keys 'translations' (book-level and group-level) removed (no resolver read them; the tree endpoint and portal render labels verbatim, so a localized book served its authoring locale to everyone). Localize the docs instead: `doc.translations` is live", "conversionId": "book-translations-removed", "toMajor": 17 }, { "surface": "job.id", - "to": "job key 'id' removed (#4667 — nothing read it; `name` is the job's identity everywhere, so two jobs differing only in `id` were the same job, and the key's own description advertised an override that did not exist)", + "to": "job key 'id' removed (nothing read it; `name` is the job's identity everywhere, so two jobs differing only in `id` were the same job, and the key's own description advertised an override that did not exist)", "conversionId": "job-id-removed", "toMajor": 17 }, { "surface": "translation.validationMessages", - "to": "translation key 'validationMessages' removed (#4667 — no resolver read it, so a translated rule message was stored and never shown; #3778's migration table had been steering retired `errors:` authors into it). Author the message on the rule itself (`object.validations[].message`), and translate it under the object-scoped group `objects.._validations..message`, which the write path resolves (17.3.0, #14381)", + "to": "translation key 'validationMessages' removed (no resolver read it, so a translated rule message was stored and never shown; the legacy-key table of the translation-bundle migration had been steering retired `errors:` authors into it). Author the message on the rule itself (`object.validations[].message`), and translate it under the object-scoped group `objects.._validations..message`, which the write path resolves (17.3.0, a translation key shipped together with its reader)", "conversionId": "translation-validation-messages-removed", "toMajor": 17 }, { "surface": "datasource.config", - "to": "datasource config keys → canonical per driver: sqlite 'file'/'database' → 'filename', postgres/mysql 'connectionString' → 'url' and 'user' → 'username', mongo 'uri' → 'url' and 'user' → 'username' (#4456 — driver-factory `??` fallback graduation)", + "to": "datasource config keys → canonical per driver: sqlite 'file'/'database' → 'filename', postgres/mysql 'connectionString' → 'url' and 'user' → 'username', mongo 'uri' → 'url' and 'user' → 'username' (undeclared driver-factory `??` fallbacks, graduated into this layer and deleted from the reader)", "conversionId": "datasource-config-driver-key-aliases", "toMajor": 17 }, @@ -1124,121 +1124,121 @@ }, { "surface": "flow.node.script.config.actionType / flow.node.script.config.template / flow.node.script.config.recipients / flow.node.script.config.variables / flow.node.script.config.script", - "to": "script flow-node config keys 'actionType' (→ 'function' when it was shorthand for one; otherwise removed — 'email'/'slack' were logger-backed stubs that delivered nothing), plus 'template' / 'recipients' / 'variables' (fed those stubs) and 'script' (inline JS the runtime never executed) (#4343)", + "to": "script flow-node config keys 'actionType' (→ 'function' when it was shorthand for one; otherwise removed — 'email'/'slack' were logger-backed stubs that delivered nothing), plus 'template' / 'recipients' / 'variables' (fed those stubs) and 'script' (inline JS the runtime never executed); script is now a pure function-call node, the only path that ran real logic", "conversionId": "flow-node-script-branch-keys-removed", "toMajor": 17 }, { "surface": "flow.errorHandling.retryDelayMs / flow.node.config.retry.retryDelayMs / job.retryPolicy.maxRetries / job.retryPolicy.backoffMultiplier", - "to": "retry policy unified across job.retryPolicy, try_catch retry and flow.errorHandling: base delay 'retryDelayMs' → 'backoffMs', and the pre-17 job defaults (maxRetries 3, backoffMultiplier 2) written out explicitly now that the merged default is 0 / 1 (#4661, #4964)", + "to": "retry policy unified across job.retryPolicy, try_catch retry and flow.errorHandling: base delay 'retryDelayMs' → 'backoffMs', and the pre-17 job defaults (maxRetries 3, backoffMultiplier 2) written out explicitly now that the merged default is 0 / 1: two declarations that differed only by accident became one, and retry is opt-in because a retry replays whatever the attempt already did", "conversionId": "retry-policy-converged", "toMajor": 17 }, { "surface": "object.managedBy", - "to": "object managedBy 'system' → 'system-data' (#3355 — ADR-0103's residual bucket named the engine-owned half v16 had already moved out to `engine-owned`; the rename leaves the name describing what the bucket actually holds: admin/user-writable platform data)", + "to": "object managedBy 'system' → 'system-data' (ADR-0103's residual bucket named the engine-owned half v16 had already moved out to `engine-owned`; the rename leaves the name describing what the bucket actually holds: admin/user-writable platform data)", "conversionId": "object-managed-by-system-to-system-data", "toMajor": 17 }, { "surface": "object.enable.trash / object.enable.mru", - "to": "object capability flags 'enable.trash'/'enable.mru' removed (#3207, #2377 close-out — no recycle bin and no MRU tracking ever ran; both default-true flags gated nothing)", + "to": "object capability flags 'enable.trash'/'enable.mru' removed (the last slice of the dead author-facing property removals: no recycle bin and no MRU tracking ever ran; both default-true flags gated nothing)", "conversionId": "object-enable-trash-mru-removed", "toMajor": 17 }, { "surface": "hook.body.capabilities / action.body.capabilities", - "to": "script-body capability token 'crypto.hash' removed (#4391 — the sandbox never installed ctx.crypto.hash, so the token granted a call that always threw; the CLI inferred it too)", + "to": "script-body capability token 'crypto.hash' removed (the sandbox never installed ctx.crypto.hash, so the token granted a call that always threw; the CLI inferred it too)", "conversionId": "hook-body-crypto-hash-removed", "toMajor": 17 }, { "surface": "dataset.measures[].aggregate", - "to": "dataset measure aggregates 'array_agg' / 'string_agg' removed (#6188 — no SQL backend compiled them and the v1 dataset runtime refused them by name, so a measure declaring one never produced a value; the measure is dropped, and with it any derived measure left referencing it)", + "to": "dataset measure aggregates 'array_agg' / 'string_agg' removed (no SQL backend compiled them and the v1 dataset runtime refused them by name, so a measure declaring one never produced a value; the measure is dropped, and with it any derived measure left referencing it)", "conversionId": "dataset-measure-array-string-agg-removed", "toMajor": 17 }, { "surface": "connector.rateLimitConfig", - "to": "connector key 'rateLimitConfig' removed (#4911 — no outbound rate-limiting engine exists; the runtime's only token bucket limits INBOUND requests, so every knob here was inert while reading like a configured cap. The whole ConnectorRateLimitConfig shape went with it)", + "to": "connector key 'rateLimitConfig' removed (no outbound rate-limiting engine exists; the runtime's only token bucket limits INBOUND requests, so every knob here was inert while reading like a configured cap. The whole ConnectorRateLimitConfig shape went with it)", "conversionId": "connector-rate-limit-config-removed", "toMajor": 17 }, { "surface": "connector.fieldMappings[].transform / externalLookup.fieldMappings[].transform", - "to": "field-mapping key 'transform' removed (#5552 — the whole five-member FieldMappingTransform union went with it: no runtime ever executed constant/cast/lookup/javascript/map, and the javascript member advertised dialect=\"js\", retired in #3278. The enforced transform pipeline is the import mapping's string-enum `mapping.fieldMapping[].transform`, which is unaffected)", + "to": "field-mapping key 'transform' removed (the whole five-member FieldMappingTransform union went with it: no runtime ever executed constant/cast/lookup/javascript/map, and the javascript member advertised dialect=\"js\", a dialect already retired because JavaScript belongs in a script body. The enforced transform pipeline is the import mapping's string-enum `mapping.fieldMapping[].transform`, which is unaffected)", "conversionId": "field-mapping-transform-removed", "toMajor": 17 }, { "surface": "theme.typography.fontSize / theme.typography.fontWeight / theme.typography.lineHeight / theme.typography.letterSpacing / theme.typography.fontFamily.heading / theme.typography.fontFamily.mono / theme.animation / theme.zIndex", - "to": "theme keys 'typography.fontSize'/'fontWeight'/'lineHeight'/'letterSpacing', 'typography.fontFamily.heading'/'mono', 'animation' and 'zIndex' removed (#5021, ADR-0049 — the engine emitted --font-size-*, --font-weight-*, --line-height-*, --letter-spacing-*, --duration-*, --timing-*, --z-*, --font-heading and --font-mono faithfully, and no first-party component or stylesheet has ever read one. Re-declare any variable you actually consume under customVars, which emits it verbatim)", + "to": "theme keys 'typography.fontSize'/'fontWeight'/'lineHeight'/'letterSpacing', 'typography.fontFamily.heading'/'mono', 'animation' and 'zIndex' removed (ADR-0049 — the engine emitted --font-size-*, --font-weight-*, --line-height-*, --letter-spacing-*, --duration-*, --timing-*, --z-*, --font-heading and --font-mono faithfully, and no first-party component or stylesheet has ever read one. Re-declare any variable you actually consume under customVars, which emits it verbatim)", "conversionId": "theme-inert-token-scales-removed", "toMajor": 17 }, { "surface": "page.component.page-header.description", - "to": "page-header component prop 'description' → 'subtitle' (objectui#3226 — the `subtitle ?? description` fallback retires)", + "to": "page-header component prop 'description' → 'subtitle' (the off-spec spelling a renderer tolerated through a bare `subtitle ?? description` fallback; `subtitle` is the declared key, and the fallback retires)", "conversionId": "page-header-subtitle-alias", "toMajor": 17 }, { "surface": "object.indexes[].type / object.indexes[].partial", - "to": "object index keys 'indexes[].type'/'indexes[].partial' removed (#5248, #4943 — no driver ever read either: the index method is the dialect's choice and a partial index is built by a database-layer migration, not declared)", + "to": "object index keys 'indexes[].type'/'indexes[].partial' removed (no driver ever read either: the index method is the dialect's choice and a partial index is built by a database-layer migration, not declared)", "conversionId": "object-index-type-partial-removed", "toMajor": 17 }, { "surface": "page.component.element:record_picker.displayField", - "to": "record-picker component prop 'displayField' → 'labelField' (#5775 — the required key no renderer read; `labelField ?? 'name'` is what renders the row)", + "to": "record-picker component prop 'displayField' → 'labelField' (the required key no renderer read; `labelField ?? 'name'` is what renders the row, so the delivered spelling became the declared one)", "conversionId": "record-picker-display-field-to-label-field", "toMajor": 17 }, { "surface": "page.component.element:record_picker.searchFields / page.component.element:record_picker.multiple", - "to": "record-picker component props 'searchFields'/'multiple' removed (#5775 — the control is a plain single-select with no search box; neither key had a reader)", + "to": "record-picker component props 'searchFields'/'multiple' removed (the control is a plain single-select with no search box; neither key had a reader)", "conversionId": "record-picker-inert-keys-removed", "toMajor": 17 }, { "surface": "page.component.page:card.body", - "to": "page:card component prop 'body' → 'children' (#5775 — one composition key across every container; the card renderer already reads both)", + "to": "page:card component prop 'body' → 'children' (one composition key across every container; the card renderer already reads both)", "conversionId": "page-card-body-to-children", "toMajor": 17 }, { "surface": "page.component.element:button.action.params", - "to": "inline type:'api' action prop 'params' (object form) → 'bodyExtra' (#5777 — the payload gets its own key; `params` stays the ActionParam[] definition array)", + "to": "inline type:'api' action prop 'params' (object form) → 'bodyExtra' (a static payload and a parameter definition are two things, so the payload gets its own key; `params` stays the ActionParam[] definition array)", "conversionId": "inline-action-api-params-to-body-extra", "toMajor": 17 }, { "surface": "page.component.page:tabs.type", - "to": "page:tabs component prop 'type' → 'tabStyle' (#6776 — a props key named `type` collides with the node's dispatch key and is unauthorable in flat/JSX carriers; `tabStyle` is the spelling the renderer reads in all of them)", + "to": "page:tabs component prop 'type' → 'tabStyle' (a props key named `type` collides with the node's dispatch key and is unauthorable in flat/JSX carriers; `tabStyle` is the spelling the renderer reads in all of them)", "conversionId": "page-tabs-type-to-tab-style", "toMajor": 17 }, { "surface": "page.component.page:header.icon / page.component.page:card.actions", - "to": "page:header prop 'icon' and page:card prop 'actions' removed (#6946 — neither has a renderer read point in objectui; the header resolves icons per action and the card renders title/children/footer only)", + "to": "page:header prop 'icon' and page:card prop 'actions' removed (neither has a renderer read point in objectui; the header resolves icons per action and the card renders title/children/footer only)", "conversionId": "page-structure-inert-keys-removed", "toMajor": 17 }, { "surface": "page.component.record:details.layout", - "to": "record:details component prop 'layout' removed (#6946 — the declared auto|custom modes were never implemented; the renderer branches only on inline|compact, values the schema never permitted, so both legal values selected nothing)", + "to": "record:details component prop 'layout' removed (the declared auto|custom modes were never implemented; the renderer branches only on inline|compact, values the schema never permitted, so both legal values selected nothing)", "conversionId": "record-details-layout-removed", "toMajor": 17 }, { "surface": "app.hidden", - "to": "stored app publish gate 'hidden' → '_unpublished' (#4829, ADR-0045 amended — `hidden` carried BOTH the publish gate and 'keep out of the App Switcher', so the built-in Account app was withheld from every non-builder; the gate is now the machine-managed `_unpublished`, and `hidden` is navigation presentation only, never an access gate. Stored rows only — an authored `hidden: true` is left untouched)", + "to": "stored app publish gate 'hidden' → '_unpublished' (ADR-0045 amended — `hidden` carried BOTH the publish gate and 'keep out of the App Switcher', so the built-in Account app was withheld from every non-builder; the gate is now the machine-managed `_unpublished`, and `hidden` is navigation presentation only, never an access gate. Stored rows only — an authored `hidden: true` is left untouched)", "conversionId": "app-hidden-to-unpublished", "toMajor": 17 }, { "surface": "action.locations[]", - "to": "action location 'global_nav' removed (#6888 — no running-app surface rendered it; the ⌘K palette reads no action metadata, while the Studio designer previewed a command-palette frame for it. The value is stripped and the key kept, so an action left with no location becomes the documented headless shape `locations: []`)", + "to": "action location 'global_nav' removed (no running-app surface rendered it; the ⌘K palette reads no action metadata, while the Studio designer previewed a command-palette frame for it. The value is stripped and the key kept, so an action left with no location becomes the documented headless shape `locations: []`)", "conversionId": "action-global-nav-location-removed", "toMajor": 17 } From 7dbf2131dfbfa0b2e03c46e570c0942c4d8fe433 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 05:27:24 +0000 Subject: [PATCH 3/3] chore(changeset): spec patch for the stage-4 reworded conversion summaries Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude --- ...spec-strings-stage4-conversion-summaries.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .changeset/20749-spec-strings-stage4-conversion-summaries.md diff --git a/.changeset/20749-spec-strings-stage4-conversion-summaries.md b/.changeset/20749-spec-strings-stage4-conversion-summaries.md new file mode 100644 index 00000000000..7e94e6deca3 --- /dev/null +++ b/.changeset/20749-spec-strings-stage4-conversion-summaries.md @@ -0,0 +1,18 @@ +--- +'@objectstack/spec': patch +--- + +The protocol 16 → 17 conversion summaries, the `autonumberFormat` description and two metadata route descriptions no longer cite tracker numbers; each one states the decision behind it in words + +Clause-②: no + +A conversion's `summary` is the line an author reads when upgrading metadata: it is the "Change" column of `docs/protocol-upgrade-guide.md`'s protocol 16 → 17 table, the `to` text of `spec-changes.json`'s `converted[]` records, and what `os migrate meta --json` reports under `specChanges`. Fifty-six of the protocol-17 summaries pointed at an issue-tracker number for the reason behind a rewrite. The number goes; where the sentence did not already say what was decided, it now does. For example: + +- `action-execute-to-target` says the spec and the renderer had resolved `execute` / `target` in opposite directions, so one key now names the handler. +- `stack-api-require-auth-removed` names the declarations that replaced the deployment-wide opt-out: a public form, a share link or `book.audience: 'public'`. +- `retry-policy-converged` says why the merged default is 0 / 1: retry is opt-in, because a retry replays whatever the attempt already did. +- The flow-node alias entries say each one was an undeclared executor fallback that graduates into the conversion layer. + +The same goes for `FieldSchema.autonumberFormat`'s description (the `{0000}` default is a contract default every driver and the engine fallback read) and the descriptions of `GET /meta/:type/:name/layers` and `POST /meta/:type/:name/publish`. + +Text only: no conversion's id, surface, protocol step, transform or order changes, and no schema key, shape or default moves. A tool or test that matches the old summary text (for example a tracker-number suffix) needs the new spelling. The protocol 17 → 18 summaries are a later change.