Uh oh!
There was an error while loading. Please reload this page.
feat(cli): validate prints the union arm the document selected, not a bare "Invalid input" - #7384
Merged
Conversation
…alid input" `objectui validate` checks a document against `AnyComponentSchema`, a `z.union`. When nothing matches, Zod reports ONE top-level issue — `invalid_union` · `Invalid input` · path `(root)` — and hangs every arm's real diagnosis off that issue's `errors` array, which nothing read. The remediation text objectui#6931 wrote into the retired menu-divider arm therefore never reached an author. Per the 2026-09-02 maintainer ruling (option B), the printer now narrows a failing union to the single arm the document's `type` selects and prints that arm's issues — paths rebased to absolute — and nothing from the other arms. When no arm accepts the `type`, it says so and offers the nearest few of the accepted values, capped by a named constant. A union with no `type` discriminator to select on (`MenuItemSchema`, whose arms both declare `type` as an ADR-0049 tombstone) falls back to reporting every arm under the same cap, which is the path that finally delivers the objectui#6523 guidance. Selection lives in a new `utils/union-arm-diagnostics.ts` and reads only the issue tree, never the schema; `commands/validate.ts` keeps every `console.*` call and stays the repository's only zod-issue printer. `check.ts` is a boolean recogniser and is untouched. The boundary case in `validate-root-path-line.test.ts` was written to pin the pre-ruling semantics so this decision would land as an explicit edit against a red test; it is inverted here, deliberately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-project-manager
marked this pull request as ready for review
September 2, 2026 16:36
Uh oh!
There was an error while loading. Please reload this page.
os-project-manager
deleted the
claude/issue-7004-union-arm-diagnostics
branch
September 2, 2026 16:51
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#7004
Implements the arm-selection half of objectui#7004, under the maintainer ruling of 2026-09-02 (comment 5507942045, option B). The mechanical root-path half landed earlier as PR #7038. Session that produced this branch: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
The defect
objectui validatechecks a document againstAnyComponentSchema, az.union. When a document matches no arm, Zod 4 reports one top-level issue —invalid_union·Invalid input· path(root)— and hangs every arm's real diagnosis off that issue'serrorsarray, which nothing read. So the remediation text objectui#6931 wrote into the retired menu-divider arm sat exactly one level below the only thing the CLI printed.Two measurements first, because the ruling's mechanism depends on them
Both were asked for explicitly in the decision facets. Both came back favourable; the numbers are in the report comment on the card.
1 — is the arm derivable at all?
AnyComponentSchemais a plainz.unionof 14 members which resolve to 108 leaf arms, and all 108 declare atypeliteral with zero literals claimed by two arms — so "exactly one arm accepts that literal" is total and unambiguous at the document root. (The decision analysis estimated ~97; 108 is the measured count on this base.)More usefully, an arm names the literals it accepts inside its own issues, and only two shapes do it:
invalid_valueat['type']carryingvalues, and a discriminated union'sinvalid_unionat['type']carryingoptions. That makes the accepted-literal set derivable from the error tree alone — so the new module never imports or introspects the schema and cannot drift from it. Zod'serrorsarray is positionally aligned with the union's options (14 entries for 14 members), but selection deliberately does not depend on that.2 — does nested recursion terminate? Yes, by construction: Zod materialises
errorseagerly as plain nested arrays at parse time, so each step strictly descends a finite acyclic tree with no lazy getters — true even forz.lazyschemas likeMenuItemSchema, where the laziness is in the schema, not in the issue tree it produces. Measured maximum depth on this tree: 3.errorsare relative to their union's node. The nested union at['items', 0]reports its arm issues at['type'], not['items', 0, 'type']. Printing those raw would have named the document's owntypekey. Every path is rebased onto its parent prefix.What is printed now
typeselects exactly one arm1.1,1.2… with absolute paths and codes, and nothing from the other armstypeNo arm accepts type "dropdwn-menu".plus the nearest few of the accepted valuestypeat alltypediscriminator[arm k/n], cappedThe cap constant, and its distance function
MAX_UNION_ARMS_REPORTED = 5, pinned by a test as the ruling requires. Ranking the 108 arm names by unit-cost edit distance against four authored typos gave the same shape every time: the intended arm is rank 1 and alone in its distance band (dropdwn-menu→dropdown-menuat 1;dropdwon-menu→dropdown-menuat 2;Page→pageat 1;obect-grid→object-gridat 1), with the next band opening 1-6 edits further out holding 1-5 names. Five shows the winner plus the following band entire and stops well short of 108; a cap of 1 would print the winner alone and read as a confident answer rather than as a ranked list.The ruling did not name a distance function. This uses unit-cost Levenshtein, ascending, ties broken lexicographically — the authored
typeand the arm names are short kebab-case identifiers whose realistic errors are a dropped, added or mistyped character. Damerau was considered and not taken: a transposition scores 2 here and still lands first.objectui check's suggestion case-only for a different surface over a different candidate set (KNOWN_SCHEMA_TYPES, the registry's keys). This surface's candidates are the schema union's arms, harvested from the error tree, and the 2026-09-02 ruling asks for the "nearest" ones. What is borrowed from #5247 is its refusal to guess: with notypeauthored there is nothing to be near, so no candidates are offered at all rather than an alphabetical slice presented as guidance.The third branch, declared
The ruling partitions on "exactly one arm accepts" vs "no arm accepts". Both presuppose arms that declare a
typecontract, andMenuItemSchema— the union this card was filed about — has none: it is a two-armz.unionwhose arms both declaretypeas an ADR-0049 retirement tombstone (z.never()), so neither names a literal. Routing it to "no arm accepts" would be literally true and exactly wrong — the candidate list would be empty and the objectui#6523 text, the very text this card exists to deliver, would be dropped.So a union with no declaring arm takes the ruling's own named fallback, "A with a cap": every arm, capped by the same constant. This is not option A at the root, which the ruling rejected on the 108-arm noise argument — the root is always discriminated, and undiscriminated unions in this mirror are small. Flagged here for review because it completes a case the ruling did not name.
Tests
validate-union-arm-selection.test.ts(new, 10 cases): the menu-nested wrong item, the no-matching-arm case, the untyped case, arm exclusivity, the non-union control, the cap pin, and the selection module's own unit tests.validate-root-path-line.test.ts: the boundary case is inverted. It was written deliberately to pin the pre-ruling semantics so this decision would land as an explicit edit against a red test rather than a silent widening; that is what happened, and the block now restates the new semantics with the root-path assertions it has always carried.Reverse verification (ablation) at commit
ce9e70b5d,validate.tsreverted to the base blob with the selection module and tests left in place: 6 failed / 10 passed of 16, the six being the inverted boundary case and the fivevalidate-driven arm cases; the ten being the five root-path controls, the non-union control, and the four pure-module unit tests, which pass on base and are what makes them controls. Mutation confirmed on disk by blob-hash equality to the base blob and the anchor dropping 3 occurrences to 0; restore confirmed by on-disk blob equal to theHEADblob and an emptygit diff HEAD. No build is involved — the run resolves@object-ui/types/zodthrough the root Vitest alias topackages/types/src, and the ablation's red is itself the proof thatsrcis what executes.Scope
objectui checkis untouched and deliberately so: it has no zod-issue printer, usingsafeValidateSchema(...).successas a boolean recogniser, and printing issues behind a negative recognition would flood its report with diagnoses of non-ObjectUI files — the failure objectui#5127 and objectui#6075 exist to prevent.validate.tskeeps everyconsole.*call and stays the repository's only zod-issue printer; the new module performs selection only, so no second rendering surface is created and no shared renderer is warranted.Nothing about which documents are ACCEPTED changes. Diagnostic output only.
Gates at
ce9e70b5d:packages/cliVitest 16 files / 246 tests passed,type-checkexit 0 (tsc --listFilesconfirms all four changed files are in the program),lintexit 0 with 11 pre-existing warnings and none in the changed files,lint:coverage46/46 packages, and the changeset, control-byte, doc-fence, doc-type, vi-mock, phantom-dep, self-import and esm-specifier gates all exit 0.check:doc-snippetsandcheck:readme-exportsreportPRECONDITION NOT MET/ population-collapsed on this unbuilt worktree — a build precondition unrelated to this diff, left to CI, which builds.🤖 Generated with Claude Code
https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
Generated by Claude Code