Skip to content

Commit f108e6d

Browse files
feat(spec): a field declares the key(s) it was renamed from — renamedFrom on FieldSchema (#30934)
Fixes #30880 Clause-②: yes (widening) Metadata-protocol: added Skills-docs: follow-up #30885 — `skills/**` does not teach the key yet: it is declared ahead of the upgrade step that carries the data (rename execution, #30885), and that step owns the skills line; this diff documents it in `content/docs/data-modeling/fields.mdx` and the generated references ## 维护者速读 - 改了什么:字段定义新增可选属性 `renamedFrom: string[]`,包作者在新字段上写旧名(如 `renamedFrom: ['company']`),并加五条最小校验(旧名须合法、不能是自己、不能和本对象现存字段重名、不能被两个字段同时认领、列表内不重复),违者在解析时拒收并点名对象、字段和旧名。 - 为什么改:按你在 cloud#3652 的裁决(Q3「默认拒绝,需显式映射(推荐)」与「写进清单 renamedFrom」),改名映射写在清单里;这推翻了 #24443 裁决 A 第 5 条「规范里不加改名写法」。本 PR 只做协议这一层,真正搬数据的是 #30885,升级预览读它是 #30884。 - 风险与回滚:不写这个键的对象解析结果不变;`FieldSchema` 是严格模式,带 `renamedFrom` 的制品会被比本版本旧的运行时拒收(未知键)。在 #30885 落地前,这个键不会让任何升级搬数据。回滚即还原本 PR。 - 先看:`packages/spec/src/data/object.zod.ts`(`refuseDishonestRenamedFrom`,四条整表校验)、`packages/spec/src/data/field.zod.ts`(新键与 describe 文案)、`packages/spec/liveness/field.json`(`planned` 行与翻转条件)。 - 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run src/data/field-renamed-from.test.ts`,预期 11 个用例全绿。 - 需要你做的:无,席位落地(落地前席位先做一次契约复核)。 ## What changes - **`FieldSchema.renamedFrom: string[]`, optional** (`packages/spec/src/data/field.zod.ts`, at the tail of the shape so the abbreviated type summaries in the references do not move). Each entry is an earlier key of the same field, in the field-key grammar `^[a-z_][a-z0-9_]*$`; a non-key entry is refused by `FieldSchema` itself with a message naming the entry. The `.describe()` says what the key means, that it is read only at package upgrade, and that it is declared ahead of the upgrade step that carries the data. - **`refuseDishonestRenamedFrom`** in `ObjectSchema`'s `superRefine`, beside `refuseStageFieldWithoutKinds` (`packages/spec/src/data/object.zod.ts`). One located `custom` issue at `fields.FIELD.renamedFrom.INDEX` per offending entry, naming the object, the field and the old key, when the entry is the field's own key, repeats an earlier entry of the same list, is a key another field of the object still declares, or is an old key an earlier field already claims (reported on the later claimant, naming the first). The own-key and repeat checks sit here rather than in `FieldSchema` because `FieldSchema.name` is optional (the map key is the field's key) and because the card asks every refusal to name the object. - **Liveness:** `renamedFrom` is a `planned` row in `packages/spec/liveness/field.json`, naming #30885 (rename execution) as the reader that flips it to `live` with its `file#symbol` pointer, and #30884 (upgrade plan) as a second reader. No `authorWarn`, as the card states. `liveness/state-counts/field.md` regenerated. - **Mirrors the gates require:** the authorable-surface shard (`packages/spec/authorable-surface/data.json`, +1 row), the generated references (`content/docs/references/**`), the field-form omission ledger (`metadata-form-zod-reconciliation.test.ts`: not offered in the field form while `planned`, the `conditionalFormatting` precedent), the two maps pinned exhaustive over `FieldSchema` (`FIELD_KEY_STORAGE_CLASS` in driver-sql as `storage`; `FIELD_REFERENCE_POSITIONS` in metadata-core as `keep`, since the parse refuses any entry naming a live field), and the `/meta/types` field property count pin (82 to 83). - **Docs:** one row under a new "Package upgrades" heading in `content/docs/data-modeling/fields.mdx`. - **Changeset:** `@objectstack/spec` minor, `@objectstack/driver-sql` and `@objectstack/metadata-core` patch. Not added, per the card: an object-level or package-level rename map, renames of objects or other metadata types, type compatibility checking (the upgrade plan judges it against the installed version), and any runtime behaviour. ### Rulings this lands - Epic objectstack-ai/cloud#3652, maintainer rulings (record on the epic): Q3 「默认拒绝,需显式映射(推荐)」, and on where the mapping lives, 「写进清单 renamedFrom」. - These supersede item 5 of objectstack#24443's ruling A (record 6259332195): "No rename spelling is added to the spec". Items 1 to 4 of that record stand. The card notes that no ADR text forbids the key (ADR-0126's dated note describes the access stages, it is not a rule), so no ADR amendment rides here. ### Publish-time consequence, for the contract review `FieldSchema` is strict. An artifact that carries `renamedFrom` is refused, as an unrecognized key, by every runtime older than this release. A package that adds the key therefore needs its installing runtime at this release or later; a package that does not write it is unaffected. The changeset says the same. ## Tests - New `packages/spec/src/data/field-renamed-from.test.ts` (11 cases): one and two earlier keys parse and are kept as written; an object without the key parses with no `renamedFrom` materialized on any field; four grammar refusals at `renamedFrom.0`; each of the four map-half refusals pinned by `code` and `path` and by its sentence (object, field, old key, reason clause). - `os validate` acceptance, run by hand with the built CLI against two throwaway `defineStack` fixtures (removed after): `renamedFrom: ['company']` exits 0; `renamedFrom: ['status']` beside a live `status` field exits 1 with `objects.0.fields.account_name.renamedFrom.0: Field account_name on object crm_account declares renamedFrom entry 'status', but object crm_account still declares a field status: that name is still in use, so no field was renamed away from it. ...` ## Acceptance notes - **The key is inert until #30885.** Between this landing and the rename execution, an upgrade that meets `renamedFrom` does what it does today (additive sync). The describe, the docs row and the changeset each say so; the ledger row's note records the card's plan that #30884 refuses a declared rename by name until #30885 lands. That refusal is #30884's to build: until it lands, nothing refuses either. - **Extension fields get the field half only.** `ObjectExtensionSchema.fields` parses `FieldSchema`, so an extension field may carry `renamedFrom` with its grammar judged, but the map half runs only in `ObjectSchema`'s refinement, where the whole field map is visible. The card scopes the checks to the object; no extension behaviour is added. - **The grammar refusal names the entry, not the object or field.** `FieldSchema` sees one field at a time and `name` is optional; the issue path locates it. Every map-half refusal names all three. - **Empty list:** `renamedFrom: []` parses (no rename declared). The card's minimal list does not refuse it, so this PR does not. - **One ablation per rule** (each through `scripts/ablation-replace.mjs`, anchor hit x1 to x0, restored, `git diff HEAD` empty after the run): grammar, repeat, live key and double claim each turned their pin red on the first run. The own-key ablation stayed green on its first run: the pin's `toContain("the field's own key")` also matched the shared prescription sentence, so the assertion was vacuous. The pin now asserts the reason clause, and the re-run ablation turned it red (`refuses the field's own key` failed, 1 of 11). - **`Metadata-protocol:` line.** The claim carried `Metadata-protocol: none`; this body declares `added`, because AGENTS.md defines a new authorable key as `added` and `check-changeset-no-major` refuses `none` beside a changed `authorable-surface/` shard. Reported to the seat. - `origin/main` moved 10 commits after the branch point; none touches `packages/spec`, `packages/metadata-core`, `packages/drivers/driver-sql`, `packages/metadata-protocol` or `content/docs` (only other cards' changesets and an ADR anchor), so the branch was not merged before opening. ## Verification All readings at head `4ad4222ab8` unless stated. <details> <summary>packages/spec full suite and typecheck (verify lock)</summary> - `pnpm exec vitest run --maxWorkers=2` in `packages/spec`: exit 0, `Test Files 909 passed (909)`, `Tests 27388 passed | 1 todo (27389)`. - `pnpm typecheck` in `packages/spec`: exit 0. - An earlier full run at `e3c18ce2a8` was red on exactly one test, `metadata-form-zod-reconciliation.test.ts` (the field form offered no control for `renamedFrom`); fixed by the omission-ledger row, then green. </details> <details> <summary>Other packages the diff touches (verify lock)</summary> - metadata-core `object-schema-fls-references.test.ts` + `objects/builtin-column-storage-attributes.test.ts`: exit 0, 67 passed. - driver-sql `builtin-column-collision.test.ts`: exit 0, 11 passed. - metadata-protocol `protocol.meta-types-degenerate-derivation.test.ts`: exit 0, 18 passed (red at 82 before the count pin moved to 83). - cli `test/data-model-rules.test.ts` (unit project): exit 0, 56 passed. lint `validate-expressions`, `validate-security-posture`, `validate-expressions.formula-one-hop`, `validate-predicate-path-refs`: exit 0, 737 passed (both at `4ad4222ab8`'s parent commits; the commits after touch only test files of other packages). - `typecheck` for metadata-core, driver-sql, metadata-protocol: exit 0 each. </details> <details> <summary>Derived gates (dispatch-gates --commands at 4ad4222, 106 families)</summary> - 105 exit 0, 1 exit 3 (NOT MEASURED): `pnpm check:dual-build-cjs-loads` refuses without a full `pnpm build` of every package; it runs in CI. - `dispatch-gates --ran` with exit codes recorded: `✓ 106 derived famil(ies) accounted for — 105 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)`. - `pnpm --filter @objectstack/spec check:generated`: `✓ All 14 generated artifacts are up to date`. `check:liveness`: `field 112 classified (live 105, planned 6, dead 1)`, every row classified, no orphan. - Families CI runs on this PR (Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core and the rest) are not re-run here; their check-runs are the reading. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01Q4EjjRhUZjdy8VXs6Kkur2)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 33043bd commit f108e6d

15 files changed

Lines changed: 294 additions & 2 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/driver-sql': patch
4+
'@objectstack/metadata-core': patch
5+
---
6+
7+
feat(spec): a field declares the key or keys it was renamed from — `renamedFrom` on `FieldSchema`
8+
9+
Clause-②: yes (widening)
10+
Metadata-protocol: added
11+
Skills-docs: follow-up #30885 — `skills/**` does not teach the key yet: it is declared ahead of the upgrade step that carries the data (rename execution, #30885), and that step owns the skills line; this diff documents it in `content/docs/data-modeling/fields.mdx` and the generated references
12+
13+
A package author can now say that a field was renamed, so that an upgrade knows a rename from a drop-and-add. On the new field, write the old key: `account_name: Field.text({ label: 'Account Name', renamedFrom: ['company'] })`. A field renamed twice lists both earlier keys. This is the manifest half of the rename mapping ruled on epic objectstack-ai/cloud#3652 (Q3 「默认拒绝,需显式映射(推荐)」, and on where the mapping lives, 「写进清单 renamedFrom」), and it supersedes item 5 of objectstack#24443's ruling A ("No rename spelling is added to the spec").
14+
15+
- **`@objectstack/spec`: `FieldSchema.renamedFrom: string[]`, optional.** Each entry is an earlier key of this same field, in the field-key grammar (lowercase snake_case). The parse refuses, in one located issue naming the object, the field and the old key: an entry that is not a field key (judged by `FieldSchema`), and, in `ObjectSchema`'s refinement beside the stage-field check, an entry that is the field's own key, an entry repeated within one list, an entry that another field of the same object still declares (the old name is still in use), and an old key that two fields of the same object both claim. Type compatibility between the old column and the new field is not judged at parse: it depends on the installed version, so the upgrade plan judges it. An object that does not write the key parses exactly as before; nothing materializes.
16+
- **Declared ahead of its readers.** Nothing in this release reads the key at upgrade or anywhere else. The upgrade plan (#30884) and the rename execution (#30885) read it; until the execution ships, no upgrade carries a column on this declaration. The liveness ledger records the row as `planned`, naming #30885 as the reader that flips it to `live`.
17+
- **Publish-time consequence.** `FieldSchema` is strict, so an artifact that carries `renamedFrom` is refused, as an unrecognized key, by every runtime older than this release. A package that adds the key needs an installing runtime at this release or later; a package that does not write it is unaffected.
18+
- **`@objectstack/driver-sql` and `@objectstack/metadata-core`: the key is classified in the two maps pinned exhaustive over `FieldSchema`.** The SQL driver's builtin-column classification files it as `storage` (on `id`, `created_at` or `updated_at` the platform's own column takes no data from an old one, so the builtin-column diagnostic reports it as discarded there). The field-level security scrub keeps it: the parse refuses any entry that names a live field, so no entry can name a field a caller is denied.

‎content/docs/data-modeling/fields.mdx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,12 @@ These properties are available on all field types:
458458
459459
Database indexes are declared at the object level in the object's `indexes[]` array — there is no per-field `index` flag. See [Indexing](/docs/data-modeling/indexing).
460460
461+
### Package upgrades
462+
463+
| Property | Type | Default | Description |
464+
| :--- | :--- | :--- | :--- |
465+
| `renamedFrom` | `string[]` | — | Earlier keys of this field (snake_case), e.g. `renamedFrom: ['company']` on a field now keyed `account_name`: on a package upgrade from a version that has the old key and not this one, the platform carries the old column's data to this field instead of adding an empty column. Each entry once; never the field's own key, a key another field of the object still declares, or an old key another field also claims — each refused at parse. Read only at package upgrade, and declared ahead of the upgrade step that carries the data: until that step ships, no upgrade carries a column on this declaration |
466+
461467
### Security
462468
463469
| Property | Type | Default | Description |

‎content/docs/references/data/field.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ const result = CurrencyConfigSchema.parse(data);
125125
| **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). |
126126
| **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. |
127127
| **externalId** | `boolean` | optional (default: `false`) | Not read: no upsert, import or seed matches on this flag. A seed dataset names its match field in its own `externalId`; an import names it in the mapping's `upsertKey`. |
128+
| **renamedFrom** | `string[]` | optional | Earlier keys of THIS field, written in the field-key grammar (lowercase snake_case), e.g. `renamedFrom: ['company']` on a field now keyed `account_name`. On a package upgrade from a version that has one of these keys and not this one, the platform carries that column's data to this field instead of adding an empty column. A field renamed twice lists both earlier keys. Each entry once; never this field's own key; never a key another field of the same object still declares (that name is still in use); never an old key another field of the same object also claims. Read only at package upgrade: no record read, write or query reads it, and the type of the old column is judged by the upgrade plan, not here. Declared ahead of the upgrade step that carries the data: until that step ships, no upgrade carries a column on this declaration. |
128129
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
129130
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
130131
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |

