Found while implementing objectui#6488 (per-FIELD key carry-over in MetadataService.saveFields). Filed, not fixed — a different file and a different writer from that card's face, and the resolution is an adjudication rather than a mechanical edit.
What it is
packages/app-shell/src/views/metadata-admin/previews/object-fields-io.ts is the single read door for draft.fields across the whole object designer. It strips retired keys on read:
exportconstRETIRED_FIELD_KEYS=['indexed']asconst;
writeFields then writes each def back verbatim (out[e.name] = e.def, and { name: e.name, ...e.def } for the array shape), so any key that survives the read reaches PUT /api/v1/meta/object/:name.
That list is one key. FieldSchema refuses five designer-written keys by name. Measured against the installed @objectstack/spec 17.2.0, each on an otherwise-green field { name: 'amount', type: 'number', label: 'Amount' }:
indexed => unrecognized_keys "never a FieldSchema key; a field-level index flag built no index" (objectui#4644)
referenceTo => unrecognized_keys "Did you mean `referenceTo` -> `reference`?" (objectui#6041)
formula => unrecognized_keys "Did you mean `formula` -> `expression`?" (objectui#6043)
isSystem => unrecognized_keys "Did you mean `isSystem` -> `system`?" (objectui#6044)
sortOrder => unrecognized_keys (objectui#6045)
So a draft carrying any of the last four round-trips through readFields / writeFields untouched and produces the hard 422 INVALID_METADATA that blocks EVERY later save of that object — which is exactly the shape objectui#4644 closed in this same file for indexed, left open for its four siblings.
Why the two neighbouring writers do not cover it
Each retired-key list is scoped to its own writer's history, and the three have drifted apart:
| site | strips |
|---|
previews/object-fields-io.ts (this one) | indexed |
plugin-designer/src/MetadataFieldsPage.tsxcarryOver | indexed, referenceTo, isSystem, formula |
app-shell/src/services/MetadataService.tscarryOver (objectui#6488) | indexed, referenceTo, formula, isSystem, sortOrder |
The parity gate does not see this: scripts/check-designer-field-key-parity.mjs compares declared property signatures against the schema's accept set, and these keys reach the payload through a verbatim spread of an untyped Record<string, unknown> — its own coverage note 3 names object-fields-io's readFields / writeFields as outside its reach.
Nothing pins the gap either. Of the three suites beside the module (object-fields-io.test.ts, object-fields-io.spec-keys.test.ts, object-fields-io.retiredKeys.test.ts), none mentions formula, referenceTo, isSystem or sortOrder.
What the card has to settle
Two questions, neither of them mechanical:
- Whether the four can actually be present in a draft this module reads. The premise the two sibling writers already work from is yes — each key was a real designer control before its card retired it, and
MetadataFieldsPage's tombstone states plainly that "objects saved from this page can still carry the key". Confirming that for the drafts this door reads is the first step; if the answer is no for some of them, the honest fix is smaller. - Whether the three lists should become one. Three independently maintained sets is the pre-gate shape
check-designer-field-key-parity's own header criticises ("two independently maintained RETIRED_FIELD_KEYS = ['indexed'] sets … Every one of them was written AFTER an instance was found in production"). A single exported list is the obvious answer, but it spans plugin-designer/src/MetadataFieldsPage.tsx, which objectui#6489 currently owns, so sequencing matters. Note the lists are not simply nested — whether sortOrder belongs in all three is part of the same question.
Refs: objectui#6488 (the carry-over that measured this) · objectui#6489 (owns MetadataFieldsPage.tsx on the neighbouring seam) · objectui#4644 (the indexed half, in this same file) · objectui#5761 (the parity gate family; this is its coverage note 3)
Found while implementing objectui#6488 (per-FIELD key carry-over in
MetadataService.saveFields). Filed, not fixed — a different file and a different writer from that card's face, and the resolution is an adjudication rather than a mechanical edit.What it is
packages/app-shell/src/views/metadata-admin/previews/object-fields-io.tsis the single read door fordraft.fieldsacross the whole object designer. It strips retired keys on read:writeFieldsthen writes each def back verbatim (out[e.name] = e.def, and{ name: e.name, ...e.def }for the array shape), so any key that survives the read reachesPUT /api/v1/meta/object/:name.That list is one key.
FieldSchemarefuses five designer-written keys by name. Measured against the installed@objectstack/spec17.2.0, each on an otherwise-green field{ name: 'amount', type: 'number', label: 'Amount' }:So a draft carrying any of the last four round-trips through
readFields/writeFieldsuntouched and produces the hard422 INVALID_METADATAthat blocks EVERY later save of that object — which is exactly the shape objectui#4644 closed in this same file forindexed, left open for its four siblings.Why the two neighbouring writers do not cover it
Each retired-key list is scoped to its own writer's history, and the three have drifted apart:
previews/object-fields-io.ts(this one)indexedplugin-designer/src/MetadataFieldsPage.tsxcarryOverindexed,referenceTo,isSystem,formulaapp-shell/src/services/MetadataService.tscarryOver(objectui#6488)indexed,referenceTo,formula,isSystem,sortOrderThe parity gate does not see this:
scripts/check-designer-field-key-parity.mjscompares declared property signatures against the schema's accept set, and these keys reach the payload through a verbatim spread of an untypedRecord<string, unknown>— its own coverage note 3 namesobject-fields-io'sreadFields/writeFieldsas outside its reach.Nothing pins the gap either. Of the three suites beside the module (
object-fields-io.test.ts,object-fields-io.spec-keys.test.ts,object-fields-io.retiredKeys.test.ts), none mentionsformula,referenceTo,isSystemorsortOrder.What the card has to settle
Two questions, neither of them mechanical:
MetadataFieldsPage's tombstone states plainly that "objects saved from this page can still carry the key". Confirming that for the drafts this door reads is the first step; if the answer is no for some of them, the honest fix is smaller.check-designer-field-key-parity's own header criticises ("two independently maintainedRETIRED_FIELD_KEYS = ['indexed']sets … Every one of them was written AFTER an instance was found in production"). A single exported list is the obvious answer, but it spansplugin-designer/src/MetadataFieldsPage.tsx, which objectui#6489 currently owns, so sequencing matters. Note the lists are not simply nested — whethersortOrderbelongs in all three is part of the same question.Refs: objectui#6488 (the carry-over that measured this) · objectui#6489 (owns
MetadataFieldsPage.tsxon the neighbouring seam) · objectui#4644 (theindexedhalf, in this same file) · objectui#5761 (the parity gate family; this is its coverage note 3)