From ea258bef9fdea9fa52d0f650b0edbfa0052cda44 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 08:12:01 +0000 Subject: [PATCH] spec(data): retire the import mapping lookup transform's steering params (#10329, ADR-0049) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Strict-delete ImportFieldMappingSchema.params' object/fromField/toField/ autoCreate — declared to steer the lookup transform, read by nothing: lookup copies the cell through and reference resolution runs off the target field's own metadata. All fifteen spellings (four canonical + eleven ex-aliases) land on guidance prescriptions; D2 conversion mapping-lookup-params-removed (protocol 18) strips the keys from stored sources; liveness sub-walk note records the retirement; docs warning deleted; changeset with FROM->TO. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01RadETjNRLALFLhFA3xehZP --- .changeset/mapping-lookup-params-removed.md | 65 ++++++++++ .../docs/data-modeling/import-mappings.mdx | 8 -- content/docs/references/data/mapping.mdx | 2 +- packages/spec/liveness/mapping.json | 2 +- packages/spec/src/conversions/registry.ts | 115 ++++++++++++++++++ packages/spec/src/data/mapping.test.ts | 98 ++++++++++++--- packages/spec/src/data/mapping.zod.ts | 96 +++++++++++++-- packages/spec/src/migrations/registry.ts | 13 +- 8 files changed, 363 insertions(+), 36 deletions(-) create mode 100644 .changeset/mapping-lookup-params-removed.md diff --git a/.changeset/mapping-lookup-params-removed.md b/.changeset/mapping-lookup-params-removed.md new file mode 100644 index 0000000000..678ef80708 --- /dev/null +++ b/.changeset/mapping-lookup-params-removed.md @@ -0,0 +1,65 @@ +--- +"@objectstack/spec": minor +--- + +feat(spec): retire the import mapping `lookup` transform's steering params (#10329, ADR-0049) + + + +**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep +launch-window convention ships it as `minor`; the migration prescription is +registered under protocol major 18, where `os migrate meta` users will look). + +`ImportFieldMappingSchema.params` declared four keys whose only stated purpose +was to steer the `lookup` transform — `object` ("Lookup Object"), `fromField` +("Match on"), `toField` ("Value to take"), `autoCreate` ("Create if missing") — +and the import path read none of them: `applyMappingToRows` handles `lookup` in +the same branch as `none` (the cell is copied through unchanged), and reference +resolution runs afterwards in `import-coerce.ts`, driven by the target field's +own metadata. Implementing them would be a second reference-resolution dialect +on the import path, which the code declines to build and the #10329 triage +ruling declined again. `autoCreate` was the one with teeth: it read as +"create the referenced record when nothing matches", and nothing was ever +created — with or without the key, an unresolved cell fails its row with +`import_reference_not_found`. + +FROM → TO, per key (all four are pure deletions — none ever had an effect to +lose, so removing them changes no import behaviour): + +- `fieldMapping[].params.object` → *(removed)*. The referenced object is the + target field's own declared `reference`. +- `fieldMapping[].params.fromField` → *(removed)*. The pipeline matches the + cell's display value against the referenced object itself. +- `fieldMapping[].params.toField` → *(removed)*. Resolution always writes the + referenced record's id. +- `fieldMapping[].params.autoCreate` → *(removed)*. Create or import the + referenced records first, then import the rows that point at them. + +One-line fix: delete the four keys (and any of their old alias spellings) from +`fieldMapping[].params`; `os migrate meta --from 17` lists the mechanical edits +for existing sources. + +The retirement kit: + +- `params` is a `strictObject`, so the keys are strict-deleted with a + `guidance` prescription per spelling — the four canonical keys and the + eleven ex-alias spellings (`lookupObject`/`targetObject`, + `match`/`matchOn`/`matchField`/`keyField`, `returnField`/`valueField`, + `create`/`createIfMissing`/`upsert`) all land on the full prescription + rather than a "did you mean" pointing at a key that is also gone (the + 17.0.0 #4509 treatment, one level down) +- ADR-0087 registration: the D2 conversion `mapping-lookup-params-removed` + (protocol 18), wired into the step-18 chain — `os migrate meta --from 17` + strips the four keys from stored `mappings[].fieldMapping[].params`. No + retired-key table entries: these keys sit one sub-walk level below the + authorable-surface drill (`data/ImportFieldMapping:params` is the walked + row, and it stays live), so there is no `defKey:name` row to register or + age out +- pin tests (`mapping.test.ts`): refusal pins per key asserting the + prescription (the `autoCreate` pin asserts the row-fails truth), alias + routing pins, and a surviving-surface pin (`value`/`valueMap`/`separator` + untouched) +- liveness ledger: `liveness/mapping.json`'s `fieldMapping` sub-walk boundary + note now records the retirement instead of parking the finding +- docs: the `import-mappings.mdx` warning about the inert params is deleted + along with the keys; the generated mapping reference no longer lists them diff --git a/content/docs/data-modeling/import-mappings.mdx b/content/docs/data-modeling/import-mappings.mdx index 712a37b734..004e525512 100644 --- a/content/docs/data-modeling/import-mappings.mdx +++ b/content/docs/data-modeling/import-mappings.mdx @@ -129,14 +129,6 @@ path, one is a deliberate pass-through, and one is refused: | `lookup` | **Pass-through.** The cell is copied unchanged, and the import's own reference resolution turns the display text into a record id afterwards — see [After the mapping](#after-the-mapping-cell-coercion). | — | | `javascript` | **Refused.** There is no server-side sandbox, and silently skipping a declared transform would corrupt data. `defineStack()` fails the build; a runtime-saved mapping is rejected by the import request with `400 UNSUPPORTED_TRANSFORM`. | — | - - The `lookup` transform's own `params` keys (`object`, `fromField`, `toField`, - `autoCreate`) parse, but the import path reads none of them: reference resolution is - driven by the **target object's field definitions**, not by the mapping. Do not write - them expecting them to steer anything — tracked as - [#10329](https://github.com/objectstack-ai/objectstack/issues/10329). - - For logic beyond these, transform the data before you post it, or model it as a [flow](/docs/automation) on the target object. diff --git a/content/docs/references/data/mapping.mdx b/content/docs/references/data/mapping.mdx index bfb55d2555..d8ae5397d8 100644 --- a/content/docs/references/data/mapping.mdx +++ b/content/docs/references/data/mapping.mdx @@ -30,7 +30,7 @@ const result = ImportFieldMappingSchema.parse(data); | **source** | `string \| string[]` | ✅ | Source column header(s) | | **target** | `string \| string[]` | ✅ | Target object field(s) | | **transform** | `Enum<'none' \| 'constant' \| 'lookup' \| 'split' \| 'join' \| 'javascript' \| 'map'>` | optional (default: `"none"`) | | -| **params** | `{ value?: any; object?: string; fromField?: string; toField?: string; … }` | optional | | +| **params** | `{ value?: any; valueMap?: Record; separator?: string }` | optional | | --- diff --git a/packages/spec/liveness/mapping.json b/packages/spec/liveness/mapping.json index cc19fb51f9..094d245760 100644 --- a/packages/spec/liveness/mapping.json +++ b/packages/spec/liveness/mapping.json @@ -30,7 +30,7 @@ "status": "live", "verifiedAt": "2026-08-01", "evidence": "packages/rest/src/import-mapping.ts:98-105, packages/rest/src/import-mapping.ts:115-167", - "note": "the pipeline itself: source/target/transform/params all consumed. none/constant/map/split/join applied in applyMappingToRows (`params.separator` :124, `.value` :132, `.valueMap` :137); `lookup` copies through for the pipeline's metaMap reference resolution; `javascript` is REJECTED with a 400 (no server sandbox — implement-or-reject-loudly). SUB-WALK BOUNDARY, recorded not hidden: `params`' lookup-specific keys (`object`/`fromField`/`toField`/`autoCreate`) are read by nothing — reference resolution comes from the target object's own field definitions, not from these — and they sit one level below the drill, so only this note governs them." + "note": "the pipeline itself: source/target/transform/params all consumed. none/constant/map/split/join applied in applyMappingToRows (`params.separator` :124, `.value` :132, `.valueMap` :137); `lookup` copies through for the pipeline's metaMap reference resolution; `javascript` is REJECTED with a 400 (no server sandbox — implement-or-reject-loudly). SUB-WALK BOUNDARY, resolved: `params`' lookup-specific keys (`object`/`fromField`/`toField`/`autoCreate`) were read by nothing — reference resolution comes from the target object's own field definitions, not from these — and were REMOVED in the 17.x line (#10329, ADR-0049 enforce-or-remove; strict deletion, so nothing dead remains one level below the drill). Every spelling — the four canonical keys and their eleven ex-aliases — now lands on a guidance prescription at parse; the mapping-lookup-params-removed conversion (protocol 18) strips them from stored sources. The surviving params keys (`value`/`valueMap`/`separator`) are all consumed by applyMappingToRows." }, "mode": { "status": "live", diff --git a/packages/spec/src/conversions/registry.ts b/packages/spec/src/conversions/registry.ts index b9384a0380..0e5eeb8f57 100644 --- a/packages/spec/src/conversions/registry.ts +++ b/packages/spec/src/conversions/registry.ts @@ -7556,6 +7556,120 @@ const recordHighlightsFieldIconRemoved: MetadataConversion = { }, }; +/** + * `mapping.fieldMapping[].params` lookup keys removed (#10329, ADR-0049 + * enforce-or-remove — the sub-walk half of the 17.0.0 #4509 mapping cleanup). + * + * `object` / `fromField` / `toField` / `autoCreate` declared a per-entry + * reference-resolution dialect that the import path never implemented: + * `applyMappingToRows` handles `lookup` in the same branch as `none` (the cell + * is copied through unchanged), and reference resolution runs afterwards in + * `import-coerce.ts` off the TARGET FIELD's own metadata — never off these + * keys. Implementing them (a second reference-resolution dialect on the import + * path) is what the code comment in `packages/rest/src/import-mapping.ts` + * declines to build, and the #10329 triage ruling confirms that posture. + * + * `autoCreate` was the one with teeth: it read as "create the referenced + * record when nothing matches", and nothing was ever created — with or without + * the key, an unresolved cell fails its row with `import_reference_not_found`. + * The schema prescription says so outright, because an author who believed the + * key is one support ticket away from "my import used to create these". + * + * **A pure lossless delete.** None of the four ever had an effect to lose, so + * stripping them preserves observed import behaviour exactly. + * + * Scoped to the `mappings` collection, then drilled two levels down + * (`fieldMapping[]` is an ARRAY one level below the item, `params` a dict one + * below that), so the top-level-only `stripKeys` runs per entry's params bag — + * the `metric-filters-removed` shape, one level deeper. Deliberately narrow: + * `object` / `targetObject`-style keys are live all over the tree, and a + * stack-wide strip would delete enforced keys from other types. + */ +const mappingLookupParamsRemoved: MetadataConversion = { + id: 'mapping-lookup-params-removed', + toMajor: 18, + retiredFromLoadPath: true, + surface: 'mapping.fieldMapping[].params.object / .fromField / .toField / .autoCreate', + summary: + "mapping lookup params 'object'/'fromField'/'toField'/'autoCreate' removed (#10329, " + + 'ADR-0049 — the import path never read them: `lookup` copies the cell through and ' + + "reference resolution runs off the target field's own metadata. `autoCreate` never " + + 'created anything — an unresolved reference fails the row either way)', + apply(stack, emit) { + const RETIRED = ['object', 'fromField', 'toField', 'autoCreate']; + return mapCollection(stack, 'mappings', (m, path) => { + const entries = m.fieldMapping; + if (!Array.isArray(entries)) return m; + let touched = false; + const nextEntries = entries.map((entry, i) => { + if (!isDict(entry)) return entry; + const params = entry.params; + if (!isDict(params)) return entry; + const stripped = stripKeys(params, RETIRED, emit, `${path}.fieldMapping[${i}].params`); + if (stripped === params) return entry; + touched = true; + return { ...entry, params: stripped }; + }); + if (!touched) return m; + return { ...m, fieldMapping: nextEntries }; + }); + }, + fixture: { + before: { + mappings: [{ + name: 'csv_import_contacts', + targetObject: 'contact', + fieldMapping: [ + // The retired shape: a lookup entry steering nothing. + { + source: 'Account Name', + target: 'account_id', + transform: 'lookup', + params: { object: 'account', fromField: 'name', toField: 'id', autoCreate: true }, + }, + // A live params bag on another transform rides through untouched — + // the strip dispatches on key presence, and copy-on-write keeps the + // reference. + { + source: 'Status', + target: 'status', + transform: 'map', + params: { valueMap: { Open: 'open' } }, + }, + // No params at all — untouched. + { source: 'Email', target: 'email' }, + ], + }], + }, + after: { + mappings: [{ + name: 'csv_import_contacts', + targetObject: 'contact', + fieldMapping: [ + // The emptied bag stays: the conversion strips KEYS, and deleting + // the bag itself would be a second, unprescribed edit. `params: {}` + // parses clean. + { + source: 'Account Name', + target: 'account_id', + transform: 'lookup', + params: {}, + }, + { + source: 'Status', + target: 'status', + transform: 'map', + params: { valueMap: { Open: 'open' } }, + }, + { source: 'Email', target: 'email' }, + ], + }], + }, + // Four notices: one per retired key on the single lookup entry. + expectedNotices: 4, + }, +}; + export const CONVERSIONS_BY_MAJOR: Readonly> = { 11: [flowNodeHttpRename, pageKindJsxToHtml, flowNodeFilterAlias, objectCompactLayoutRename], 13: [stackRolesToPositions, owdLegacyReadAliases, sharingRecipientRoleToPosition], @@ -7637,6 +7751,7 @@ export const CONVERSIONS_BY_MAJOR: Readonly { expect(mapping.params?.value).toBe('active'); }); - it('should accept lookup transform', () => { + it('should accept lookup transform (a pass-through with no params of its own)', () => { const mapping = ImportFieldMappingSchema.parse({ source: 'account_name', target: 'account_id', transform: 'lookup', - params: { - object: 'account', - fromField: 'name', - toField: 'id', - autoCreate: false - } }); expect(mapping.transform).toBe('lookup'); - expect(mapping.params?.object).toBe('account'); - expect(mapping.params?.fromField).toBe('name'); - expect(mapping.params?.toField).toBe('id'); + }); + + // ── `params` lookup keys retired in the 17.x line (#10329, ADR-0049) ─────── + // + // `object` / `fromField` / `toField` / `autoCreate` declared a per-entry + // reference-resolution dialect the import path never implemented: `lookup` + // copies the cell through and resolution runs afterwards off the target + // field's own metadata. These pin the REJECTION carrying the prescription, + // not just the absence — the schema is strict, so a bare "unrecognized key" + // would already fail; what must survive refactors is the guidance. + + const lookupEntry = { source: 'account_name', target: 'account_id', transform: 'lookup' as const }; + + it('rejects the retired `params.object` with the target-field-metadata prescription', () => { + expect(() => ImportFieldMappingSchema.parse({ + ...lookupEntry, + params: { object: 'account' }, + })).toThrow(/params\.object.*removed.*TARGET FIELD/s); + }); + + it('rejects the retired `params.fromField` with the display-value-matching prescription', () => { + expect(() => ImportFieldMappingSchema.parse({ + ...lookupEntry, + params: { fromField: 'name' }, + })).toThrow(/params\.fromField.*removed.*display.*value/s); + }); + + it('rejects the retired `params.toField` with the record-id prescription', () => { + expect(() => ImportFieldMappingSchema.parse({ + ...lookupEntry, + params: { toField: 'id' }, + })).toThrow(/params\.toField.*removed.*record.*id/s); + }); + + it('rejects the retired `params.autoCreate` saying what ACTUALLY happens (row fails)', () => { + // The one with teeth: the key read as "create the referenced record when + // nothing matches". Nothing was ever created — the row fails with an + // unresolved-reference error either way — so the prescription must say so + // rather than merely "removed". + const parse = () => ImportFieldMappingSchema.parse({ + ...lookupEntry, + params: { autoCreate: true }, + }); + expect(parse).toThrow(/params\.autoCreate.*removed/s); + expect(parse).toThrow(/nothing was ever created/is); + expect(parse).toThrow(/import_reference_not_found/s); + }); + + it('routes the retired ALIAS spellings to the same prescriptions', () => { + // The alias table used to fold eleven spellings onto the four keys. Leaving + // them as aliases would answer "did you mean `fromField`?" — a rename + // suggestion pointing at a key that is also gone, i.e. a second rejection. + expect(() => ImportFieldMappingSchema.parse({ + ...lookupEntry, params: { lookupObject: 'account' }, + })).toThrow(/params\.object.*removed/s); + expect(() => ImportFieldMappingSchema.parse({ + ...lookupEntry, params: { matchOn: 'name' }, + })).toThrow(/params\.fromField.*removed/s); + expect(() => ImportFieldMappingSchema.parse({ + ...lookupEntry, params: { returnField: 'id' }, + })).toThrow(/params\.toField.*removed/s); + expect(() => ImportFieldMappingSchema.parse({ + ...lookupEntry, params: { createIfMissing: true }, + })).toThrow(/params\.autoCreate.*removed/s); + }); + + it('leaves the surviving params surface intact', () => { + const mapping = ImportFieldMappingSchema.parse({ + source: 'status', + target: 'status_code', + transform: 'map', + params: { valueMap: { Open: 'open' } }, + }); + expect(mapping.params).not.toHaveProperty('object'); + expect(mapping.params).not.toHaveProperty('fromField'); + expect(mapping.params).not.toHaveProperty('toField'); + expect(mapping.params).not.toHaveProperty('autoCreate'); + expect(mapping.params?.valueMap).toHaveProperty('Open', 'open'); }); it('should accept map transform', () => { @@ -375,14 +444,11 @@ describe('MappingSchema', () => { params: { value: 'active' } }, { + // `lookup` is a pass-through: the import pipeline resolves the + // reference from the target field's own metadata (#10329). source: 'account_name', target: 'account_id', - transform: 'lookup', - params: { - object: 'account', - fromField: 'name', - toField: 'id' - } + transform: 'lookup' }, { source: ['first_name', 'last_name'], diff --git a/packages/spec/src/data/mapping.zod.ts b/packages/spec/src/data/mapping.zod.ts index adea9ffa8d..cc744f5ebc 100644 --- a/packages/spec/src/data/mapping.zod.ts +++ b/packages/spec/src/data/mapping.zod.ts @@ -76,6 +76,79 @@ const MAPPING_RETIRED_KEY_GUIDANCE: Readonly> = { chunkSize: RETIRED_BATCH_SIZE, }; +/** + * `params`' lookup-steering keys, retired in the 17.x line (#10329, ADR-0049). + * + * `object` / `fromField` / `toField` / `autoCreate` declared a per-entry + * reference-resolution dialect that the import path never implemented: + * `applyMappingToRows` handles `lookup` in the same branch as `none` (the cell + * is copied through unchanged), and reference resolution happens afterwards in + * `import-coerce.ts`, driven by the TARGET FIELD's own metadata — never by + * these keys. Implementing them was considered and declined (a second + * reference-resolution dialect on the import path; the code comment in + * `packages/rest/src/import-mapping.ts` declines it and the #10329 triage + * ruling confirms), so under ADR-0049 they go. + * + * `autoCreate` is the one with teeth: it reads as "create the referenced + * record when nothing matches", and what actually happens — with or without + * the key — is that the row FAILS with an unresolved-reference error + * (`import_reference_not_found`). The guidance says so outright. + * + * The alias spellings (`lookupObject`/`targetObject`, `match`/`matchOn`/ + * `matchField`/`keyField`, `returnField`/`valueField`, `create`/ + * `createIfMissing`/`upsert`) are listed too: an author who learned any of + * them should land on the prescription, not on a "did you mean" pointing at a + * key that is also gone. + */ +const RETIRED_LOOKUP_OBJECT = + '`fieldMapping[].params.object` was removed in @objectstack/spec 17 (#10329, ADR-0049) — the ' + + '`lookup` transform never read it: the cell is copied through unchanged and the import ' + + 'pipeline resolves references from the TARGET FIELD\'s own metadata (the field\'s declared ' + + '`reference` names the lookup object), so "Lookup Object" steered nothing. Delete the key; ' + + 'point `target` at a reference field and the referenced object is the field\'s own ' + + '`reference`. ' + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + +const RETIRED_LOOKUP_FROM_FIELD = + '`fieldMapping[].params.fromField` was removed in @objectstack/spec 17 (#10329, ADR-0049) — ' + + 'the `lookup` transform never read it: the import pipeline matches the cell\'s display ' + + 'value (name / email / id) against the referenced object itself, not against a ' + + 'mapping-declared match field, so "Match on" steered nothing. Delete the key. ' + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + +const RETIRED_LOOKUP_TO_FIELD = + '`fieldMapping[].params.toField` was removed in @objectstack/spec 17 (#10329, ADR-0049) — ' + + 'the `lookup` transform never read it: reference resolution always writes the referenced ' + + 'record\'s id (what a reference column stores), so "Value to take" steered nothing. ' + + 'Delete the key. ' + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + +const RETIRED_LOOKUP_AUTO_CREATE = + '`fieldMapping[].params.autoCreate` was removed in @objectstack/spec 17 (#10329, ADR-0049) — ' + + 'it read as "create the referenced record when nothing matches", and nothing was ever ' + + 'created: with or without this key, a cell that resolves to no record FAILS its row with an ' + + 'unresolved-reference error (`import_reference_not_found`). Delete the key; create or ' + + 'import the referenced records first, then import the rows that point at them. ' + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + +const PARAMS_RETIRED_KEY_GUIDANCE: Readonly> = { + object: RETIRED_LOOKUP_OBJECT, + lookupObject: RETIRED_LOOKUP_OBJECT, + targetObject: RETIRED_LOOKUP_OBJECT, + fromField: RETIRED_LOOKUP_FROM_FIELD, + match: RETIRED_LOOKUP_FROM_FIELD, + matchOn: RETIRED_LOOKUP_FROM_FIELD, + matchField: RETIRED_LOOKUP_FROM_FIELD, + keyField: RETIRED_LOOKUP_FROM_FIELD, + toField: RETIRED_LOOKUP_TO_FIELD, + returnField: RETIRED_LOOKUP_TO_FIELD, + valueField: RETIRED_LOOKUP_TO_FIELD, + autoCreate: RETIRED_LOOKUP_AUTO_CREATE, + create: RETIRED_LOOKUP_AUTO_CREATE, + createIfMissing: RETIRED_LOOKUP_AUTO_CREATE, + upsert: RETIRED_LOOKUP_AUTO_CREATE, +}; + /** * Transformation Logic * Built-in helpers for converting data during import. @@ -150,22 +223,27 @@ export const ImportFieldMappingSchema = lazySchema(() => strictObject({ history: MAPPING_HISTORY, aliases: { default: 'value', defaultValue: 'value', constant: 'value', - lookupObject: 'object', targetObject: 'object', - match: 'fromField', matchOn: 'fromField', matchField: 'fromField', keyField: 'fromField', - returnField: 'toField', valueField: 'toField', - create: 'autoCreate', createIfMissing: 'autoCreate', upsert: 'autoCreate', + // NOTE: `lookupObject` / `targetObject` / `match` / `matchOn` / + // `matchField` / `keyField` / `returnField` / `valueField` / `create` / + // `createIfMissing` / `upsert` were aliases onto the four lookup keys + // removed in the 17.x line (#10329). An alias pointing at a key that no + // longer exists routes the author into a second rejection, so their + // spellings fall through to the `guidance` prescriptions instead — + // the 17.0.0 (#4509) treatment, one level down. map: 'valueMap', mapping: 'valueMap', values: 'valueMap', valueMapping: 'valueMap', delimiter: 'separator', splitOn: 'separator', joinWith: 'separator', }, + guidance: PARAMS_RETIRED_KEY_GUIDANCE, }, { // Constant value: z.unknown().optional(), - // Lookup - object: z.string().optional(), // Lookup Object - fromField: z.string().optional(), // Match on (e.g. "name") - toField: z.string().optional(), // Value to take (e.g. "id") - autoCreate: z.boolean().optional(), // Create if missing + // `object` / `fromField` / `toField` / `autoCreate` — the `lookup` + // transform's steering keys — were removed in the 17.x line (#10329, + // ADR-0049); see PARAMS_RETIRED_KEY_GUIDANCE above. The live mechanism: + // `lookup` copies the cell through and the import pipeline resolves the + // reference from the target field's own metadata (`import-coerce.ts`), + // so there was never anything for these to steer. // Map valueMap: z.record(z.string(), z.unknown()).optional(), // { "Open": "draft" } diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index dcefed1260..08ec169a8c 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -5133,7 +5133,17 @@ const step18: MigrationStep = { 'on the highlight chip). The mechanical conversion strips the key from the object ' + 'entries of every `record:highlights` `fields[]` (pure lossless delete — the chip ' + 'renders label and value only, so it never had an effect to lose); there is no ' + - 'replacement, and the live neighbour `readonly` (#5176) is untouched.', + 'replacement, and the live neighbour `readonly` (#5176) is untouched. ' + + 'It also retires the import mapping `lookup` transform\'s steering params (#10329, ' + + 'ADR-0049 enforce-or-remove — the sub-walk half of 17.0.0\'s #4509 mapping cleanup): ' + + '`fieldMapping[].params.object` / `.fromField` / `.toField` / `.autoCreate` declared a ' + + 'per-entry reference-resolution dialect the import path never implemented — `lookup` ' + + 'copies the cell through and resolution runs off the target field\'s own metadata — ' + + 'and `autoCreate` read as create-if-missing while an unresolved reference actually ' + + 'fails the row (`import_reference_not_found`), with or without the key. The eleven ' + + 'alias spellings convert to guidance so every spelling lands on the prescription; the ' + + 'mechanical conversion strips the four keys from stored sources (pure lossless ' + + 'deletes — none ever had an effect to lose).', conversionIds: [ 'field-malformed-scale-precision-removed', 'record-chatter-position-vocabulary', @@ -5143,6 +5153,7 @@ const step18: MigrationStep = { 'field-column-lists-canonicalized', 'metric-filters-removed', 'record-highlights-field-icon-removed', + 'mapping-lookup-params-removed', ], semantic: [ // One file per entry under `entries/semantic/`, concatenated here sorted by