Found while implementing #12868 (the form-view-option-default-removed conversion follows the family precedent and inherits this gap; it is a family-wide property, not specific to that entry).
Mechanism
ViewMetadataSchema (packages/spec/src/ui/view.zod.ts) accepts THREE persisted view metadata body spellings: the container ({ object, list, listViews, form, formViews, ... }), the ViewItem RECORD ({ viewKind: 'list' | 'form', config: <ListView | FormView>, ... }), and the flattened overlay (view keys at top level). All three land in sys_metadata rows.
applyConversionsToStoredItem('view', item) (packages/spec/src/conversions/stored.ts) replays the full chain over { views: [item] } — its documented policy is that data at rest is "the perpetual consumer arriving late" and the full chain covers it.
But every view-family conversion's walker reads ONLY the container spelling: view-visibleOn-to-visibleWhen walks views[].form / views[].formViews.*, view-export-options-pdf-removed walks views[].list / views[].listViews.*, and the same holds for view-inert-keys-removed, view-list-passthrough-keys-removed, and the new form-view-option-default-removed. A stored ViewItem record keeps its payload under config (discriminated by viewKind), and a flattened overlay keeps it at top level — neither key is consulted by any walker, so for those two spellings the whole view conversion chain is a no-op.
Consequence
A sys_metadata view row persisted as a ViewItem record or flattened overlay under an old protocol never gets its historical shape canonicalized: the visibleOn rename, the inert-key strips, the pdf-format strip, and the per-option default strip all silently skip it. Any rehydration seam that then parses the body faces a schema that refuses (or tombstones) exactly what the conversion layer claims to have handled — the "row that once worked breaks with no author in the loop" case stored.ts's module doc says the replay exists to prevent.
Closed #10001 is the same structural gap one layer over, already fixed for the LINT walk (a standalone ViewItem record's nested config.* judged by neither list-view field rule): the walkers that predate the ViewItem spelling see only the container. The conversion layer has not had that pass.
Remedy sketch
A shared view-body walker in packages/spec/src/conversions/walk.ts that recognizes the three persisted spellings and hands each conversion the list/form payloads wherever they live (the way region-slots.ts centralized the flow-node region walk), adopted by every view-family conversion — plus fixtures pinning the ViewItem-record and flattened-overlay spellings per conversion.
Sizing note: whether any real deployment stores pre-protocol-15 ViewItem records is unmeasured — the walker gap is measured (code-shape), the blast radius is not; triage may want that measurement first.
Filed unassigned from the #12868 dev run (branch claude/issue-12868-formview-option-default-narrow).
Found while implementing #12868 (the
form-view-option-default-removedconversion follows the family precedent and inherits this gap; it is a family-wide property, not specific to that entry).Mechanism
ViewMetadataSchema(packages/spec/src/ui/view.zod.ts) accepts THREE persistedviewmetadata body spellings: the container ({ object, list, listViews, form, formViews, ... }), the ViewItem RECORD ({ viewKind: 'list' | 'form', config: <ListView | FormView>, ... }), and the flattened overlay (view keys at top level). All three land insys_metadatarows.applyConversionsToStoredItem('view', item)(packages/spec/src/conversions/stored.ts) replays the full chain over{ views: [item] }— its documented policy is that data at rest is "the perpetual consumer arriving late" and the full chain covers it.But every view-family conversion's walker reads ONLY the container spelling:
view-visibleOn-to-visibleWhenwalksviews[].form/views[].formViews.*,view-export-options-pdf-removedwalksviews[].list/views[].listViews.*, and the same holds forview-inert-keys-removed,view-list-passthrough-keys-removed, and the newform-view-option-default-removed. A stored ViewItem record keeps its payload underconfig(discriminated byviewKind), and a flattened overlay keeps it at top level — neither key is consulted by any walker, so for those two spellings the whole view conversion chain is a no-op.Consequence
A
sys_metadataview row persisted as a ViewItem record or flattened overlay under an old protocol never gets its historical shape canonicalized: thevisibleOnrename, the inert-key strips, the pdf-format strip, and the per-optiondefaultstrip all silently skip it. Any rehydration seam that then parses the body faces a schema that refuses (or tombstones) exactly what the conversion layer claims to have handled — the "row that once worked breaks with no author in the loop" casestored.ts's module doc says the replay exists to prevent.Closed #10001 is the same structural gap one layer over, already fixed for the LINT walk (a standalone ViewItem record's nested
config.*judged by neither list-view field rule): the walkers that predate the ViewItem spelling see only the container. The conversion layer has not had that pass.Remedy sketch
A shared view-body walker in
packages/spec/src/conversions/walk.tsthat recognizes the three persisted spellings and hands each conversion the list/form payloads wherever they live (the wayregion-slots.tscentralized the flow-node region walk), adopted by every view-family conversion — plus fixtures pinning the ViewItem-record and flattened-overlay spellings per conversion.Sizing note: whether any real deployment stores pre-protocol-15 ViewItem records is unmeasured — the walker gap is measured (code-shape), the blast radius is not; triage may want that measurement first.
Filed unassigned from the #12868 dev run (branch
claude/issue-12868-formview-option-default-narrow).