‎content/docs/references/data/object.mdx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ const result = ApiMethod.parse(data);
294294
| **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). |
295295
| **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. |
296296
| **externalId** | `boolean` | optional (default: `false`) | Not read: no upsert, import or seed matches on this flag. A seed dataset names its match field in its own `externalId`; an import names it in the mapping's `upsertKey`. |
297+
| **renamedFrom** | `string[]` | optional | Earlier keys of THIS field, written in the field-key grammar (lowercase snake_case), e.g. `renamedFrom: ['company']` on a field now keyed `account_name`. On a package upgrade from a version that has one of these keys and not this one, the platform carries that column's data to this field instead of adding an empty column. A field renamed twice lists both earlier keys. Each entry once; never this field's own key; never a key another field of the same object still declares (that name is still in use); never an old key another field of the same object also claims. Read only at package upgrade: no record read, write or query reads it, and the type of the old column is judged by the upgrade plan, not here. Declared ahead of the upgrade step that carries the data: until that step ships, no upgrade carries a column on this declaration. |
297298
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
298299
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
299300
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |
@@ -666,6 +667,7 @@ const result = ApiMethod.parse(data);
666667
| **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). |
667668
| **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. |
668669
| **externalId** | `boolean` | optional (default: `false`) | Not read: no upsert, import or seed matches on this flag. A seed dataset names its match field in its own `externalId`; an import names it in the mapping's `upsertKey`. |
670+
| **renamedFrom** | `string[]` | optional | Earlier keys of THIS field, written in the field-key grammar (lowercase snake_case), e.g. `renamedFrom: ['company']` on a field now keyed `account_name`. On a package upgrade from a version that has one of these keys and not this one, the platform carries that column's data to this field instead of adding an empty column. A field renamed twice lists both earlier keys. Each entry once; never this field's own key; never a key another field of the same object still declares (that name is still in use); never an old key another field of the same object also claims. Read only at package upgrade: no record read, write or query reads it, and the type of the old column is judged by the upgrade plan, not here. Declared ahead of the upgrade step that carries the data: until that step ships, no upgrade carries a column on this declaration. |
669671
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
670672
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
671673
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |

