Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/7702-union-arm-count-prose.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
---

Comment-only: `union-arm-diagnostics.ts` and its test state the size of
`AnyComponentSchema` as prose integers, and both had drifted (objectui#7702).
Nothing ships — no executable line changes, so no package is released by this
change.

The module's header said "108 leaf arms and 108 DISTINCT `type` literals" and
its display-cap argument said the cap "stops well short of the 108"; the
alignment fact said "14 entries for `AnyComponentSchema`'s 14 members". The
filer measured 105/103, and this branch measures 107 and 13 — three different
arm counts inside one week, in both directions.

The repair states the property instead of the count, per the ruling on the card:
what the cap argument actually rests on is that **no literal is claimed by two
arms** (which makes "exactly one arm accepts that literal" total), and the
alignment sentence needs no count at all. Those cannot go stale. Re-measuring
and re-stating the integers would only defer the next drift, so the numbers are
gone rather than refreshed, and no test pins them — a pinned count would red the
build every time a component lands.
11 changes: 6 additions & 5 deletions packages/cli/src/__tests__/validate-union-arm-selection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
*
* ⚠️ Two halves of that ruling are load-bearing in opposite directions, and
* both are asserted here. "Print the selected arm" is worthless if the other
* 107 arms come with it (that is option A, rejected for noise), so the
* exclusion is tested as hard as the inclusion.
* arms come with it (that is option A, rejected for noise), so the exclusion is
* tested as hard as the inclusion.
*
* Harness (fixtures under `os.tmpdir()`, `process.exit` recorded rather than
* taken) follows `validate-root-path-line.test.ts`.
Expand Down Expand Up @@ -151,7 +151,7 @@ describe('objectui validate — the arm the document selected', () => {
const text = printed();
// The arm `object-grid` selects, failing on its own required key.
expect(text).toContain('Path: objectName');
// ...and nothing from the 107 arms that merely disagree about `type`.
// ...and nothing from the arms that merely disagree about `type`.
// Option A would have printed one of these per arm; this is the assertion
// that the ruling's rejection of it is real rather than nominal.
expect(text).not.toContain('Invalid discriminator value');
Expand All @@ -176,7 +176,8 @@ describe('objectui validate — when no arm accepts the type', () => {
// distance rather than by case alone.
const names = (line as string).split(':')[1].split(',').map((n) => n.trim());
expect(names[0]).toBe('dropdown-menu');
// ...and the list is CAPPED. Uncapped, this is 108 names.
// ...and the list is CAPPED. Uncapped, this is every arm name the root
// accepts.
expect(names.length).toBeLessThanOrEqual(MAX_UNION_ARMS_REPORTED);
expect(/Nearest of the \d+ accepted types/.test(line as string)).toBe(true);
});
Expand All @@ -189,7 +190,7 @@ describe('objectui validate — when no arm accepts the type', () => {
});

it('offers NO candidates when the document declares no type at all', async () => {
// "Nearest" needs something to be near. Ranking 108 arm names against a
// "Nearest" needs something to be near. Ranking every arm name against a
// `type` the author never wrote would present an alphabetical slice as
// guidance — the bogus suggestion `known-type-case-suggestion.ts` refuses
// to make on the sibling surface.
Expand Down
49 changes: 27 additions & 22 deletions packages/cli/src/utils/union-arm-diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
*
* ## Measured facts this rests on (Zod 4.4.3, measured on this tree)
*
* 1. `errors` is positionally aligned with the union's options: 14 entries for
* `AnyComponentSchema`'s 14 members, `errors[i]` being option `i`'s issues.
* 1. `errors` is positionally aligned with the union's options: one entry per
* member of `AnyComponentSchema`, `errors[i]` being option `i`'s issues.
* Selection here never relies on that alignment — see (2) — but it is why
* the arm lists can be read as arms at all.
* 2. **An arm names the literals it accepts, in its own issues.** Two shapes do
Expand All @@ -48,9 +48,12 @@
* union at `['items', 0]` reports its arm issues at `['type']`, not at
* `['items', 0, 'type']`. Printing them raw would name the wrong node, so
* every path here is rebased onto its parent's prefix.
* 4. `AnyComponentSchema` resolves to 108 leaf arms and 108 DISTINCT `type`
* literals — no literal is claimed by two arms — so "exactly one arm accepts
* that literal" is total and unambiguous at the document root.
* 4. Every leaf arm of `AnyComponentSchema` carries a DISTINCT `type` literal —
* no literal is claimed by two arms — so "exactly one arm accepts that
* literal" is total and unambiguous at the document root. Stated as a
* property and not as a count on purpose: the number of arms moves every
* time a component lands, the distinctness does not, and it is the
* distinctness the selection rests on.
*
* ## The third branch, and why it is not option A
*
Expand All @@ -68,7 +71,7 @@
* dropped. So a union with NO declaring arm falls back to the ruling's own
* named fallback, "A with a cap": every arm reported, capped by
* {@link MAX_UNION_ARMS_REPORTED}. This is not option A at the root, which the
* ruling rejected on the 108-arm noise argument — the root is always
* ruling rejected on the arm-count noise argument — the root is always
* discriminated (fact 4), and undiscriminated unions in this mirror are small
* (`MenuItemSchema` has two arms).
*/
Expand Down Expand Up @@ -117,19 +120,19 @@ export interface UnionArmNote {
* Nearest arm names, already ranked and capped.
*
* EMPTY when the document declares no `type` at all. "Nearest" needs
* something to be near, and an alphabetical slice of 108 arm names presented
* something to be near, and an alphabetical slice of the arm names presented
* under a `type` the author never wrote is a bogus suggestion — the failure
* `known-type-case-suggestion.ts` refuses by returning `undefined` rather
* than guessing. The note still fires; it just names the missing `type` key
* instead of pretending to rank against it.
*/
candidates: string[];
/**
* How many `type` values the union accepts in total (108 at the document
* root). Printed with the candidates so the list reads as "the nearest few of
* a closed set" rather than as a confident "did you mean" — which matters
* because a foreign `type` has no near miss at all: measured, `module`'s
* nearest arm is `toggle` at distance 3.
* How many `type` values the union accepts in total. Printed with the
* candidates so the list reads as "the nearest few of a closed set" rather
* than as a confident "did you mean" — which matters because a foreign
* `type` has no near miss at all: measured, `module`'s nearest arm is
* `toggle` at distance 3.
*/
totalArmNames: number;
}
Expand All @@ -142,20 +145,22 @@ export type UnionArmLine = UnionArmIssue | UnionArmNote;
* and pinned").
*
* Five, from a measurement rather than taste. The candidate set is the union's
* arm names — 108 of them at the document root — so the cap is the whole
* distance between a hint and the option A the ruling rejected.
* arm names, and the document root carries far more of them than five, so the
* cap is the whole distance between a hint and the option A the ruling
* rejected.
*
* Ranking those 108 by 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-menu` at 1, one name at that distance;
* Ranking those arm names by 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-menu` at 1, one name at that distance;
* `dropdwon-menu` -> `dropdown-menu` at 2, one name; `Page` -> `page` at 1, one
* name; `obect-grid` -> `object-grid` at 1, one name), and the next band opens
* 1-6 edits further out holding 1-5 names. So a cap of 5 shows the winner plus
* the following band entire, and stops well short of the 108. A cap of 1 would
* print the winner with nothing around it and read as a confident answer rather
* than as a ranked list — the same over-claim `known-type-case-suggestion.ts`
* refuses on the sibling surface. Five is also the conventional shell and
* compiler suggestion size and holds one terminal line at these name lengths.
* the following band entire, and stops well short of the arm count. A cap of 1
* would print the winner with nothing around it and read as a confident answer
* rather than as a ranked list — the same over-claim
* `known-type-case-suggestion.ts` refuses on the sibling surface. Five is also
* the conventional shell and compiler suggestion size and holds one terminal
* line at these name lengths.
*
* The same number caps the arms reported when a union has no discriminator to
* select on (see this module's header). Both lists answer the same question —
Expand Down
Loading