From 2546a0b97b0e898fd4f337b792b8db2a50f59813 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 12:32:11 +0000 Subject: [PATCH 1/2] fix(types): header-bar refusals and nine body docblocks name the parser tier's unknown-prop warning The nine HeaderBarSchema refusal strings (title, logo, nav, left, center, right, sticky, height, variant) and the nine `body?: never` docblocks in layout.ts (box, span, container, flex, stack, grid, scroll-area) and form.ts (toggle, form) said an authored value drew "no error, no warning". The render path is as described, but the parser tier is not: validateTree answers each of those keys, on the bare and the ui: registration, with an `unknown-prop` warning. Only that clause changes; every other byte of each string and docblock is kept. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm --- packages/types/src/form.ts | 10 ++++--- packages/types/src/layout.ts | 35 ++++++++++++++---------- packages/types/src/zod/navigation.zod.ts | 18 ++++++------ 3 files changed, 36 insertions(+), 27 deletions(-) diff --git a/packages/types/src/form.ts b/packages/types/src/form.ts index 0123d61988..daad6d395f 100644 --- a/packages/types/src/form.ts +++ b/packages/types/src/form.ts @@ -877,8 +877,9 @@ export interface ToggleSchema extends BaseSchema { * * Before objectui#8284 tombstoned it here, `body` was inherited-and-optional * from {@link BaseSchema} — so authoring it here type-checked, parsed green - * through `.passthrough()`, and rendered an EMPTY element with no error and - * no warning. Per component, the channel a renderer does not read is now + * through `.passthrough()`, and rendered an EMPTY element with no render-time + * error or warning; only the parser tier's `unknown-prop` warning noticed it. + * Per component, the channel a renderer does not read is now * tombstoned on both published faces (maintainer ruling, summon #17 decision * batch #2, 2026-09-07), and objectui#6771 has since retired `body` on * `BaseSchema` itself. @@ -2131,8 +2132,9 @@ export interface FormSchema extends BaseSchema { * * Before objectui#8284 tombstoned it here, `body` was inherited-and-optional * from {@link BaseSchema} — so authoring it here type-checked, parsed green - * through `.passthrough()`, and rendered an EMPTY element with no error and - * no warning. Per component, the channel a renderer does not read is now + * through `.passthrough()`, and rendered an EMPTY element with no render-time + * error or warning; only the parser tier's `unknown-prop` warning noticed it. + * Per component, the channel a renderer does not read is now * tombstoned on both published faces (maintainer ruling, summon #17 decision * batch #2, 2026-09-07), and objectui#6771 has since retired `body` on * `BaseSchema` itself. diff --git a/packages/types/src/layout.ts b/packages/types/src/layout.ts index cdf8f0f7e1..a90b246c7a 100644 --- a/packages/types/src/layout.ts +++ b/packages/types/src/layout.ts @@ -70,8 +70,9 @@ export interface BoxSchema extends BaseSchema { * * Before objectui#8284 tombstoned it here, `body` was inherited-and-optional * from {@link BaseSchema} — so authoring it here type-checked, parsed green - * through `.passthrough()`, and rendered an EMPTY element with no error and - * no warning. Per component, the channel a renderer does not read is now + * through `.passthrough()`, and rendered an EMPTY element with no render-time + * error or warning; only the parser tier's `unknown-prop` warning noticed it. + * Per component, the channel a renderer does not read is now * tombstoned on both published faces (maintainer ruling, summon #17 decision * batch #2, 2026-09-07), and objectui#6771 has since retired `body` on * `BaseSchema` itself. @@ -109,8 +110,9 @@ export interface TextSpanSchema extends BaseSchema { * * Before objectui#8284 tombstoned it here, `body` was inherited-and-optional * from {@link BaseSchema} — so authoring it here type-checked, parsed green - * through `.passthrough()`, and rendered an EMPTY element with no error and - * no warning. Per component, the channel a renderer does not read is now + * through `.passthrough()`, and rendered an EMPTY element with no render-time + * error or warning; only the parser tier's `unknown-prop` warning noticed it. + * Per component, the channel a renderer does not read is now * tombstoned on both published faces (maintainer ruling, summon #17 decision * batch #2, 2026-09-07), and objectui#6771 has since retired `body` on * `BaseSchema` itself. @@ -583,8 +585,9 @@ export interface ContainerSchema extends BaseSchema { * * Before objectui#8284 tombstoned it here, `body` was inherited-and-optional * from {@link BaseSchema} — so authoring it here type-checked, parsed green - * through `.passthrough()`, and rendered an EMPTY element with no error and - * no warning. Per component, the channel a renderer does not read is now + * through `.passthrough()`, and rendered an EMPTY element with no render-time + * error or warning; only the parser tier's `unknown-prop` warning noticed it. + * Per component, the channel a renderer does not read is now * tombstoned on both published faces (maintainer ruling, summon #17 decision * batch #2, 2026-09-07), and objectui#6771 has since retired `body` on * `BaseSchema` itself. @@ -712,8 +715,9 @@ export interface FlexSchema extends BaseSchema, FlexLayoutProps { * * Before objectui#8284 tombstoned it here, `body` was inherited-and-optional * from {@link BaseSchema} — so authoring it here type-checked, parsed green - * through `.passthrough()`, and rendered an EMPTY element with no error and - * no warning. Per component, the channel a renderer does not read is now + * through `.passthrough()`, and rendered an EMPTY element with no render-time + * error or warning; only the parser tier's `unknown-prop` warning noticed it. + * Per component, the channel a renderer does not read is now * tombstoned on both published faces (maintainer ruling, summon #17 decision * batch #2, 2026-09-07), and objectui#6771 has since retired `body` on * `BaseSchema` itself. @@ -747,8 +751,9 @@ export interface StackSchema extends BaseSchema, FlexLayoutProps { * * Before objectui#8284 tombstoned it here, `body` was inherited-and-optional * from {@link BaseSchema} — so authoring it here type-checked, parsed green - * through `.passthrough()`, and rendered an EMPTY element with no error and - * no warning. Per component, the channel a renderer does not read is now + * through `.passthrough()`, and rendered an EMPTY element with no render-time + * error or warning; only the parser tier's `unknown-prop` warning noticed it. + * Per component, the channel a renderer does not read is now * tombstoned on both published faces (maintainer ruling, summon #17 decision * batch #2, 2026-09-07), and objectui#6771 has since retired `body` on * `BaseSchema` itself. @@ -799,8 +804,9 @@ export interface GridSchema extends BaseSchema { * * Before objectui#8284 tombstoned it here, `body` was inherited-and-optional * from {@link BaseSchema} — so authoring it here type-checked, parsed green - * through `.passthrough()`, and rendered an EMPTY element with no error and - * no warning. Per component, the channel a renderer does not read is now + * through `.passthrough()`, and rendered an EMPTY element with no render-time + * error or warning; only the parser tier's `unknown-prop` warning noticed it. + * Per component, the channel a renderer does not read is now * tombstoned on both published faces (maintainer ruling, summon #17 decision * batch #2, 2026-09-07), and objectui#6771 has since retired `body` on * `BaseSchema` itself. @@ -1079,8 +1085,9 @@ export interface ScrollAreaSchema extends BaseSchema { * * Before objectui#8284 tombstoned it here, `body` was inherited-and-optional * from {@link BaseSchema} — so authoring it here type-checked, parsed green - * through `.passthrough()`, and rendered an EMPTY element with no error and - * no warning. Per component, the channel a renderer does not read is now + * through `.passthrough()`, and rendered an EMPTY element with no render-time + * error or warning; only the parser tier's `unknown-prop` warning noticed it. + * Per component, the channel a renderer does not read is now * tombstoned on both published faces (maintainer ruling, summon #17 decision * batch #2, 2026-09-07), and objectui#6771 has since retired `body` on * `BaseSchema` itself. diff --git a/packages/types/src/zod/navigation.zod.ts b/packages/types/src/zod/navigation.zod.ts index 2a50556c9b..ad46ba82cb 100644 --- a/packages/types/src/zod/navigation.zod.ts +++ b/packages/types/src/zod/navigation.zod.ts @@ -66,21 +66,21 @@ export const HeaderBarSchema = BaseSchema.extend({ 'REFUSED (objectui#10387, ADR-0049) — `header-bar` reads no `title`: the key is not in ' + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent`, `search` and ' + 'the inherited `className` off the node, and forwards to its root only what the shared DOM whitelist admits plus `style`, so an authored title rendered nothing — ' - + 'no error, no warning, no element. What it renders instead: `actions`, `crumbs`, ' + + 'no render-time error or warning and no element; only the parser tier\'s `unknown-prop` warning noticed it. What it renders instead: `actions`, `crumbs`, ' + '`rightContent`, `search`. For the current page name use the last entry of `crumbs`.', ), logo: retirementTombstone( 'REFUSED (objectui#10387, ADR-0049) — `header-bar` reads no `logo`: the key is not in ' + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent`, `search` and ' + 'the inherited `className` off the node, and forwards to its root only what the shared DOM whitelist admits plus `style`, so an authored logo (URL or node) rendered nothing — ' - + 'no error, no warning, no element. What it renders instead: `actions`, `crumbs`, ' + + 'no render-time error or warning and no element; only the parser tier\'s `unknown-prop` warning noticed it. What it renders instead: `actions`, `crumbs`, ' + '`rightContent`, `search`. For brand content use `rightContent` or `actions`.', ), nav: retirementTombstone( 'REFUSED (objectui#10387, ADR-0049) — `header-bar` reads no `nav`: the key is not in ' + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent`, `search` and ' + 'the inherited `className` off the node, and forwards to its root only what the shared DOM whitelist admits plus `style`, so an authored link list rendered nothing — ' - + 'no error, no warning, no element. What it renders instead: `actions`, `crumbs`, ' + + 'no render-time error or warning and no element; only the parser tier\'s `unknown-prop` warning noticed it. What it renders instead: `actions`, `crumbs`, ' + '`rightContent`, `search`. For links use `crumbs`, or a `navigation-menu` / `sidebar` node.', ), crumbs: z.array(BreadcrumbItemSchema).optional().describe('Breadcrumb items'), @@ -95,35 +95,35 @@ export const HeaderBarSchema = BaseSchema.extend({ 'REFUSED (objectui#10387, ADR-0049) — `header-bar` reads no `left`: the key is not in ' + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent`, `search` and ' + 'the inherited `className` off the node, and forwards to its root only what the shared DOM whitelist admits plus `style`, so an authored node rendered nothing — ' - + 'no error, no warning, no element. What it renders instead: `actions`, `crumbs`, ' + + 'no render-time error or warning and no element; only the parser tier\'s `unknown-prop` warning noticed it. What it renders instead: `actions`, `crumbs`, ' + '`rightContent`, `search`. For custom content use `rightContent` or `actions`.', ), center: retirementTombstone( 'REFUSED (objectui#10387, ADR-0049) — `header-bar` reads no `center`: the key is not in ' + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent`, `search` and ' + 'the inherited `className` off the node, and forwards to its root only what the shared DOM whitelist admits plus `style`, so an authored node rendered nothing — ' - + 'no error, no warning, no element. What it renders instead: `actions`, `crumbs`, ' + + 'no render-time error or warning and no element; only the parser tier\'s `unknown-prop` warning noticed it. What it renders instead: `actions`, `crumbs`, ' + '`rightContent`, `search`. For custom content use `rightContent` or `actions`.', ), right: retirementTombstone( 'REFUSED (objectui#10387, ADR-0049) — `header-bar` reads no `right`: the key is not in ' + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent`, `search` and ' + 'the inherited `className` off the node, and forwards to its root only what the shared DOM whitelist admits plus `style`, so an authored node rendered nothing — ' - + 'no error, no warning, no element. What it renders instead: `actions`, `crumbs`, ' + + 'no render-time error or warning and no element; only the parser tier\'s `unknown-prop` warning noticed it. What it renders instead: `actions`, `crumbs`, ' + '`rightContent`, `search`. The right side is `actions` (a node list) and `rightContent` (one node).', ), sticky: retirementTombstone( 'REFUSED (objectui#10387, ADR-0049) — `header-bar` reads no `sticky`: the key is not in ' + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent`, `search` and ' + 'the inherited `className` off the node, and forwards to its root only what the shared DOM whitelist admits plus `style`, so true and false drew the same non-sticky header — ' - + 'no error, no warning, no element. What it renders instead: `actions`, `crumbs`, ' + + 'no render-time error or warning and no element; only the parser tier\'s `unknown-prop` warning noticed it. What it renders instead: `actions`, `crumbs`, ' + '`rightContent`, `search`. To pin the header, make its parent layout sticky.', ), height: retirementTombstone( 'REFUSED (objectui#10387, ADR-0049) — `header-bar` reads no `height`: the key is not in ' + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent`, `search` and ' + 'the inherited `className` off the node, and forwards to its root only what the shared DOM whitelist admits plus `style`, so every value drew the same default-height header — ' - + 'no error, no warning, no element. What it renders instead: `actions`, `crumbs`, ' + + 'no render-time error or warning and no element; only the parser tier\'s `unknown-prop` warning noticed it. What it renders instead: `actions`, `crumbs`, ' + '`rightContent`, `search`. To change the height, author `className` (e.g. `h-20 sm:h-20`); ' + 'it is merged after the default `h-14` / `sm:h-16` the renderer sets.', ), @@ -131,7 +131,7 @@ export const HeaderBarSchema = BaseSchema.extend({ 'REFUSED (objectui#10286, ADR-0049) — `header-bar` reads no `variant`: the key is not in ' + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent`, `search` and ' + 'the inherited `className` off the node, and forwards to its root only what the shared DOM whitelist admits plus `style`, so every variant rendered the same header — ' - + 'no error, no warning, no class. What it renders instead: `actions`, `crumbs`, `rightContent`, ' + + 'no render-time error or warning and no class; only the parser tier\'s `unknown-prop` warning noticed it. What it renders instead: `actions`, `crumbs`, `rightContent`, ' + '`search`.', ), body: retirementTombstone( From 7089f691c36e8160c79347588c489313b89cd78c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 12:34:25 +0000 Subject: [PATCH 2/2] test(types): pin the unknown-prop clause on header-bar refusals and body docblocks; changeset The pin reads one header-bar key of each clause form through its refusal (invalid_type at the key's own path, then the clause substring), every header-bar member of the objectui#10387 / objectui#10286 retirements, the body docblock of BoxSchema and FormSchema, and every objectui#8284 body docblock in layout.ts and form.ts. Changeset: @object-ui/types patch. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm --- .../10959-unknown-prop-parser-tier-clause.md | 9 + ...nown-prop-parser-tier-clause-10959.test.ts | 191 ++++++++++++++++++ 2 files changed, 200 insertions(+) create mode 100644 .changeset/10959-unknown-prop-parser-tier-clause.md create mode 100644 packages/types/src/__tests__/unknown-prop-parser-tier-clause-10959.test.ts diff --git a/.changeset/10959-unknown-prop-parser-tier-clause.md b/.changeset/10959-unknown-prop-parser-tier-clause.md new file mode 100644 index 0000000000..c17049658d --- /dev/null +++ b/.changeset/10959-unknown-prop-parser-tier-clause.md @@ -0,0 +1,9 @@ +--- +'@object-ui/types': patch +--- + +Correct a false clause in the `header-bar` refusal messages and in nine `body?: never` docblocks (objectui#10959). + +The `HeaderBarSchema` refusals for `title`, `logo`, `nav`, `left`, `center`, `right`, `sticky`, `height` (objectui#10387) and `variant` (objectui#10286) said an authored value drew "no error, no warning". So did the `body?: never` docblocks on `box`, `span`, `container`, `flex`, `stack`, `grid`, `scroll-area`, `toggle` and `form` (objectui#8284), which ship in the emitted `.d.ts`. The render path is as described, but the parser tier is not: `validateTree` answers each of those keys with an `unknown-prop` warning. So the refusals now say "no render-time error or warning and no element; only the parser tier's `unknown-prop` warning noticed it" (`variant` keeps "no class" where the others say "no element"), and the docblocks say the `body` "rendered an EMPTY element with no render-time error or warning; only the parser tier's `unknown-prop` warning noticed it". + +Message, `.describe()` and documentation text only: every document is accepted or refused exactly as before, with the same issue code at the same path. Every other word of each message and docblock is unchanged. diff --git a/packages/types/src/__tests__/unknown-prop-parser-tier-clause-10959.test.ts b/packages/types/src/__tests__/unknown-prop-parser-tier-clause-10959.test.ts new file mode 100644 index 0000000000..b080e3cce7 --- /dev/null +++ b/packages/types/src/__tests__/unknown-prop-parser-tier-clause-10959.test.ts @@ -0,0 +1,191 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * objectui#10959 — what an unread `header-bar` key and an unread `body` drew, + * stated for BOTH tiers. The sibling of objectui#10928, with a different code. + * + * Two families of published text said an authored value drew "no error, no + * warning": + * + * - the `HeaderBarSchema` refusals of objectui#10387 (`title`, `logo`, `nav`, + * `left`, `center`, `right`, `sticky`, `height`) and objectui#10286 + * (`variant`), on the zod face (message and `.describe()` are one string); + * - the `body?: never` docblocks objectui#8284 put on the nine nodes whose + * renderer reads `children` (`box`, `span`, `container`, `flex`, `stack`, + * `grid`, `scroll-area` in `layout.ts`; `toggle`, `form` in `form.ts`), + * which ship in the emitted `.d.ts`. + * + * The render half is true: the value changed nothing on the page and logged + * nothing. The warning half was false. The parser tier's `validateTree` + * (`@object-ui/sdui-parser`) answers a key its registration does not declare + * with `unknown-prop`, and `header-bar` declares only `crumbs`, `search`, + * `actions` and `rightContent`. It answers `body` the same way on these nine, + * whatever its shape, because each of them declares the `children` input: + * `checkRetiredBodyDialect` gives `not-a-container` only where that input is + * missing, which is why objectui#10928's clause would be false here. So the + * clause now names `unknown-prop`: + * + * refusal strings: "no render-time error or warning and no element; only the + * parser tier's `unknown-prop` warning noticed it" + * (`variant` says "no class" where the others say + * "no element", as it did before) + * docblocks: "rendered an EMPTY element with no render-time error or + * warning; only the parser tier's `unknown-prop` warning + * noticed it" + * + * ## Why this file pins the prose, and where the parser half lives + * + * `@object-ui/types` declares no workspace dependency and its tests import + * none, so `@object-ui/sdui-parser` is not importable here without declaring + * it (the same reason as objectui#10928's pin). The `body` half of the parser + * behaviour is pinned in `packages/sdui-parser`: "the retired `body` spelling + * follows the SAME predicate" in `containment-declared-slot-9910.test.ts`, and + * "a `body` child list under a CONTAINER draws `unknown-prop` naming + * `children`" in `body-dialect-6771.test.ts`. The `header-bar` half is the + * generic undeclared-key branch of `validateTree`; no pin names `header-bar` + * there. The live-registry reading taken for this change (every key above, on + * `header-bar` and `ui:header-bar` and on the bare and `ui:` key of the nine, + * with a render leg and a `not-a-container` control) is on objectui#10959's + * pull request: a record of one run, not re-derived here. + * + * ## What is pinned, and what is deliberately not + * + * The clause as a substring. On the zod face: one `header-bar` key of each + * clause form, read through its refusal with its code and path, then every + * `header-bar` member whose refusal is one of these two retirements. On the TS + * face: the `body` docblock of `BoxSchema` and of `FormSchema`, then every + * docblock in `layout.ts` and `form.ts` that carries objectui#8284's sentence. + * The rest of each string is NOT pinned; `header-bar-unread-keys-10387`, + * `mirror-groups-cd-10286` and the objectui#8284 pins read it. + */ + +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { z } from 'zod'; +import { HeaderBarSchema } from '../zod/navigation.zod'; + +/** The clause the parser tier makes true — every site below ends on it. */ +const TAIL = "only the parser tier's `unknown-prop` warning noticed it"; + +/** The refusal strings' clause, in its two forms. */ +const ELEMENT_CLAUSE = `no render-time error or warning and no element; ${TAIL}`; +const CLASS_CLAUSE = `no render-time error or warning and no class; ${TAIL}`; + +/** The docblocks' clause. */ +const DOCBLOCK_CLAUSE = `rendered an EMPTY element with no render-time error or warning; ${TAIL}`; + +/** The clauses they replaced — false about the parser tier. */ +const RETIRED_STRING_CLAUSE = 'no error, no warning'; +const RETIRED_DOCBLOCK_CLAUSE = 'with no error and no warning'; + +/** objectui#10928's code, which would be false on every site here. */ +const WRONG_CODE = '`not-a-container`'; + +/** The sentence that marks a docblock as one of objectui#8284's `body` refusals. */ +const MARK_8284 = 'Before objectui#8284 tombstoned it here, `body` was inherited-and-optional'; + +/** + * The repo root, derived from THIS FILE's own location and never from the cwd + * (objectui#7799): bare `import.meta.url`, taken apart by hand (the spelling + * landed for objectui#7791, PR #7796). + */ +const SELF_DEPTH_BELOW_REPO_ROOT = 5; // packages / types / src / __tests__ / this file +const REPO_ROOT = decodeURIComponent(new URL(import.meta.url).pathname) + .split('/') + .slice(0, -SELF_DEPTH_BELOW_REPO_ROOT) + .join('/'); +const read = (relative: string): string => readFileSync(join(REPO_ROOT, relative), 'utf8'); + +const LAYOUT = 'packages/types/src/layout.ts'; +const FORM = 'packages/types/src/form.ts'; + +/** A docblock's text with its ` * ` continuations joined, as a reader sees it. */ +const joinDocblock = (doc: string): string => doc.replace(/\s*\n\s*\*\s?/g, ' '); + +/** The docblock on `member` inside `export interface name`, joined. */ +function memberDocblock(src: string, name: string, member: string): string { + const start = src.indexOf(`export interface ${name} `); + expect(start, `interface ${name} not found`).toBeGreaterThan(-1); + const end = src.indexOf('\n}', start); + const body = src.slice(start, end); + const at = body.search(new RegExp(`\\n\\s*${member}\\?:`)); + expect(at, `member ${name}.${member} not found`).toBeGreaterThan(-1); + const before = body.slice(0, at); + const open = before.lastIndexOf('/**'); + expect(open, `no docblock before ${name}.${member}`).toBeGreaterThan(-1); + return joinDocblock(before.slice(open)); +} + +describe('objectui#10959 — a header-bar refusal names the parser tier\'s `unknown-prop`', () => { + const NODE = { type: 'header-bar' as const, crumbs: [{ label: 'Home' }] }; + const ARMS = [ + ['title', 'My App', ELEMENT_CLAUSE], + ['variant', 'floating', CLASS_CLAUSE], + ] as const; + + it.each(ARMS)('`%s` is refused by name, and the refusal names `unknown-prop`', (key, value, clause) => { + const result = HeaderBarSchema.safeParse({ ...NODE, [key]: value }); + expect(result.success).toBe(false); + const issue = result.success ? undefined : result.error.issues.find((i) => i.path.length === 1 && i.path[0] === key); + expect(issue?.code).toBe('invalid_type'); + expect(issue?.path).toEqual([key]); + expect(issue?.message).toContain(clause); + expect(issue?.message).not.toContain(RETIRED_STRING_CLAUSE); + expect(issue?.message).not.toContain(WRONG_CODE); + }); +}); + +describe('objectui#10959 — every header-bar retirement carries the clause', () => { + const shape = HeaderBarSchema.shape as Record; + const members = Object.entries(shape) + .map(([key, schema]) => ({ key, text: schema.description ?? '' })) + .filter((m) => /^REFUSED \(objectui#(?:10387|10286), ADR-0049\)/.test(m.text)); + + it('the walk reaches both retirements (non-vacuity, by name rather than by count)', () => { + const keys = members.map((m) => m.key); + expect(keys).toContain('title'); + expect(keys).toContain('variant'); + }); + + it('none of them still says "no error, no warning", and each names `unknown-prop`', () => { + const stale = members.filter((m) => m.text.includes(RETIRED_STRING_CLAUSE) || !m.text.includes(TAIL)); + expect(stale.map((m) => m.key)).toEqual([]); + }); +}); + +describe('objectui#10959 — the `body?: never` docblocks name the parser tier\'s `unknown-prop`', () => { + const sources = [ + [LAYOUT, read(LAYOUT)], + [FORM, read(FORM)], + ] as const; + + it.each([ + [LAYOUT, 'BoxSchema'], + [FORM, 'FormSchema'], + ])('%s — `%s.body` carries the clause', (path, name) => { + const doc = memberDocblock(read(path), name, 'body'); + expect(doc).toContain(MARK_8284); + expect(doc).toContain(DOCBLOCK_CLAUSE); + expect(doc).not.toContain(RETIRED_DOCBLOCK_CLAUSE); + expect(doc).not.toContain(WRONG_CODE); + }); + + it('every objectui#8284 `body` docblock in both files carries it (non-vacuity: both files reached)', () => { + const stale: string[] = []; + for (const [path, src] of sources) { + const docs = (src.match(/\/\*\*[\s\S]*?\*\//g) ?? []).map(joinDocblock).filter((d) => d.includes(MARK_8284)); + expect(docs.length, `${path}: no objectui#8284 docblock found`).toBeGreaterThan(0); + docs.forEach((d, i) => { + if (d.includes(RETIRED_DOCBLOCK_CLAUSE) || !d.includes(DOCBLOCK_CLAUSE)) stale.push(`${path} #${i}`); + }); + } + expect(stale).toEqual([]); + }); +});