|
| 1 | +--- |
| 2 | +"@objectstack/spec": minor |
| 3 | +"@objectstack/rest": patch |
| 4 | +"@objectstack/platform-objects": patch |
| 5 | +--- |
| 6 | + |
| 7 | +fix(spec,rest): the metadata forms save what they show — form ↔ Zod reconciliation (#3786) |
| 8 | + |
| 9 | +Every entry in `METADATA_FORM_REGISTRY` is a hand-written `defineForm` layout |
| 10 | +that names keys of a Zod schema it never imports: two descriptions of one key |
| 11 | +set, a comment asking the next author to keep them in step, and nothing that |
| 12 | +fails when they don't. #3786 asked for a sweep of that shape across the repo. |
| 13 | +**Four of the seventeen forms had already drifted, every one of them silently.** |
| 14 | + |
| 15 | +The silence is the point. `ObjectSchema` / `FieldSchema` are deliberately not |
| 16 | +`.strict()`, so a key the schema does not declare parses clean and is stripped |
| 17 | +on the way to storage — the same ADR-0104 failure class the `field.zod.ts` |
| 18 | +prune tombstone already describes in prose. An admin toggled a switch in |
| 19 | +Studio, got no error, and the value never landed. |
| 20 | + |
| 21 | +**What was broken, from an author's seat:** |
| 22 | + |
| 23 | +- **Object → Capabilities.** The block bound to `capabilities`; the |
| 24 | + `ObjectSchema` key is `enable`. All seven toggles (Track history, Searchable, |
| 25 | + API enabled, Files, Feeds, Activities, Clone) saved nothing. |
| 26 | +- **Object → Fields.** The inline column grid offered 16 keys `FieldSchema` has |
| 27 | + never declared. `PII`, `Encrypted`, `Indexed`, `Immutable`, `Filterable`, |
| 28 | + `Placeholder`, `Validation`/`Error message` and `Starting number` were |
| 29 | + controls with no storage behind them at all; the rest named keys the schema |
| 30 | + had **renamed** and the form never followed: |
| 31 | + `referenceFilter` → `lookupFilters`, `cascadeDelete` → `deleteBehavior` |
| 32 | + (a three-way enum, not a boolean), `formula` → `expression`, |
| 33 | + `displayFormat` → `autonumberFormat`, and the flat `summaryType` / |
| 34 | + `summaryField` pair → the single `summaryOperations` object, which also |
| 35 | + restores the `object` key the flat pair had no slot for. Roll-ups authored in |
| 36 | + that grid saved nothing. |
| 37 | +- **Report → Advanced.** `aria` and `performance` were pruned from |
| 38 | + `ReportSchema` by #3496; the form kept rendering both. |
| 39 | +- **Hook / Action → Body.** `memoryMb` was unauthorable — named in |
| 40 | + `hook.form.ts`'s own doc comment, absent from the list beneath it. |
| 41 | +- **Page → Interface.** `interfaceConfig.sort` was unauthorable, so a page's |
| 42 | + default sort order could not be set in Studio at all. |
| 43 | + |
| 44 | +**No authored metadata changes and nothing you can write is removed.** These |
| 45 | +were UI controls that never persisted; every corrected key is one `FieldSchema` |
| 46 | +/ `ObjectSchema` already accepted. Metadata authored in YAML/TS was always |
| 47 | +validated against the real schema and is unaffected. If you had been filling |
| 48 | +those Studio controls expecting them to stick, they now either work (the |
| 49 | +renamed five) or are gone rather than lying to you. |
| 50 | + |
| 51 | +The metadata-form translation bundles are derived from the registry, so all |
| 52 | +four locales are regenerated. Worth naming what they contained: translated |
| 53 | +labels, in four languages, for switches that saved nothing — the drift had |
| 54 | +propagated into a generated artifact and been dutifully translated there. |
| 55 | + |
| 56 | +**The mechanism.** `metadata-form-zod-reconciliation.test.ts` walks every |
| 57 | +registered form and reconciles it against `getMetadataTypeSchema()`. The two |
| 58 | +directions are deliberately asymmetric: **form-only** (a control whose value is |
| 59 | +discarded) is always a defect and cannot be excused, because no design wants |
| 60 | +one; **zod-only** is ledgerable with a reason, for a deprecated key held back |
| 61 | +from new authoring or a curated quick-add subset that defers to a fuller |
| 62 | +editor. Ledger entries are checked for non-vacuity and for still resolving on |
| 63 | +both sides, per the #4045 / #4040 discipline. Verified by mutation — re-adding |
| 64 | +a stripped key, dropping a covered key, and offering a ledgered omission each |
| 65 | +turn the gate red. |
| 66 | + |
| 67 | +**New export: `TRANSLATABLE_METADATA_TYPES`** (`@objectstack/spec/system`), the |
| 68 | +set of metadata types whose labels `translateMetadataDocument` localizes, |
| 69 | +derived from its dispatch table rather than restated. `@objectstack/rest` had |
| 70 | +been carrying a hand-copied literal set under a "keep in sync with the type |
| 71 | +dispatch" comment; it now reads this instead. Registering a translator in spec |
| 72 | +reaches the REST boundary with nothing else to remember — the second list is |
| 73 | +deleted rather than checked, which is the better half of derive-or-gate. |
| 74 | + |
| 75 | +Also corrected: `ActionAiCategorySchema`'s comment claimed it mirrored |
| 76 | +`ToolCategorySchema` in `ai/tool.zod` and told the next author to update both |
| 77 | +sides — but #3896 deleted `ToolCategorySchema` along with the inert |
| 78 | +`tool.category` key it typed. The instruction had been pointing at a source |
| 79 | +that no longer exists. The enum is canonical now and says so. |
0 commit comments