From e83fe5064d2d91bb8ce2a412ba9478a4ffd111cc Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 19:23:06 +0000 Subject: [PATCH] docs(cli): state the union-arm property, not the arm count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `union-arm-diagnostics.ts` and its test quoted the size of `AnyComponentSchema` as prose integers — "108 leaf arms and 108 DISTINCT `type` literals", "stops well short of the 108", "14 entries for `AnyComponentSchema`'s 14 members" — inside a module whose header is explicit that its facts are measured on the tree. Both numbers had drifted. The card (objectui#7702) measured 105/103; this branch measures 107 arms and 13 root options. Three different arm counts inside one week, in both directions. Rather than re-measure and re-date the integers, state the property they were standing in for. What the display-cap argument actually rests on is that no literal is claimed by two arms — that is what makes "exactly one arm accepts that literal" total — and that is true independently of how many arms there are. The alignment sentence needs no count at all; the module already records that it never relies on the alignment. Twelve comment sites in two files, found by content rather than by line number. Two of them (the test file's "the other 107 arms") restate the same integer derived, and were not on the card's list; a content sweep is what turned them up. No count is pinned by a test: a pinned count would red the build every time a component lands, which is strictly worse than the stale comment. No executable line changes. Proven with the TypeScript parser: transpiling both revisions with `removeComments: true` yields byte-identical output for both files. The changeset therefore declares an empty frontmatter — nothing ships. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3 --- .changeset/7702-union-arm-count-prose.md | 21 ++++++++ .../validate-union-arm-selection.test.ts | 11 +++-- .../cli/src/utils/union-arm-diagnostics.ts | 49 ++++++++++--------- 3 files changed, 54 insertions(+), 27 deletions(-) create mode 100644 .changeset/7702-union-arm-count-prose.md diff --git a/.changeset/7702-union-arm-count-prose.md b/.changeset/7702-union-arm-count-prose.md new file mode 100644 index 0000000000..e663ecf618 --- /dev/null +++ b/.changeset/7702-union-arm-count-prose.md @@ -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. diff --git a/packages/cli/src/__tests__/validate-union-arm-selection.test.ts b/packages/cli/src/__tests__/validate-union-arm-selection.test.ts index 9ea95490c4..0bcd627fe3 100644 --- a/packages/cli/src/__tests__/validate-union-arm-selection.test.ts +++ b/packages/cli/src/__tests__/validate-union-arm-selection.test.ts @@ -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`. @@ -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'); @@ -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); }); @@ -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. diff --git a/packages/cli/src/utils/union-arm-diagnostics.ts b/packages/cli/src/utils/union-arm-diagnostics.ts index dc406e1ae7..7129aba6e8 100644 --- a/packages/cli/src/utils/union-arm-diagnostics.ts +++ b/packages/cli/src/utils/union-arm-diagnostics.ts @@ -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 @@ -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 * @@ -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). */ @@ -117,7 +120,7 @@ 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 @@ -125,11 +128,11 @@ export interface UnionArmNote { */ 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; } @@ -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 —