Filed unassigned by the dev seat working #6931 (branch claude/issue-6931-tombstone-guidance), from a measurement taken there. Out of that card's scope — recorded rather than fixed on its PR.
What was measured
packages/cli/src/commands/validate.ts renders parse failures with:
result.error.issues.forEach((issue,index)=>{console.error(chalk.red(`\n${index+1}. ${issue.message}`));if(issue.path&&issue.path.length>0){/* Path: … */}if(issue.code){/* Code: … */}});check.ts prints the same three fields the same way. That reads only the top level of error.issues.
For a z.union(...) schema zod 4 reports ONE top-level issue: code: "invalid_union", path: [], message: "Invalid input". Everything that says which arm failed and why hangs off that issue's errors array, which nothing here walks. Measured against MenuItemSchema (packages/types/src/zod/overlay.zod.ts), authoring { label: 'New Tab', type: 'separator' }:
| channel | what it carries |
|---|
| top-level issue (the ONLY thing printed) | invalid_union · path [] · Invalid input |
| arm 0 issue (not printed) | invalid_type · path ['type'] · the full #6523 remediation text |
| arm 1 issue (not printed) | invalid_type · path ['type'] · RETIRED (objectui#6523) — see the command-item arm above. |
So the author gets 1. Invalid input with no path and no remedy, for a document whose defect the schema diagnosed precisely.
Why it is worth a card
The path/code suppression is the sharper half: with path: [] the printer's if (issue.path.length > 0) guard also drops the Path: line, so the author is not even told which node failed. On a large page schema that is a bare "Invalid input" against the whole document.
It also silently caps work done elsewhere. #6931 converted eleven ADR-0049 tombstones to retirementTombstone() so each refusal carries its remediation text; for the two that live on a union (MenuItemSchema's arms) the improved text lands in the arm issues and therefore never reaches this printer. Any future guidance written into a union member has the same fate, and nothing currently signals that.
Scope is wider than menus: the same suppression applies to every union in the mirror (for example TableSchema.footer, and any discriminated union added later).
Suggested direction, not a decision
Walk the union's errors when a top-level issue is invalid_union and print the arm issues (or, for a discriminated union, the arm that matched the discriminator) instead of the placeholder. Worth deciding deliberately: printing every arm for a wide union can be noisier than one line, and picking a single arm is a heuristic. A maintainer call on which shape to print belongs on this card, not in a drive-by patch — hence unassigned and unlabelled for triage.
Evidence and probe live on claude/issue-6931-tombstone-guidance; related: #6931, #6523, #6105.
Filed unassigned by the dev seat working #6931 (branch
claude/issue-6931-tombstone-guidance), from a measurement taken there. Out of that card's scope — recorded rather than fixed on its PR.What was measured
packages/cli/src/commands/validate.tsrenders parse failures with:check.tsprints the same three fields the same way. That reads only the top level oferror.issues.For a
z.union(...)schema zod 4 reports ONE top-level issue:code: "invalid_union",path: [],message: "Invalid input". Everything that says which arm failed and why hangs off that issue'serrorsarray, which nothing here walks. Measured againstMenuItemSchema(packages/types/src/zod/overlay.zod.ts), authoring{ label: 'New Tab', type: 'separator' }:invalid_union· path[]·Invalid inputinvalid_type· path['type']· the full #6523 remediation textinvalid_type· path['type']·RETIRED (objectui#6523) — see the command-item arm above.So the author gets
1. Invalid inputwith no path and no remedy, for a document whose defect the schema diagnosed precisely.Why it is worth a card
The path/code suppression is the sharper half: with
path: []the printer'sif (issue.path.length > 0)guard also drops the Path: line, so the author is not even told which node failed. On a large page schema that is a bare "Invalid input" against the whole document.It also silently caps work done elsewhere. #6931 converted eleven ADR-0049 tombstones to
retirementTombstone()so each refusal carries its remediation text; for the two that live on a union (MenuItemSchema's arms) the improved text lands in the arm issues and therefore never reaches this printer. Any future guidance written into a union member has the same fate, and nothing currently signals that.Scope is wider than menus: the same suppression applies to every union in the mirror (for example
TableSchema.footer, and any discriminated union added later).Suggested direction, not a decision
Walk the union's
errorswhen a top-level issue isinvalid_unionand print the arm issues (or, for a discriminated union, the arm that matched the discriminator) instead of the placeholder. Worth deciding deliberately: printing every arm for a wide union can be noisier than one line, and picking a single arm is a heuristic. A maintainer call on which shape to print belongs on this card, not in a drive-by patch — hence unassigned and unlabelled for triage.Evidence and probe live on
claude/issue-6931-tombstone-guidance; related: #6931, #6523, #6105.