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
39 changes: 39 additions & 0 deletions .changeset/7004-cli-union-arm-selection.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
---
'@object-ui/cli': minor
---

`objectui validate` now prints the failing union arm the document selected, instead of a
bare "Invalid input" (objectui#7004, maintainer ruling 2026-09-02 — option B).

`safeValidateSchema` checks a document against `AnyComponentSchema`, a `z.union`. When a
document matches no arm, 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. So a menu whose item used the divider spelling retired in
objectui#6523 printed a bare verdict on the whole document, while the remediation text
objectui#6931 wrote into that arm sat one level down, unreachable.

**What is printed now.** When the top-level issue is a failing union:

- the document's `type` selects exactly one arm ⇒ that arm's issues are printed beneath
the entry as `1.1`, `1.2` … with their real paths (`Path: items → 0 → type`) and codes,
and **nothing** from the other arms;
- no arm accepts the `type` ⇒ `No arm accepts type "dropdwn-menu".` plus the nearest few
of the accepted values, ranked by edit distance and **capped** at five
(`MAX_UNION_ARMS_REPORTED`);
- the document declares no `type` at all ⇒ the note says so and offers no candidates —
"nearest" needs something to be near, and an alphabetical slice of 108 arm names
presented as guidance would be a bogus suggestion;
- a union with no `type` discriminator to select on — `MenuItemSchema`, whose two arms
both declare `type` as an ADR-0049 retirement tombstone — reports every arm, labelled
and capped by the same constant. This is the path that finally delivers the
objectui#6523 text to the author.

Printing EVERY arm was rejected in the ruling: `AnyComponentSchema` resolves to 108 leaf
arms, so one mistyped `type` would have produced hundreds of lines.

`objectui check` is unchanged and deliberately so: it has no zod-issue printer, using
`safeValidateSchema(...).success` as a boolean recogniser. 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.

Nothing about which documents are ACCEPTED changes — this is diagnostic output only.
31 changes: 31 additions & 0 deletions content/docs/utilities/cli.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -158,6 +158,37 @@ objectui validate app.json
objectui validate ./schemas/dashboard.yaml
```

#### Reading a failure

A document is checked against a union of every component type, so a failure is
reported first at the document root and then narrowed to the arm your `type`
selected. Each numbered issue carries a path and a code; the `1.1`-style entries
beneath it are that arm's own diagnosis, with the real path to the node that
failed:

```text
1. Invalid input
Path: (root)
Code: invalid_union
1.1 RETIRED (objectui#6523) — dividers are `{ separator: true }`; …
Path: items → 0 → type
Code: invalid_type
```

Only the selected arm is shown. When your `type` matches no component at all,
the report says so and offers the nearest few of the accepted values instead:

```text
1. Invalid input
Path: (root)
Code: invalid_union
No arm accepts type "dropdwn-menu".
Nearest of the 108 accepted types: dropdown-menu, context-menu, component, drawer, breadcrumb
```

A document with no `type` at all is reported the same way, without a candidate
list — there is nothing for the suggestions to be near.

### `objectui check`

Scan the project for ObjectUI schema files and report on the ones it
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,6 +117,13 @@ Add a new component renderer to your project.

Validate a schema file against the ObjectUI specification.

A failure is reported at the document root and then narrowed to the union arm
your `type` selected — the `1.1`-style entries under a numbered issue are that
arm's own diagnosis, at the real path of the node that failed. When no component
type matches, the report names the nearest few of the accepted values instead of
listing all of them. See the
[docs](https://www.objectui.org/docs/utilities/cli) for worked output.

### `objectui check`, `objectui doctor`, `objectui studio`, `objectui analyze`, `objectui create plugin <name>`

Utility commands — see `objectui --help`.
Expand Down
48 changes: 33 additions & 15 deletions packages/cli/src/__tests__/validate-root-path-line.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,11 @@
/**
* `objectui validate` and the ROOT-LEVEL issue (objectui#7004, mechanical half).
*
* The arm-selection half of the same card landed later, on the 2026-09-02
* ruling; its contract lives in `validate-union-arm-selection.test.ts`. The last
* describe block here was the boundary pin that made that landing an explicit
* edit, and now restates the new semantics from this file's point of view.
*
* The printer used to guard its Path line with `issue.path.length > 0`, so an
* issue at the document root (`path: []`) printed no Path line at all — silent
* in precisely the case a reader most needs oriented.
Expand DownExpand Up@@ -181,27 +186,40 @@ describe('objectui validate — a real path is still a real path', () => {
});
});

describe('objectui validate — the arm-selection half is deliberately NOT done here', () => {
describe('objectui validate — the arm-selection half, now that it is ruled', () => {
/**
* ⚠️ This case pins a BOUNDARY, not a desired end state. objectui#7004 splits
* into the root-path line (done, above) and the question of whether a failing
* union should also surface its per-arm diagnoses — and if so, which arm's.
* The second is an author-facing diagnostic contract and is awaiting a
* maintainer ruling, so this file records that the printer walks only the
* top level today.
* ⚠️ This case USED to pin the opposite. It was written as a deliberate
* boundary: while the arm-selection question was with the maintainer, it
* asserted that the printer walked only the top level — `not.toContain
* ('RETIRED (objectui#6523)')`, `not.toContain('Path: items')` — so that when
* the ruling landed it would land as an explicit edit against a RED test
* rather than as a silent widening.
*
* The 2026-09-02 ruling landed (option B: the arm the document's `type`
* selects, nothing from the others), so the boundary moved and these
* assertions are inverted. That is the mechanism working, not a test being
* loosened: the pin forced this file to be opened and the semantics restated
* by hand.
*
* When that ruling lands, this case is EXPECTED to change with it. It exists
* so the change is a deliberate edit rather than a silent widening.
* The full contract lives in `validate-union-arm-selection.test.ts`. What
* stays HERE is the part this file has always been about — that widening the
* printer did not cost the root-path line, and did not turn one top-level
* issue into many.
*/
it('prints one top-level entry for a union, not the per-arm remediation text', async () => {
it('surfaces the selected arm\'s diagnosis without multiplying top-level entries', async () => {
await validate(writeSchema('menu.json', MENU_WITH_RETIRED_DIVIDER));

const text = printed();
// The arm issues carry the objectui#6523 tombstone guidance. Nothing here
// reads `issue.errors`, so none of it is printed.
expect(text).not.toContain('RETIRED (objectui#6523)');
expect(text).not.toContain('Path: items');
// Exactly one numbered issue — an arm walk would multiply this.
// Was `not.toContain` until the ruling. The objectui#6523 tombstone text
// rides the per-arm issues, which is exactly why it never reached an author.
expect(text).toContain('RETIRED (objectui#6523)');
expect(text).toContain('Path: items → 0 → type');
// Unchanged, and load-bearing: the top-level entry still carries the root
// path line this file exists for.
expect(text).toContain('Path: (root)');
// Still exactly one NUMBERED issue. Arm entries are `1.1`-shaped, so a
// reader (and this assertion) can still count the top-level failures — an
// arm walk that emitted them as `2.`, `3.` … would have multiplied this.
const numbered = printed()
.split('\n')
.filter((line) => /^\d+\. /.test(line.trim()));
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
39 changes: 39 additions & 0 deletions .changeset/7004-cli-union-arm-selection.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
---
'@object-ui/cli': minor
---

`objectui validate` now prints the failing union arm the document selected, instead of a
bare "Invalid input" (objectui#7004, maintainer ruling 2026-09-02 — option B).

`safeValidateSchema` checks a document against `AnyComponentSchema`, a `z.union`. When a
document matches no arm, 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. So a menu whose item used the divider spelling retired in
objectui#6523 printed a bare verdict on the whole document, while the remediation text
objectui#6931 wrote into that arm sat one level down, unreachable.

**What is printed now.** When the top-level issue is a failing union:

- the document's `type` selects exactly one arm ⇒ that arm's issues are printed beneath
the entry as `1.1`, `1.2` … with their real paths (`Path: items → 0 → type`) and codes,
and **nothing** from the other arms;
- no arm accepts the `type` ⇒ `No arm accepts type "dropdwn-menu".` plus the nearest few
of the accepted values, ranked by edit distance and **capped** at five
(`MAX_UNION_ARMS_REPORTED`);
- the document declares no `type` at all ⇒ the note says so and offers no candidates —
"nearest" needs something to be near, and an alphabetical slice of 108 arm names
presented as guidance would be a bogus suggestion;
- a union with no `type` discriminator to select on — `MenuItemSchema`, whose two arms
both declare `type` as an ADR-0049 retirement tombstone — reports every arm, labelled
and capped by the same constant. This is the path that finally delivers the
objectui#6523 text to the author.

Printing EVERY arm was rejected in the ruling: `AnyComponentSchema` resolves to 108 leaf
arms, so one mistyped `type` would have produced hundreds of lines.

`objectui check` is unchanged and deliberately so: it has no zod-issue printer, using
`safeValidateSchema(...).success` as a boolean recogniser. 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.

Nothing about which documents are ACCEPTED changes — this is diagnostic output only.
31 changes: 31 additions & 0 deletions content/docs/utilities/cli.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -158,6 +158,37 @@ objectui validate app.json
objectui validate ./schemas/dashboard.yaml
```

#### Reading a failure

A document is checked against a union of every component type, so a failure is
reported first at the document root and then narrowed to the arm your `type`
selected. Each numbered issue carries a path and a code; the `1.1`-style entries
beneath it are that arm's own diagnosis, with the real path to the node that
failed:

```text
1. Invalid input
Path: (root)
Code: invalid_union
1.1 RETIRED (objectui#6523) — dividers are `{ separator: true }`; …
Path: items → 0 → type
Code: invalid_type
```

Only the selected arm is shown. When your `type` matches no component at all,
the report says so and offers the nearest few of the accepted values instead:

```text
1. Invalid input
Path: (root)
Code: invalid_union
No arm accepts type "dropdwn-menu".
Nearest of the 108 accepted types: dropdown-menu, context-menu, component, drawer, breadcrumb
```

A document with no `type` at all is reported the same way, without a candidate
list — there is nothing for the suggestions to be near.

### `objectui check`

Scan the project for ObjectUI schema files and report on the ones it
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,6 +117,13 @@ Add a new component renderer to your project.

Validate a schema file against the ObjectUI specification.

A failure is reported at the document root and then narrowed to the union arm
your `type` selected — the `1.1`-style entries under a numbered issue are that
arm's own diagnosis, at the real path of the node that failed. When no component
type matches, the report names the nearest few of the accepted values instead of
listing all of them. See the
[docs](https://www.objectui.org/docs/utilities/cli) for worked output.

### `objectui check`, `objectui doctor`, `objectui studio`, `objectui analyze`, `objectui create plugin <name>`

Utility commands — see `objectui --help`.
Expand Down
48 changes: 33 additions & 15 deletions packages/cli/src/__tests__/validate-root-path-line.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,11 @@
/**
* `objectui validate` and the ROOT-LEVEL issue (objectui#7004, mechanical half).
*
* The arm-selection half of the same card landed later, on the 2026-09-02
* ruling; its contract lives in `validate-union-arm-selection.test.ts`. The last
* describe block here was the boundary pin that made that landing an explicit
* edit, and now restates the new semantics from this file's point of view.
*
* The printer used to guard its Path line with `issue.path.length > 0`, so an
* issue at the document root (`path: []`) printed no Path line at all — silent
* in precisely the case a reader most needs oriented.
Expand DownExpand Up@@ -181,27 +186,40 @@ describe('objectui validate — a real path is still a real path', () => {
});
});

describe('objectui validate — the arm-selection half is deliberately NOT done here', () => {
describe('objectui validate — the arm-selection half, now that it is ruled', () => {
/**
* ⚠️ This case pins a BOUNDARY, not a desired end state. objectui#7004 splits
* into the root-path line (done, above) and the question of whether a failing
* union should also surface its per-arm diagnoses — and if so, which arm's.
* The second is an author-facing diagnostic contract and is awaiting a
* maintainer ruling, so this file records that the printer walks only the
* top level today.
* ⚠️ This case USED to pin the opposite. It was written as a deliberate
* boundary: while the arm-selection question was with the maintainer, it
* asserted that the printer walked only the top level — `not.toContain
* ('RETIRED (objectui#6523)')`, `not.toContain('Path: items')` — so that when
* the ruling landed it would land as an explicit edit against a RED test
* rather than as a silent widening.
*
* The 2026-09-02 ruling landed (option B: the arm the document's `type`
* selects, nothing from the others), so the boundary moved and these
* assertions are inverted. That is the mechanism working, not a test being
* loosened: the pin forced this file to be opened and the semantics restated
* by hand.
*
* When that ruling lands, this case is EXPECTED to change with it. It exists
* so the change is a deliberate edit rather than a silent widening.
* The full contract lives in `validate-union-arm-selection.test.ts`. What
* stays HERE is the part this file has always been about — that widening the
* printer did not cost the root-path line, and did not turn one top-level
* issue into many.
*/
it('prints one top-level entry for a union, not the per-arm remediation text', async () => {
it('surfaces the selected arm\'s diagnosis without multiplying top-level entries', async () => {
await validate(writeSchema('menu.json', MENU_WITH_RETIRED_DIVIDER));

const text = printed();
// The arm issues carry the objectui#6523 tombstone guidance. Nothing here
// reads `issue.errors`, so none of it is printed.
expect(text).not.toContain('RETIRED (objectui#6523)');
expect(text).not.toContain('Path: items');
// Exactly one numbered issue — an arm walk would multiply this.
// Was `not.toContain` until the ruling. The objectui#6523 tombstone text
// rides the per-arm issues, which is exactly why it never reached an author.
expect(text).toContain('RETIRED (objectui#6523)');
expect(text).toContain('Path: items → 0 → type');
// Unchanged, and load-bearing: the top-level entry still carries the root
// path line this file exists for.
expect(text).toContain('Path: (root)');
// Still exactly one NUMBERED issue. Arm entries are `1.1`-shaped, so a
// reader (and this assertion) can still count the top-level failures — an
// arm walk that emitted them as `2.`, `3.` … would have multiplied this.
const numbered = printed()
.split('\n')
.filter((line) => /^\d+\. /.test(line.trim()));
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
39 changes: 39 additions & 0 deletions .changeset/7004-cli-union-arm-selection.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
---
'@object-ui/cli': minor
---

`objectui validate` now prints the failing union arm the document selected, instead of a
bare "Invalid input" (objectui#7004, maintainer ruling 2026-09-02 — option B).

`safeValidateSchema` checks a document against `AnyComponentSchema`, a `z.union`. When a
document matches no arm, 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. So a menu whose item used the divider spelling retired in
objectui#6523 printed a bare verdict on the whole document, while the remediation text
objectui#6931 wrote into that arm sat one level down, unreachable.

**What is printed now.** When the top-level issue is a failing union:

- the document's `type` selects exactly one arm ⇒ that arm's issues are printed beneath
the entry as `1.1`, `1.2` … with their real paths (`Path: items → 0 → type`) and codes,
and **nothing** from the other arms;
- no arm accepts the `type` ⇒ `No arm accepts type "dropdwn-menu".` plus the nearest few
of the accepted values, ranked by edit distance and **capped** at five
(`MAX_UNION_ARMS_REPORTED`);
- the document declares no `type` at all ⇒ the note says so and offers no candidates —
"nearest" needs something to be near, and an alphabetical slice of 108 arm names
presented as guidance would be a bogus suggestion;
- a union with no `type` discriminator to select on — `MenuItemSchema`, whose two arms
both declare `type` as an ADR-0049 retirement tombstone — reports every arm, labelled
and capped by the same constant. This is the path that finally delivers the
objectui#6523 text to the author.

Printing EVERY arm was rejected in the ruling: `AnyComponentSchema` resolves to 108 leaf
arms, so one mistyped `type` would have produced hundreds of lines.

`objectui check` is unchanged and deliberately so: it has no zod-issue printer, using
`safeValidateSchema(...).success` as a boolean recogniser. 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.

Nothing about which documents are ACCEPTED changes — this is diagnostic output only.
31 changes: 31 additions & 0 deletions content/docs/utilities/cli.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -158,6 +158,37 @@ objectui validate app.json
objectui validate ./schemas/dashboard.yaml
```

#### Reading a failure

A document is checked against a union of every component type, so a failure is
reported first at the document root and then narrowed to the arm your `type`
selected. Each numbered issue carries a path and a code; the `1.1`-style entries
beneath it are that arm's own diagnosis, with the real path to the node that
failed:

```text
1. Invalid input
Path: (root)
Code: invalid_union
1.1 RETIRED (objectui#6523) — dividers are `{ separator: true }`; …
Path: items → 0 → type
Code: invalid_type
```

Only the selected arm is shown. When your `type` matches no component at all,
the report says so and offers the nearest few of the accepted values instead:

```text
1. Invalid input
Path: (root)
Code: invalid_union
No arm accepts type "dropdwn-menu".
Nearest of the 108 accepted types: dropdown-menu, context-menu, component, drawer, breadcrumb
```

A document with no `type` at all is reported the same way, without a candidate
list — there is nothing for the suggestions to be near.

### `objectui check`

Scan the project for ObjectUI schema files and report on the ones it
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,6 +117,13 @@ Add a new component renderer to your project.

Validate a schema file against the ObjectUI specification.

A failure is reported at the document root and then narrowed to the union arm
your `type` selected — the `1.1`-style entries under a numbered issue are that
arm's own diagnosis, at the real path of the node that failed. When no component
type matches, the report names the nearest few of the accepted values instead of
listing all of them. See the
[docs](https://www.objectui.org/docs/utilities/cli) for worked output.

### `objectui check`, `objectui doctor`, `objectui studio`, `objectui analyze`, `objectui create plugin <name>`

Utility commands — see `objectui --help`.
Expand Down
48 changes: 33 additions & 15 deletions packages/cli/src/__tests__/validate-root-path-line.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,11 @@
/**
* `objectui validate` and the ROOT-LEVEL issue (objectui#7004, mechanical half).
*
* The arm-selection half of the same card landed later, on the 2026-09-02
* ruling; its contract lives in `validate-union-arm-selection.test.ts`. The last
* describe block here was the boundary pin that made that landing an explicit
* edit, and now restates the new semantics from this file's point of view.
*
* The printer used to guard its Path line with `issue.path.length > 0`, so an
* issue at the document root (`path: []`) printed no Path line at all — silent
* in precisely the case a reader most needs oriented.
Expand DownExpand Up@@ -181,27 +186,40 @@ describe('objectui validate — a real path is still a real path', () => {
});
});

describe('objectui validate — the arm-selection half is deliberately NOT done here', () => {
describe('objectui validate — the arm-selection half, now that it is ruled', () => {
/**
* ⚠️ This case pins a BOUNDARY, not a desired end state. objectui#7004 splits
* into the root-path line (done, above) and the question of whether a failing
* union should also surface its per-arm diagnoses — and if so, which arm's.
* The second is an author-facing diagnostic contract and is awaiting a
* maintainer ruling, so this file records that the printer walks only the
* top level today.
* ⚠️ This case USED to pin the opposite. It was written as a deliberate
* boundary: while the arm-selection question was with the maintainer, it
* asserted that the printer walked only the top level — `not.toContain
* ('RETIRED (objectui#6523)')`, `not.toContain('Path: items')` — so that when
* the ruling landed it would land as an explicit edit against a RED test
* rather than as a silent widening.
*
* The 2026-09-02 ruling landed (option B: the arm the document's `type`
* selects, nothing from the others), so the boundary moved and these
* assertions are inverted. That is the mechanism working, not a test being
* loosened: the pin forced this file to be opened and the semantics restated
* by hand.
*
* When that ruling lands, this case is EXPECTED to change with it. It exists
* so the change is a deliberate edit rather than a silent widening.
* The full contract lives in `validate-union-arm-selection.test.ts`. What
* stays HERE is the part this file has always been about — that widening the
* printer did not cost the root-path line, and did not turn one top-level
* issue into many.
*/
it('prints one top-level entry for a union, not the per-arm remediation text', async () => {
it('surfaces the selected arm\'s diagnosis without multiplying top-level entries', async () => {
await validate(writeSchema('menu.json', MENU_WITH_RETIRED_DIVIDER));

const text = printed();
// The arm issues carry the objectui#6523 tombstone guidance. Nothing here
// reads `issue.errors`, so none of it is printed.
expect(text).not.toContain('RETIRED (objectui#6523)');
expect(text).not.toContain('Path: items');
// Exactly one numbered issue — an arm walk would multiply this.
// Was `not.toContain` until the ruling. The objectui#6523 tombstone text
// rides the per-arm issues, which is exactly why it never reached an author.
expect(text).toContain('RETIRED (objectui#6523)');
expect(text).toContain('Path: items → 0 → type');
// Unchanged, and load-bearing: the top-level entry still carries the root
// path line this file exists for.
expect(text).toContain('Path: (root)');
// Still exactly one NUMBERED issue. Arm entries are `1.1`-shaped, so a
// reader (and this assertion) can still count the top-level failures — an
// arm walk that emitted them as `2.`, `3.` … would have multiplied this.
const numbered = printed()
.split('\n')
.filter((line) => /^\d+\. /.test(line.trim()));
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
39 changes: 39 additions & 0 deletions .changeset/7004-cli-union-arm-selection.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
---
'@object-ui/cli': minor
---

`objectui validate` now prints the failing union arm the document selected, instead of a
bare "Invalid input" (objectui#7004, maintainer ruling 2026-09-02 — option B).

`safeValidateSchema` checks a document against `AnyComponentSchema`, a `z.union`. When a
document matches no arm, 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. So a menu whose item used the divider spelling retired in
objectui#6523 printed a bare verdict on the whole document, while the remediation text
objectui#6931 wrote into that arm sat one level down, unreachable.

**What is printed now.** When the top-level issue is a failing union:

- the document's `type` selects exactly one arm ⇒ that arm's issues are printed beneath
the entry as `1.1`, `1.2` … with their real paths (`Path: items → 0 → type`) and codes,
and **nothing** from the other arms;
- no arm accepts the `type` ⇒ `No arm accepts type "dropdwn-menu".` plus the nearest few
of the accepted values, ranked by edit distance and **capped** at five
(`MAX_UNION_ARMS_REPORTED`);
- the document declares no `type` at all ⇒ the note says so and offers no candidates —
"nearest" needs something to be near, and an alphabetical slice of 108 arm names
presented as guidance would be a bogus suggestion;
- a union with no `type` discriminator to select on — `MenuItemSchema`, whose two arms
both declare `type` as an ADR-0049 retirement tombstone — reports every arm, labelled
and capped by the same constant. This is the path that finally delivers the
objectui#6523 text to the author.

Printing EVERY arm was rejected in the ruling: `AnyComponentSchema` resolves to 108 leaf
arms, so one mistyped `type` would have produced hundreds of lines.

`objectui check` is unchanged and deliberately so: it has no zod-issue printer, using
`safeValidateSchema(...).success` as a boolean recogniser. 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.

Nothing about which documents are ACCEPTED changes — this is diagnostic output only.
31 changes: 31 additions & 0 deletions content/docs/utilities/cli.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -158,6 +158,37 @@ objectui validate app.json
objectui validate ./schemas/dashboard.yaml
```

#### Reading a failure

A document is checked against a union of every component type, so a failure is
reported first at the document root and then narrowed to the arm your `type`
selected. Each numbered issue carries a path and a code; the `1.1`-style entries
beneath it are that arm's own diagnosis, with the real path to the node that
failed:

```text
1. Invalid input
Path: (root)
Code: invalid_union
1.1 RETIRED (objectui#6523) — dividers are `{ separator: true }`; …
Path: items → 0 → type
Code: invalid_type
```

Only the selected arm is shown. When your `type` matches no component at all,
the report says so and offers the nearest few of the accepted values instead:

```text
1. Invalid input
Path: (root)
Code: invalid_union
No arm accepts type "dropdwn-menu".
Nearest of the 108 accepted types: dropdown-menu, context-menu, component, drawer, breadcrumb
```

A document with no `type` at all is reported the same way, without a candidate
list — there is nothing for the suggestions to be near.

### `objectui check`

Scan the project for ObjectUI schema files and report on the ones it
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,6 +117,13 @@ Add a new component renderer to your project.

Validate a schema file against the ObjectUI specification.

A failure is reported at the document root and then narrowed to the union arm
your `type` selected — the `1.1`-style entries under a numbered issue are that
arm's own diagnosis, at the real path of the node that failed. When no component
type matches, the report names the nearest few of the accepted values instead of
listing all of them. See the
[docs](https://www.objectui.org/docs/utilities/cli) for worked output.

### `objectui check`, `objectui doctor`, `objectui studio`, `objectui analyze`, `objectui create plugin <name>`

Utility commands — see `objectui --help`.
Expand Down
48 changes: 33 additions & 15 deletions packages/cli/src/__tests__/validate-root-path-line.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,11 @@
/**
* `objectui validate` and the ROOT-LEVEL issue (objectui#7004, mechanical half).
*
* The arm-selection half of the same card landed later, on the 2026-09-02
* ruling; its contract lives in `validate-union-arm-selection.test.ts`. The last
* describe block here was the boundary pin that made that landing an explicit
* edit, and now restates the new semantics from this file's point of view.
*
* The printer used to guard its Path line with `issue.path.length > 0`, so an
* issue at the document root (`path: []`) printed no Path line at all — silent
* in precisely the case a reader most needs oriented.
Expand DownExpand Up@@ -181,27 +186,40 @@ describe('objectui validate — a real path is still a real path', () => {
});
});

describe('objectui validate — the arm-selection half is deliberately NOT done here', () => {
describe('objectui validate — the arm-selection half, now that it is ruled', () => {
/**
* ⚠️ This case pins a BOUNDARY, not a desired end state. objectui#7004 splits
* into the root-path line (done, above) and the question of whether a failing
* union should also surface its per-arm diagnoses — and if so, which arm's.
* The second is an author-facing diagnostic contract and is awaiting a
* maintainer ruling, so this file records that the printer walks only the
* top level today.
* ⚠️ This case USED to pin the opposite. It was written as a deliberate
* boundary: while the arm-selection question was with the maintainer, it
* asserted that the printer walked only the top level — `not.toContain
* ('RETIRED (objectui#6523)')`, `not.toContain('Path: items')` — so that when
* the ruling landed it would land as an explicit edit against a RED test
* rather than as a silent widening.
*
* The 2026-09-02 ruling landed (option B: the arm the document's `type`
* selects, nothing from the others), so the boundary moved and these
* assertions are inverted. That is the mechanism working, not a test being
* loosened: the pin forced this file to be opened and the semantics restated
* by hand.
*
* When that ruling lands, this case is EXPECTED to change with it. It exists
* so the change is a deliberate edit rather than a silent widening.
* The full contract lives in `validate-union-arm-selection.test.ts`. What
* stays HERE is the part this file has always been about — that widening the
* printer did not cost the root-path line, and did not turn one top-level
* issue into many.
*/
it('prints one top-level entry for a union, not the per-arm remediation text', async () => {
it('surfaces the selected arm\'s diagnosis without multiplying top-level entries', async () => {
await validate(writeSchema('menu.json', MENU_WITH_RETIRED_DIVIDER));

const text = printed();
// The arm issues carry the objectui#6523 tombstone guidance. Nothing here
// reads `issue.errors`, so none of it is printed.
expect(text).not.toContain('RETIRED (objectui#6523)');
expect(text).not.toContain('Path: items');
// Exactly one numbered issue — an arm walk would multiply this.
// Was `not.toContain` until the ruling. The objectui#6523 tombstone text
// rides the per-arm issues, which is exactly why it never reached an author.
expect(text).toContain('RETIRED (objectui#6523)');
expect(text).toContain('Path: items → 0 → type');
// Unchanged, and load-bearing: the top-level entry still carries the root
// path line this file exists for.
expect(text).toContain('Path: (root)');
// Still exactly one NUMBERED issue. Arm entries are `1.1`-shaped, so a
// reader (and this assertion) can still count the top-level failures — an
// arm walk that emitted them as `2.`, `3.` … would have multiplied this.
const numbered = printed()
.split('\n')
.filter((line) => /^\d+\. /.test(line.trim()));
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
39 changes: 39 additions & 0 deletions .changeset/7004-cli-union-arm-selection.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
---
'@object-ui/cli': minor
---

`objectui validate` now prints the failing union arm the document selected, instead of a
bare "Invalid input" (objectui#7004, maintainer ruling 2026-09-02 — option B).

`safeValidateSchema` checks a document against `AnyComponentSchema`, a `z.union`. When a
document matches no arm, 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. So a menu whose item used the divider spelling retired in
objectui#6523 printed a bare verdict on the whole document, while the remediation text
objectui#6931 wrote into that arm sat one level down, unreachable.

**What is printed now.** When the top-level issue is a failing union:

- the document's `type` selects exactly one arm ⇒ that arm's issues are printed beneath
the entry as `1.1`, `1.2` … with their real paths (`Path: items → 0 → type`) and codes,
and **nothing** from the other arms;
- no arm accepts the `type` ⇒ `No arm accepts type "dropdwn-menu".` plus the nearest few
of the accepted values, ranked by edit distance and **capped** at five
(`MAX_UNION_ARMS_REPORTED`);
- the document declares no `type` at all ⇒ the note says so and offers no candidates —
"nearest" needs something to be near, and an alphabetical slice of 108 arm names
presented as guidance would be a bogus suggestion;
- a union with no `type` discriminator to select on — `MenuItemSchema`, whose two arms
both declare `type` as an ADR-0049 retirement tombstone — reports every arm, labelled
and capped by the same constant. This is the path that finally delivers the
objectui#6523 text to the author.

Printing EVERY arm was rejected in the ruling: `AnyComponentSchema` resolves to 108 leaf
arms, so one mistyped `type` would have produced hundreds of lines.

`objectui check` is unchanged and deliberately so: it has no zod-issue printer, using
`safeValidateSchema(...).success` as a boolean recogniser. 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.

Nothing about which documents are ACCEPTED changes — this is diagnostic output only.
31 changes: 31 additions & 0 deletions content/docs/utilities/cli.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -158,6 +158,37 @@ objectui validate app.json
objectui validate ./schemas/dashboard.yaml
```

#### Reading a failure

A document is checked against a union of every component type, so a failure is
reported first at the document root and then narrowed to the arm your `type`
selected. Each numbered issue carries a path and a code; the `1.1`-style entries
beneath it are that arm's own diagnosis, with the real path to the node that
failed:

```text
1. Invalid input
Path: (root)
Code: invalid_union
1.1 RETIRED (objectui#6523) — dividers are `{ separator: true }`; …
Path: items → 0 → type
Code: invalid_type
```

Only the selected arm is shown. When your `type` matches no component at all,
the report says so and offers the nearest few of the accepted values instead:

```text
1. Invalid input
Path: (root)
Code: invalid_union
No arm accepts type "dropdwn-menu".
Nearest of the 108 accepted types: dropdown-menu, context-menu, component, drawer, breadcrumb
```

A document with no `type` at all is reported the same way, without a candidate
list — there is nothing for the suggestions to be near.

### `objectui check`

Scan the project for ObjectUI schema files and report on the ones it
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,6 +117,13 @@ Add a new component renderer to your project.

Validate a schema file against the ObjectUI specification.

A failure is reported at the document root and then narrowed to the union arm
your `type` selected — the `1.1`-style entries under a numbered issue are that
arm's own diagnosis, at the real path of the node that failed. When no component
type matches, the report names the nearest few of the accepted values instead of
listing all of them. See the
[docs](https://www.objectui.org/docs/utilities/cli) for worked output.

### `objectui check`, `objectui doctor`, `objectui studio`, `objectui analyze`, `objectui create plugin <name>`

Utility commands — see `objectui --help`.
Expand Down
48 changes: 33 additions & 15 deletions packages/cli/src/__tests__/validate-root-path-line.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,11 @@
/**
* `objectui validate` and the ROOT-LEVEL issue (objectui#7004, mechanical half).
*
* The arm-selection half of the same card landed later, on the 2026-09-02
* ruling; its contract lives in `validate-union-arm-selection.test.ts`. The last
* describe block here was the boundary pin that made that landing an explicit
* edit, and now restates the new semantics from this file's point of view.
*
* The printer used to guard its Path line with `issue.path.length > 0`, so an
* issue at the document root (`path: []`) printed no Path line at all — silent
* in precisely the case a reader most needs oriented.
Expand DownExpand Up@@ -181,27 +186,40 @@ describe('objectui validate — a real path is still a real path', () => {
});
});

describe('objectui validate — the arm-selection half is deliberately NOT done here', () => {
describe('objectui validate — the arm-selection half, now that it is ruled', () => {
/**
* ⚠️ This case pins a BOUNDARY, not a desired end state. objectui#7004 splits
* into the root-path line (done, above) and the question of whether a failing
* union should also surface its per-arm diagnoses — and if so, which arm's.
* The second is an author-facing diagnostic contract and is awaiting a
* maintainer ruling, so this file records that the printer walks only the
* top level today.
* ⚠️ This case USED to pin the opposite. It was written as a deliberate
* boundary: while the arm-selection question was with the maintainer, it
* asserted that the printer walked only the top level — `not.toContain
* ('RETIRED (objectui#6523)')`, `not.toContain('Path: items')` — so that when
* the ruling landed it would land as an explicit edit against a RED test
* rather than as a silent widening.
*
* The 2026-09-02 ruling landed (option B: the arm the document's `type`
* selects, nothing from the others), so the boundary moved and these
* assertions are inverted. That is the mechanism working, not a test being
* loosened: the pin forced this file to be opened and the semantics restated
* by hand.
*
* When that ruling lands, this case is EXPECTED to change with it. It exists
* so the change is a deliberate edit rather than a silent widening.
* The full contract lives in `validate-union-arm-selection.test.ts`. What
* stays HERE is the part this file has always been about — that widening the
* printer did not cost the root-path line, and did not turn one top-level
* issue into many.
*/
it('prints one top-level entry for a union, not the per-arm remediation text', async () => {
it('surfaces the selected arm\'s diagnosis without multiplying top-level entries', async () => {
await validate(writeSchema('menu.json', MENU_WITH_RETIRED_DIVIDER));

const text = printed();
// The arm issues carry the objectui#6523 tombstone guidance. Nothing here
// reads `issue.errors`, so none of it is printed.
expect(text).not.toContain('RETIRED (objectui#6523)');
expect(text).not.toContain('Path: items');
// Exactly one numbered issue — an arm walk would multiply this.
// Was `not.toContain` until the ruling. The objectui#6523 tombstone text
// rides the per-arm issues, which is exactly why it never reached an author.
expect(text).toContain('RETIRED (objectui#6523)');
expect(text).toContain('Path: items → 0 → type');
// Unchanged, and load-bearing: the top-level entry still carries the root
// path line this file exists for.
expect(text).toContain('Path: (root)');
// Still exactly one NUMBERED issue. Arm entries are `1.1`-shaped, so a
// reader (and this assertion) can still count the top-level failures — an
// arm walk that emitted them as `2.`, `3.` … would have multiplied this.
const numbered = printed()
.split('\n')
.filter((line) => /^\d+\. /.test(line.trim()));
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
39 changes: 39 additions & 0 deletions .changeset/7004-cli-union-arm-selection.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
---
'@object-ui/cli': minor
---

`objectui validate` now prints the failing union arm the document selected, instead of a
bare "Invalid input" (objectui#7004, maintainer ruling 2026-09-02 — option B).

`safeValidateSchema` checks a document against `AnyComponentSchema`, a `z.union`. When a
document matches no arm, 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. So a menu whose item used the divider spelling retired in
objectui#6523 printed a bare verdict on the whole document, while the remediation text
objectui#6931 wrote into that arm sat one level down, unreachable.

**What is printed now.** When the top-level issue is a failing union:

- the document's `type` selects exactly one arm ⇒ that arm's issues are printed beneath
the entry as `1.1`, `1.2` … with their real paths (`Path: items → 0 → type`) and codes,
and **nothing** from the other arms;
- no arm accepts the `type` ⇒ `No arm accepts type "dropdwn-menu".` plus the nearest few
of the accepted values, ranked by edit distance and **capped** at five
(`MAX_UNION_ARMS_REPORTED`);
- the document declares no `type` at all ⇒ the note says so and offers no candidates —
"nearest" needs something to be near, and an alphabetical slice of 108 arm names
presented as guidance would be a bogus suggestion;
- a union with no `type` discriminator to select on — `MenuItemSchema`, whose two arms
both declare `type` as an ADR-0049 retirement tombstone — reports every arm, labelled
and capped by the same constant. This is the path that finally delivers the
objectui#6523 text to the author.

Printing EVERY arm was rejected in the ruling: `AnyComponentSchema` resolves to 108 leaf
arms, so one mistyped `type` would have produced hundreds of lines.

`objectui check` is unchanged and deliberately so: it has no zod-issue printer, using
`safeValidateSchema(...).success` as a boolean recogniser. 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.

Nothing about which documents are ACCEPTED changes — this is diagnostic output only.
31 changes: 31 additions & 0 deletions content/docs/utilities/cli.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -158,6 +158,37 @@ objectui validate app.json
objectui validate ./schemas/dashboard.yaml
```

#### Reading a failure

A document is checked against a union of every component type, so a failure is
reported first at the document root and then narrowed to the arm your `type`
selected. Each numbered issue carries a path and a code; the `1.1`-style entries
beneath it are that arm's own diagnosis, with the real path to the node that
failed:

```text
1. Invalid input
Path: (root)
Code: invalid_union
1.1 RETIRED (objectui#6523) — dividers are `{ separator: true }`; …
Path: items → 0 → type
Code: invalid_type
```

Only the selected arm is shown. When your `type` matches no component at all,
the report says so and offers the nearest few of the accepted values instead:

```text
1. Invalid input
Path: (root)
Code: invalid_union
No arm accepts type "dropdwn-menu".
Nearest of the 108 accepted types: dropdown-menu, context-menu, component, drawer, breadcrumb
```

A document with no `type` at all is reported the same way, without a candidate
list — there is nothing for the suggestions to be near.

### `objectui check`

Scan the project for ObjectUI schema files and report on the ones it
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,6 +117,13 @@ Add a new component renderer to your project.

Validate a schema file against the ObjectUI specification.

A failure is reported at the document root and then narrowed to the union arm
your `type` selected — the `1.1`-style entries under a numbered issue are that
arm's own diagnosis, at the real path of the node that failed. When no component
type matches, the report names the nearest few of the accepted values instead of
listing all of them. See the
[docs](https://www.objectui.org/docs/utilities/cli) for worked output.

### `objectui check`, `objectui doctor`, `objectui studio`, `objectui analyze`, `objectui create plugin <name>`

Utility commands — see `objectui --help`.
Expand Down
48 changes: 33 additions & 15 deletions packages/cli/src/__tests__/validate-root-path-line.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,11 @@
/**
* `objectui validate` and the ROOT-LEVEL issue (objectui#7004, mechanical half).
*
* The arm-selection half of the same card landed later, on the 2026-09-02
* ruling; its contract lives in `validate-union-arm-selection.test.ts`. The last
* describe block here was the boundary pin that made that landing an explicit
* edit, and now restates the new semantics from this file's point of view.
*
* The printer used to guard its Path line with `issue.path.length > 0`, so an
* issue at the document root (`path: []`) printed no Path line at all — silent
* in precisely the case a reader most needs oriented.
Expand DownExpand Up@@ -181,27 +186,40 @@ describe('objectui validate — a real path is still a real path', () => {
});
});

describe('objectui validate — the arm-selection half is deliberately NOT done here', () => {
describe('objectui validate — the arm-selection half, now that it is ruled', () => {
/**
* ⚠️ This case pins a BOUNDARY, not a desired end state. objectui#7004 splits
* into the root-path line (done, above) and the question of whether a failing
* union should also surface its per-arm diagnoses — and if so, which arm's.
* The second is an author-facing diagnostic contract and is awaiting a
* maintainer ruling, so this file records that the printer walks only the
* top level today.
* ⚠️ This case USED to pin the opposite. It was written as a deliberate
* boundary: while the arm-selection question was with the maintainer, it
* asserted that the printer walked only the top level — `not.toContain
* ('RETIRED (objectui#6523)')`, `not.toContain('Path: items')` — so that when
* the ruling landed it would land as an explicit edit against a RED test
* rather than as a silent widening.
*
* The 2026-09-02 ruling landed (option B: the arm the document's `type`
* selects, nothing from the others), so the boundary moved and these
* assertions are inverted. That is the mechanism working, not a test being
* loosened: the pin forced this file to be opened and the semantics restated
* by hand.
*
* When that ruling lands, this case is EXPECTED to change with it. It exists
* so the change is a deliberate edit rather than a silent widening.
* The full contract lives in `validate-union-arm-selection.test.ts`. What
* stays HERE is the part this file has always been about — that widening the
* printer did not cost the root-path line, and did not turn one top-level
* issue into many.
*/
it('prints one top-level entry for a union, not the per-arm remediation text', async () => {
it('surfaces the selected arm\'s diagnosis without multiplying top-level entries', async () => {
await validate(writeSchema('menu.json', MENU_WITH_RETIRED_DIVIDER));

const text = printed();
// The arm issues carry the objectui#6523 tombstone guidance. Nothing here
// reads `issue.errors`, so none of it is printed.
expect(text).not.toContain('RETIRED (objectui#6523)');
expect(text).not.toContain('Path: items');
// Exactly one numbered issue — an arm walk would multiply this.
// Was `not.toContain` until the ruling. The objectui#6523 tombstone text
// rides the per-arm issues, which is exactly why it never reached an author.
expect(text).toContain('RETIRED (objectui#6523)');
expect(text).toContain('Path: items → 0 → type');
// Unchanged, and load-bearing: the top-level entry still carries the root
// path line this file exists for.
expect(text).toContain('Path: (root)');
// Still exactly one NUMBERED issue. Arm entries are `1.1`-shaped, so a
// reader (and this assertion) can still count the top-level failures — an
// arm walk that emitted them as `2.`, `3.` … would have multiplied this.
const numbered = printed()
.split('\n')
.filter((line) => /^\d+\. /.test(line.trim()));
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
39 changes: 39 additions & 0 deletions .changeset/7004-cli-union-arm-selection.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
---
'@object-ui/cli': minor
---

`objectui validate` now prints the failing union arm the document selected, instead of a
bare "Invalid input" (objectui#7004, maintainer ruling 2026-09-02 — option B).

`safeValidateSchema` checks a document against `AnyComponentSchema`, a `z.union`. When a
document matches no arm, 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. So a menu whose item used the divider spelling retired in
objectui#6523 printed a bare verdict on the whole document, while the remediation text
objectui#6931 wrote into that arm sat one level down, unreachable.

**What is printed now.** When the top-level issue is a failing union:

- the document's `type` selects exactly one arm ⇒ that arm's issues are printed beneath
the entry as `1.1`, `1.2` … with their real paths (`Path: items → 0 → type`) and codes,
and **nothing** from the other arms;
- no arm accepts the `type` ⇒ `No arm accepts type "dropdwn-menu".` plus the nearest few
of the accepted values, ranked by edit distance and **capped** at five
(`MAX_UNION_ARMS_REPORTED`);
- the document declares no `type` at all ⇒ the note says so and offers no candidates —
"nearest" needs something to be near, and an alphabetical slice of 108 arm names
presented as guidance would be a bogus suggestion;
- a union with no `type` discriminator to select on — `MenuItemSchema`, whose two arms
both declare `type` as an ADR-0049 retirement tombstone — reports every arm, labelled
and capped by the same constant. This is the path that finally delivers the
objectui#6523 text to the author.

Printing EVERY arm was rejected in the ruling: `AnyComponentSchema` resolves to 108 leaf
arms, so one mistyped `type` would have produced hundreds of lines.

`objectui check` is unchanged and deliberately so: it has no zod-issue printer, using
`safeValidateSchema(...).success` as a boolean recogniser. 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.

Nothing about which documents are ACCEPTED changes — this is diagnostic output only.
31 changes: 31 additions & 0 deletions content/docs/utilities/cli.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -158,6 +158,37 @@ objectui validate app.json
objectui validate ./schemas/dashboard.yaml
```

#### Reading a failure

A document is checked against a union of every component type, so a failure is
reported first at the document root and then narrowed to the arm your `type`
selected. Each numbered issue carries a path and a code; the `1.1`-style entries
beneath it are that arm's own diagnosis, with the real path to the node that
failed:

```text
1. Invalid input
Path: (root)
Code: invalid_union
1.1 RETIRED (objectui#6523) — dividers are `{ separator: true }`; …
Path: items → 0 → type
Code: invalid_type
```

Only the selected arm is shown. When your `type` matches no component at all,
the report says so and offers the nearest few of the accepted values instead:

```text
1. Invalid input
Path: (root)
Code: invalid_union
No arm accepts type "dropdwn-menu".
Nearest of the 108 accepted types: dropdown-menu, context-menu, component, drawer, breadcrumb
```

A document with no `type` at all is reported the same way, without a candidate
list — there is nothing for the suggestions to be near.

### `objectui check`

Scan the project for ObjectUI schema files and report on the ones it
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,6 +117,13 @@ Add a new component renderer to your project.

Validate a schema file against the ObjectUI specification.

A failure is reported at the document root and then narrowed to the union arm
your `type` selected — the `1.1`-style entries under a numbered issue are that
arm's own diagnosis, at the real path of the node that failed. When no component
type matches, the report names the nearest few of the accepted values instead of
listing all of them. See the
[docs](https://www.objectui.org/docs/utilities/cli) for worked output.

### `objectui check`, `objectui doctor`, `objectui studio`, `objectui analyze`, `objectui create plugin <name>`

Utility commands — see `objectui --help`.
Expand Down
48 changes: 33 additions & 15 deletions packages/cli/src/__tests__/validate-root-path-line.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,11 @@
/**
* `objectui validate` and the ROOT-LEVEL issue (objectui#7004, mechanical half).
*
* The arm-selection half of the same card landed later, on the 2026-09-02
* ruling; its contract lives in `validate-union-arm-selection.test.ts`. The last
* describe block here was the boundary pin that made that landing an explicit
* edit, and now restates the new semantics from this file's point of view.
*
* The printer used to guard its Path line with `issue.path.length > 0`, so an
* issue at the document root (`path: []`) printed no Path line at all — silent
* in precisely the case a reader most needs oriented.
Expand DownExpand Up@@ -181,27 +186,40 @@ describe('objectui validate — a real path is still a real path', () => {
});
});

describe('objectui validate — the arm-selection half is deliberately NOT done here', () => {
describe('objectui validate — the arm-selection half, now that it is ruled', () => {
/**
* ⚠️ This case pins a BOUNDARY, not a desired end state. objectui#7004 splits
* into the root-path line (done, above) and the question of whether a failing
* union should also surface its per-arm diagnoses — and if so, which arm's.
* The second is an author-facing diagnostic contract and is awaiting a
* maintainer ruling, so this file records that the printer walks only the
* top level today.
* ⚠️ This case USED to pin the opposite. It was written as a deliberate
* boundary: while the arm-selection question was with the maintainer, it
* asserted that the printer walked only the top level — `not.toContain
* ('RETIRED (objectui#6523)')`, `not.toContain('Path: items')` — so that when
* the ruling landed it would land as an explicit edit against a RED test
* rather than as a silent widening.
*
* The 2026-09-02 ruling landed (option B: the arm the document's `type`
* selects, nothing from the others), so the boundary moved and these
* assertions are inverted. That is the mechanism working, not a test being
* loosened: the pin forced this file to be opened and the semantics restated
* by hand.
*
* When that ruling lands, this case is EXPECTED to change with it. It exists
* so the change is a deliberate edit rather than a silent widening.
* The full contract lives in `validate-union-arm-selection.test.ts`. What
* stays HERE is the part this file has always been about — that widening the
* printer did not cost the root-path line, and did not turn one top-level
* issue into many.
*/
it('prints one top-level entry for a union, not the per-arm remediation text', async () => {
it('surfaces the selected arm\'s diagnosis without multiplying top-level entries', async () => {
await validate(writeSchema('menu.json', MENU_WITH_RETIRED_DIVIDER));

const text = printed();
// The arm issues carry the objectui#6523 tombstone guidance. Nothing here
// reads `issue.errors`, so none of it is printed.
expect(text).not.toContain('RETIRED (objectui#6523)');
expect(text).not.toContain('Path: items');
// Exactly one numbered issue — an arm walk would multiply this.
// Was `not.toContain` until the ruling. The objectui#6523 tombstone text
// rides the per-arm issues, which is exactly why it never reached an author.
expect(text).toContain('RETIRED (objectui#6523)');
expect(text).toContain('Path: items → 0 → type');
// Unchanged, and load-bearing: the top-level entry still carries the root
// path line this file exists for.
expect(text).toContain('Path: (root)');
// Still exactly one NUMBERED issue. Arm entries are `1.1`-shaped, so a
// reader (and this assertion) can still count the top-level failures — an
// arm walk that emitted them as `2.`, `3.` … would have multiplied this.
const numbered = printed()
.split('\n')
.filter((line) => /^\d+\. /.test(line.trim()));
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
39 changes: 39 additions & 0 deletions .changeset/7004-cli-union-arm-selection.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
---
'@object-ui/cli': minor
---

`objectui validate` now prints the failing union arm the document selected, instead of a
bare "Invalid input" (objectui#7004, maintainer ruling 2026-09-02 — option B).

`safeValidateSchema` checks a document against `AnyComponentSchema`, a `z.union`. When a
document matches no arm, 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. So a menu whose item used the divider spelling retired in
objectui#6523 printed a bare verdict on the whole document, while the remediation text
objectui#6931 wrote into that arm sat one level down, unreachable.

**What is printed now.** When the top-level issue is a failing union:

- the document's `type` selects exactly one arm ⇒ that arm's issues are printed beneath
the entry as `1.1`, `1.2` … with their real paths (`Path: items → 0 → type`) and codes,
and **nothing** from the other arms;
- no arm accepts the `type` ⇒ `No arm accepts type "dropdwn-menu".` plus the nearest few
of the accepted values, ranked by edit distance and **capped** at five
(`MAX_UNION_ARMS_REPORTED`);
- the document declares no `type` at all ⇒ the note says so and offers no candidates —
"nearest" needs something to be near, and an alphabetical slice of 108 arm names
presented as guidance would be a bogus suggestion;
- a union with no `type` discriminator to select on — `MenuItemSchema`, whose two arms
both declare `type` as an ADR-0049 retirement tombstone — reports every arm, labelled
and capped by the same constant. This is the path that finally delivers the
objectui#6523 text to the author.

Printing EVERY arm was rejected in the ruling: `AnyComponentSchema` resolves to 108 leaf
arms, so one mistyped `type` would have produced hundreds of lines.

`objectui check` is unchanged and deliberately so: it has no zod-issue printer, using
`safeValidateSchema(...).success` as a boolean recogniser. 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.

Nothing about which documents are ACCEPTED changes — this is diagnostic output only.
31 changes: 31 additions & 0 deletions content/docs/utilities/cli.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -158,6 +158,37 @@ objectui validate app.json
objectui validate ./schemas/dashboard.yaml
```

#### Reading a failure

A document is checked against a union of every component type, so a failure is
reported first at the document root and then narrowed to the arm your `type`
selected. Each numbered issue carries a path and a code; the `1.1`-style entries
beneath it are that arm's own diagnosis, with the real path to the node that
failed:

```text
1. Invalid input
Path: (root)
Code: invalid_union
1.1 RETIRED (objectui#6523) — dividers are `{ separator: true }`; …
Path: items → 0 → type
Code: invalid_type
```

Only the selected arm is shown. When your `type` matches no component at all,
the report says so and offers the nearest few of the accepted values instead:

```text
1. Invalid input
Path: (root)
Code: invalid_union
No arm accepts type "dropdwn-menu".
Nearest of the 108 accepted types: dropdown-menu, context-menu, component, drawer, breadcrumb
```

A document with no `type` at all is reported the same way, without a candidate
list — there is nothing for the suggestions to be near.

### `objectui check`

Scan the project for ObjectUI schema files and report on the ones it
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,6 +117,13 @@ Add a new component renderer to your project.

Validate a schema file against the ObjectUI specification.

A failure is reported at the document root and then narrowed to the union arm
your `type` selected — the `1.1`-style entries under a numbered issue are that
arm's own diagnosis, at the real path of the node that failed. When no component
type matches, the report names the nearest few of the accepted values instead of
listing all of them. See the
[docs](https://www.objectui.org/docs/utilities/cli) for worked output.

### `objectui check`, `objectui doctor`, `objectui studio`, `objectui analyze`, `objectui create plugin <name>`

Utility commands — see `objectui --help`.
Expand Down
48 changes: 33 additions & 15 deletions packages/cli/src/__tests__/validate-root-path-line.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,11 @@
/**
* `objectui validate` and the ROOT-LEVEL issue (objectui#7004, mechanical half).
*
* The arm-selection half of the same card landed later, on the 2026-09-02
* ruling; its contract lives in `validate-union-arm-selection.test.ts`. The last
* describe block here was the boundary pin that made that landing an explicit
* edit, and now restates the new semantics from this file's point of view.
*
* The printer used to guard its Path line with `issue.path.length > 0`, so an
* issue at the document root (`path: []`) printed no Path line at all — silent
* in precisely the case a reader most needs oriented.
Expand DownExpand Up@@ -181,27 +186,40 @@ describe('objectui validate — a real path is still a real path', () => {
});
});

describe('objectui validate — the arm-selection half is deliberately NOT done here', () => {
describe('objectui validate — the arm-selection half, now that it is ruled', () => {
/**
* ⚠️ This case pins a BOUNDARY, not a desired end state. objectui#7004 splits
* into the root-path line (done, above) and the question of whether a failing
* union should also surface its per-arm diagnoses — and if so, which arm's.
* The second is an author-facing diagnostic contract and is awaiting a
* maintainer ruling, so this file records that the printer walks only the
* top level today.
* ⚠️ This case USED to pin the opposite. It was written as a deliberate
* boundary: while the arm-selection question was with the maintainer, it
* asserted that the printer walked only the top level — `not.toContain
* ('RETIRED (objectui#6523)')`, `not.toContain('Path: items')` — so that when
* the ruling landed it would land as an explicit edit against a RED test
* rather than as a silent widening.
*
* The 2026-09-02 ruling landed (option B: the arm the document's `type`
* selects, nothing from the others), so the boundary moved and these
* assertions are inverted. That is the mechanism working, not a test being
* loosened: the pin forced this file to be opened and the semantics restated
* by hand.
*
* When that ruling lands, this case is EXPECTED to change with it. It exists
* so the change is a deliberate edit rather than a silent widening.
* The full contract lives in `validate-union-arm-selection.test.ts`. What
* stays HERE is the part this file has always been about — that widening the
* printer did not cost the root-path line, and did not turn one top-level
* issue into many.
*/
it('prints one top-level entry for a union, not the per-arm remediation text', async () => {
it('surfaces the selected arm\'s diagnosis without multiplying top-level entries', async () => {
await validate(writeSchema('menu.json', MENU_WITH_RETIRED_DIVIDER));

const text = printed();
// The arm issues carry the objectui#6523 tombstone guidance. Nothing here
// reads `issue.errors`, so none of it is printed.
expect(text).not.toContain('RETIRED (objectui#6523)');
expect(text).not.toContain('Path: items');
// Exactly one numbered issue — an arm walk would multiply this.
// Was `not.toContain` until the ruling. The objectui#6523 tombstone text
// rides the per-arm issues, which is exactly why it never reached an author.
expect(text).toContain('RETIRED (objectui#6523)');
expect(text).toContain('Path: items → 0 → type');
// Unchanged, and load-bearing: the top-level entry still carries the root
// path line this file exists for.
expect(text).toContain('Path: (root)');
// Still exactly one NUMBERED issue. Arm entries are `1.1`-shaped, so a
// reader (and this assertion) can still count the top-level failures — an
// arm walk that emitted them as `2.`, `3.` … would have multiplied this.
const numbered = printed()
.split('\n')
.filter((line) => /^\d+\. /.test(line.trim()));
Expand Down
Loading
Loading