‎content/docs/references/system/migration.mdx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ Add a new field to an existing object
128128
| **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). |
129129
| **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. |
130130
| **externalId** | `boolean` | optional (default: `false`) | Not read: no upsert, import or seed matches on this flag. A seed dataset names its match field in its own `externalId`; an import names it in the mapping's `upsertKey`. |
131+
| **renamedFrom** | `string[]` | optional | Earlier keys of THIS field, written in the field-key grammar (lowercase snake_case), e.g. `renamedFrom: ['company']` on a field now keyed `account_name`. On a package upgrade from a version that has one of these keys and not this one, the platform carries that column's data to this field instead of adding an empty column. A field renamed twice lists both earlier keys. Each entry once; never this field's own key; never a key another field of the same object still declares (that name is still in use); never an old key another field of the same object also claims. Read only at package upgrade: no record read, write or query reads it, and the type of the old column is judged by the upgrade plan, not here. Declared ahead of the upgrade step that carries the data: until that step ships, no upgrade carries a column on this declaration. |
131132
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
132133
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
133134
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |
@@ -562,6 +563,7 @@ Add a new field to an existing object
562563
| **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). |
563564
| **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. |
564565
| **externalId** | `boolean` | optional (default: `false`) | Not read: no upsert, import or seed matches on this flag. A seed dataset names its match field in its own `externalId`; an import names it in the mapping's `upsertKey`. |
566+
| **renamedFrom** | `string[]` | optional | Earlier keys of THIS field, written in the field-key grammar (lowercase snake_case), e.g. `renamedFrom: ['company']` on a field now keyed `account_name`. On a package upgrade from a version that has one of these keys and not this one, the platform carries that column's data to this field instead of adding an empty column. A field renamed twice lists both earlier keys. Each entry once; never this field's own key; never a key another field of the same object still declares (that name is still in use); never an old key another field of the same object also claims. Read only at package upgrade: no record read, write or query reads it, and the type of the old column is judged by the upgrade plan, not here. Declared ahead of the upgrade step that carries the data: until that step ships, no upgrade carries a column on this declaration. |
565567
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
566568
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
567569
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |

