Filed by the domain:spec dev seat while landing #17063 (PR #17298). Unassigned, unlabelled — triage owns both.
The measurement
ViewMetadataSchema is the union behind PUT /api/v1/meta/view — the door #13100 measured as the one an MCP/AI author actually reaches, with no CLI anywhere on the path. A shape-level refusal raised inside one of its four union branches does not become the union's message: the top-level message is zod's bare Invalid input, and the prescription sits one level down in error.issues[0].errors[k][j].message.
Probe (run on origin/main + PR #17298, both identical for the three pre-existing rows), against ViewMetadataSchema.safeParse({ name: 'crm_lead.dash', object: 'crm_lead', viewKind: 'list', ...body }), printing only the TOP-LEVEL messages:
| body |
top-level message |
{ type: 'grid', columns: ['name'], virtualScroll: true } |
Invalid input |
{ type: 'grid', columns: ['name'], striped: true } |
Invalid input |
{ type: 'grid', columns: ['name'], pageName: 'p' } (new in #17063) |
Invalid input |
{ type: 'page', columns: [] } (new in #17063) |
Invalid input |
In every case the real prescription — `view.virtualScroll` was removed in @objectstack/spec 17.0.0 … and its siblings — is present, but nested. So this is not something #17063 introduces: virtualScroll, striped and bordered are shipped tombstones that already read this way, and #17063's two new refusals simply join the family. Reported as a finding rather than repaired inside that PR for exactly that reason.
Why this is a defect and not an observation
The house already ruled on this shape one key over, in the same file. exportOptionsPdfUnionError (packages/spec/src/ui/view.zod.ts, #8010) exists with this rationale, verbatim from its docblock:
Loud top-level refusal for a retired 'pdf' anywhere in exportOptions (#8010). Without this, the prescription raised inside a union BRANCH is buried in invalid_union sub-errors; with it, the union's own message IS the prescription whenever the authored value — either spelling — declares 'pdf'.
retired-key.ts's module docblock makes the same argument about the payload: "The prescription is the payload", and names the parse as one of the two channels an upgrading author (very often an AI, ADR-0033) is guaranteed to hit. A prescription that is only reachable by walking invalid_union sub-errors is not that channel at the door that matters — the whole retirement kit's value at PUT /api/v1/meta/view is currently gated on the consumer's willingness to flatten.
Note that #8010 solved it per case, with one dispatch function for one retired value. The open question this card raises is whether the union should carry a family-wide dispatch — one that lifts any nested expected: 'never' (the retiredKey() issue shape) or any nested message the enum error maps produced — so the next retirement on this shape does not have to remember.
Prior art, and why this is not a duplicate of it
Searched is:issue over this repo before filing; no open card asks for this.
Repro
import { ViewMetadataSchema } from '@objectstack/spec/ui';
const r = ViewMetadataSchema.safeParse({
name: 'crm_lead.dash', object: 'crm_lead', viewKind: 'list',
type: 'grid', columns: ['name'], virtualScroll: true,
});
console.log(r.success ? 'PARSED' : r.error.issues.map((i) => i.message).join(' | '));
// -> "Invalid input" (the `view.virtualScroll` prescription is nested one level down)
Published surface
⚠️ This affects shipped behaviour: virtualScroll, striped and bordered have been tombstoned on this shape since 17.0.0 and every one of them reads this way today at the runtime write door. #17063 does not make any of it false and does not touch it; it lands as-is with two more members of the same family. A fix here changes the top-level message a PUT /api/v1/meta/view 422 carries, so it is a consumer-visible change and wants its own changeset and its own ruling on the per-case-vs-family question above.
Back-link: found while landing #17063 / PR #17298 (retire the type: 'page' list-view mount).
Filed by the
domain:specdev seat while landing #17063 (PR #17298). Unassigned, unlabelled — triage owns both.The measurement
ViewMetadataSchemais the union behindPUT /api/v1/meta/view— the door #13100 measured as the one an MCP/AI author actually reaches, with no CLI anywhere on the path. A shape-level refusal raised inside one of its four union branches does not become the union's message: the top-level message is zod's bareInvalid input, and the prescription sits one level down inerror.issues[0].errors[k][j].message.Probe (run on
origin/main+ PR #17298, both identical for the three pre-existing rows), againstViewMetadataSchema.safeParse({ name: 'crm_lead.dash', object: 'crm_lead', viewKind: 'list', ...body }), printing only the TOP-LEVEL messages:{ type: 'grid', columns: ['name'], virtualScroll: true }Invalid input{ type: 'grid', columns: ['name'], striped: true }Invalid input{ type: 'grid', columns: ['name'], pageName: 'p' }(new in #17063)Invalid input{ type: 'page', columns: [] }(new in #17063)Invalid inputIn every case the real prescription —
`view.virtualScroll` was removed in @objectstack/spec 17.0.0 …and its siblings — is present, but nested. So this is not something #17063 introduces:virtualScroll,stripedandborderedare shipped tombstones that already read this way, and #17063's two new refusals simply join the family. Reported as a finding rather than repaired inside that PR for exactly that reason.Why this is a defect and not an observation
The house already ruled on this shape one key over, in the same file.
exportOptionsPdfUnionError(packages/spec/src/ui/view.zod.ts, #8010) exists with this rationale, verbatim from its docblock:retired-key.ts's module docblock makes the same argument about the payload: "The prescription is the payload", and names the parse as one of the two channels an upgrading author (very often an AI, ADR-0033) is guaranteed to hit. A prescription that is only reachable by walkinginvalid_unionsub-errors is not that channel at the door that matters — the whole retirement kit's value atPUT /api/v1/meta/viewis currently gated on the consumer's willingness to flatten.Note that #8010 solved it per case, with one dispatch function for one retired value. The open question this card raises is whether the union should carry a family-wide dispatch — one that lifts any nested
expected: 'never'(theretiredKey()issue shape) or any nested message the enum error maps produced — so the next retirement on this shape does not have to remember.Prior art, and why this is not a duplicate of it
ViewMetadataSchema's union has no discriminant and its container members are not exported, so a consumer doing failure diagnosis has to index nestederrorsby member position. Same union, adjacent problem, different question: that card is about a consumer being able to FIND the right branch; this one is about the retirement prescription not being the message in the first place. Neither closes the other.retiredKey()tombstones over protocol-17 conversions never mentionos migrate meta— the author is told to hand-edit when a tool exists #6914 / [spec] 15 moreretiredKey()prescriptions over v17 conversions lack theos migrate metasentence — hidden from #6914's census because their FILES already contain the sentence elsewhere #7062 (closed) — theos migrate metasentence census. About prescription CONTENT, not its reachability.retiredKey()墓碑渲染成any—— 嵌套两层时连[REMOVED]处方都没有,退役键读起来像自由槽 #5606 (closed) — generated reference docs rendering tombstones asany. A third channel, already fixed.Searched
is:issueover this repo before filing; no open card asks for this.Repro
Published surface
virtualScroll,stripedandborderedhave been tombstoned on this shape since 17.0.0 and every one of them reads this way today at the runtime write door. #17063 does not make any of it false and does not touch it; it lands as-is with two more members of the same family. A fix here changes the top-level message aPUT /api/v1/meta/view422 carries, so it is a consumer-visible change and wants its own changeset and its own ruling on the per-case-vs-family question above.Back-link: found while landing #17063 / PR #17298 (retire the
type: 'page'list-view mount).