‎packages/drivers/driver-sql/src/builtin-column-collision.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export const FIELD_KEY_STORAGE_CLASS: Readonly<Record<string, FieldKeyClass>> =
8787
returnType: 'storage', // the formula's stored/returned type
8888
summaryOperations: 'storage', // rollup storage
8989
currencyConfig: 'storage', // currency mode can change what is physically stored
90+
renamedFrom: 'storage', // the earlier column(s) a package upgrade carries into this one (cloud#3652 ruling D3) — this driver's DDL does not read it yet, and on a builtin column name the platform's own column takes no data from an old one
9091

9192
// ---- presentation: honoured by layers other than the DDL -----------------
9293
name: 'presentation', // identity; the physical column is the field KEY (`columnName` was retired, #2377)

‎packages/metadata-core/src/object-schema-fls-references.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,11 @@ export const FIELD_REFERENCE_POSITIONS: Readonly<Record<string, Scrub>> = {
636636
// A boolean facet of this lookup: it declares the target's name readable
637637
// by every reader of this record (#29171) and names no field.
638638
referenceNameVisible: keep,
639+
// The earlier keys of THIS field (cloud#3652 ruling D3): names of columns
640+
// the field had in earlier package versions. The parse refuses any entry
641+
// that is a key another field of the object still declares, so no entry
642+
// can name a field a caller is denied — kept, like a boolean facet.
643+
renamedFrom: keep,
639644
// Names of ANOTHER object's fields — that object's projection governs them.
640645
// `displayField` / `descriptionField` / `lookupColumns` / `lookupFilters`
641646
// name fields of the `reference` target (the lookup picker reads them);

‎packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,8 @@ describe('#17501 — /meta/types serves a real schema for `action`, and moves no
364364
// lookup fills.
365365
// [#29171] `field` 81 → 82: `referenceNameVisible`, a lookup's declared
366366
// name exposure.
367+
// [#30880] `field` 82 → 83: `renamedFrom`, the earlier key(s) a package
368+
// upgrade carries a field's column from (cloud#3652 ruling D3).
367369
// [#28022] `app` 22 → 23: `landing`, the per-audience landing list.
368370
// [#28404] `app` 23 → 24, `dashboard` 20 → 21, `dataset` 16 → 17, `flow`
369371
// 22 → 23, `page` 26 → 27, `report` 23 → 24: `overrides`, the ADR-0146 D2
@@ -372,7 +374,7 @@ describe('#17501 — /meta/types serves a real schema for `action`, and moves no
372374
// gain it, and their counts stay.
373375
// [#28979] `page` 27 → 28: `apps`, the app scope of a record page.
374376
const CARD_PROPERTY_COUNTS: Record<string, number> = {
375-
agent: 24, app: 24, dashboard: 21, dataset: 17, field: 82, flow: 23,
377+
agent: 24, app: 24, dashboard: 21, dataset: 17, field: 83, flow: 23,
376378
hook: 23, object: 49, page: 28, position: 13, report: 24, skill: 16, tool: 14,
377379
};
378380

‎packages/spec/authorable-surface/data.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@
344344
"data/Field:relatedListColumns",
345345
"data/Field:relatedListFilter",
346346
"data/Field:relatedListTitle",
347+
"data/Field:renamedFrom",
347348
"data/Field:required",
348349
"data/Field:requiredPermissions",
349350
"data/Field:requiredWhen",

0 commit comments

Comments
 (0)