From 90f9e75ba8837028ca777b2170af01530ca36134 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 15:41:52 +0000 Subject: [PATCH 1/3] fix(types): the last four "no error, no warning" clauses name what the parser tier answers (objectui#10981) Closes the family of objectui#10928 and objectui#10959. Only the false clause changes; every accept or refuse verdict is byte-identical. - FormSchema.mode refusal (objectui#10286): names `unknown-prop`. - TimelineSchema.events docblock (objectui#6170): names `unknown-prop`. - BaseSchema.bind docblock (data-table): names the render-time console warning of objectui#6575 and the parser tier's `unknown-prop`. - exportOptions note (objectui#7762): names `type-mismatch`, the code the object-typed registration input answers an array with. New pin: silence-clause-parser-tier-closure-10981.test.ts, one string per site plus a closure walk over packages/types/src. Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude --- ...0981-silence-clause-parser-tier-closure.md | 12 + ...e-clause-parser-tier-closure-10981.test.ts | 224 ++++++++++++++++++ packages/types/src/base.ts | 5 +- packages/types/src/data-display.ts | 3 +- packages/types/src/zod/form.zod.ts | 2 +- packages/types/src/zod/objectql.zod.ts | 5 +- 6 files changed, 246 insertions(+), 5 deletions(-) create mode 100644 .changeset/10981-silence-clause-parser-tier-closure.md create mode 100644 packages/types/src/__tests__/silence-clause-parser-tier-closure-10981.test.ts diff --git a/.changeset/10981-silence-clause-parser-tier-closure.md b/.changeset/10981-silence-clause-parser-tier-closure.md new file mode 100644 index 0000000000..0e4a036c76 --- /dev/null +++ b/.changeset/10981-silence-clause-parser-tier-closure.md @@ -0,0 +1,12 @@ +--- +'@object-ui/types': patch +--- + +Correct the last four published "no error, no warning" clauses that the parser tier contradicts (objectui#10981, closing the family of objectui#10928 and objectui#10959). + +- The `FormSchema.mode` refusal (objectui#10286) said every spelling "rendered the same form — no error, no warning". `validateTree` answers `mode` on a `form` node with an `unknown-prop` warning, so it now says "no render-time error or warning; only the parser tier's `unknown-prop` warning noticed it". +- The `TimelineSchema.events` docblock (objectui#6170), which ships in the emitted `.d.ts`, said a timeline authored with `events` "drew an EMPTY rail, with no error and no warning". It gets the same clause: the parser tier answers `events` with `unknown-prop`. +- The `BaseSchema.bind` docblock said a `bind` on `data-table` renders its header over an empty body "with no error and no warning". Neither half was true: `data-table` logs a console warning for an authored `bind` (objectui#6575), and the parser tier answers it with `unknown-prop`. It now says "with no render-time error; nothing on the page says why, but a render-time console warning (`[ObjectUI] DataTable bind:`, objectui#6575) and the parser tier's `unknown-prop` warning both name it". +- The note on the `object-grid` `exportOptions` refusal (objectui#7762) said a bare format array lost to the default with "no error, no warning, no console line". The registration declares `exportOptions` as an object, so the parser tier answers an array with `type-mismatch`, not `unknown-prop`, and the note now says "no render-time error, warning or console line (only the parser tier's `type-mismatch` 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__/silence-clause-parser-tier-closure-10981.test.ts b/packages/types/src/__tests__/silence-clause-parser-tier-closure-10981.test.ts new file mode 100644 index 0000000000..f4012e9f5e --- /dev/null +++ b/packages/types/src/__tests__/silence-clause-parser-tier-closure-10981.test.ts @@ -0,0 +1,224 @@ +/** + * 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#10981 — the family closure for the published "no error, no warning" + * claims, after objectui#10928 (`not-a-container`) and objectui#10959 + * (`unknown-prop`). + * + * Four more sites in this package said an authored value drew no error and no + * warning. The parser tier's `validateTree` (`@object-ui/sdui-parser`) answers + * each of them, with the code the registration's declared inputs decide: + * + * - `FormSchema.mode` (`zod/form.zod.ts`, objectui#10286, zod face): + * `form` declares no `mode` input, so every spelling draws `unknown-prop`. + * - `TimelineSchema.events` (`data-display.ts`, objectui#6170, TS face): + * neither the presentational `TimelineRenderer` registration this type + * describes nor the view registration the bare key resolves to declares + * `events`, so it draws `unknown-prop`. + * - `BaseSchema.bind` on `data-table` (`base.ts`, TS face): `data-table` + * declares no `bind` input, so it draws `unknown-prop`. This one was not + * silent at render either: `data-table` has logged a console warning for + * an authored `bind` since objectui#6575, so the clause names both. + * - The bare `exportOptions` array on `object-grid` (`zod/objectql.zod.ts`, + * objectui#7762, the source note above the refusal string): the + * registration declares `exportOptions` as an object, so an array draws + * `type-mismatch`, not `unknown-prop`. + * + * ## 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 (the same reason as + * the objectui#10928 and objectui#10959 pins). The four readings behind these + * clauses — every key spelling and value shape each sentence describes, over + * the live registry, with a render leg — are on objectui#10981's pull request: + * a record of one run, not re-derived here. The `data-table` render warning is + * pinned in `packages/components`, by `skill-guide-data-table-binding.test.tsx`. + * + * ## What is pinned, and what is deliberately not + * + * One string per site, as a substring: on the zod face read through the + * refusal with its code and path first, on the TS face read off the member's + * docblock. Then the closure walk: over every non-test `.ts` file of this + * package's `src`, no joined text still pairs "no error" with "no warning", + * and every "no … error or warning" phrasing is followed by the parser tier's + * clause. The rest of each string is NOT pinned; the sites' own pins read it. + */ + +import { describe, it, expect } from 'vitest'; +import { readFileSync, readdirSync, statSync } from 'node:fs'; +import { join, relative } from 'node:path'; +import { FormSchema } from '../zod/form.zod'; +import { ObjectGridSchema } from '../zod/objectql.zod'; + +/** The clause each site now carries. */ +const UNKNOWN_PROP_TAIL = "only the parser tier's `unknown-prop` warning noticed it"; +const FORM_CLAUSE = `rendered the same form — no render-time error or warning; ${UNKNOWN_PROP_TAIL}`; +const TIMELINE_CLAUSE = `drew an EMPTY rail, with no render-time error or warning; ${UNKNOWN_PROP_TAIL}`; +const BIND_CLAUSE = + 'with no render-time error; nothing on the page says why, but a render-time console warning ' + + "(`[ObjectUI] DataTable bind:`, objectui#6575) and the parser tier's `unknown-prop` warning both name it"; +const EXPORT_CLAUSE = + "no render-time error, warning or console line (only the parser tier's `type-mismatch` warning noticed it)"; + +/** The phrasings they replaced — false about the parser tier. */ +const RETIRED = ['no error, no warning', 'no error and no warning']; + +/** + * 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 = (rel: string): string => readFileSync(join(REPO_ROOT, rel), 'utf8'); + +const SRC = 'packages/types/src'; + +/** + * Text as a reader sees it: JSDoc and line-comment continuations joined, + * `'…' + '…'` concatenations joined, escaped quotes unescaped. + */ +const joinText = (src: string): string => + src + .replace(/(['"`])\s*\+\s*\n?\s*(['"`])/g, '') + .replace(/\n\s*\*\/?\s?/g, ' ') + .replace(/\n\s*\/\/\s?/g, ' ') + .replace(/\\(['"`])/g, '$1') + .replace(/[ \t]+/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 joinText(before.slice(open)); +} + +/** The issue a schema raises at exactly `[key]`, if any. */ +function issueAt(result: { success: boolean; error?: { issues: Array<{ code: string; path: PropertyKey[]; message: string }> } }, key: string) { + return result.success ? undefined : result.error?.issues.find((i) => i.path.length === 1 && i.path[0] === key); +} + +describe('objectui#10981 — the `FormSchema.mode` refusal names the parser tier\'s `unknown-prop`', () => { + const NODE = { type: 'form' as const, fields: [{ name: 'title', label: 'Title', type: 'text' }] }; + + it('`mode` is refused by name, and the refusal names `unknown-prop`', () => { + const issue = issueAt(FormSchema.safeParse({ ...NODE, mode: 'edit' }), 'mode'); + expect(issue?.code).toBe('invalid_type'); + expect(issue?.path).toEqual(['mode']); + expect(issue?.message).toContain(FORM_CLAUSE); + for (const old of RETIRED) expect(issue?.message).not.toContain(old); + }); + + it('every spelling the objectui#10286 pin refuses carries it, and so does the `.describe()` text', () => { + const stale = ['edit', 'read', 'disabled', 'create', 'view'].filter( + (mode) => !issueAt(FormSchema.safeParse({ ...NODE, mode }), 'mode')?.message.includes(FORM_CLAUSE), + ); + expect(stale).toEqual([]); + expect(FormSchema.shape.mode.description).toContain(FORM_CLAUSE); + }); +}); + +describe('objectui#10981 — the `exportOptions` note names the parser tier\'s `type-mismatch`', () => { + const NODE = { type: 'object-grid' as const, objectName: 'accounts' }; + + it('a bare array is refused at `exportOptions` with the one string the note sits above', () => { + const issue = issueAt(ObjectGridSchema.safeParse({ ...NODE, exportOptions: ['csv', 'xlsx'] }), 'exportOptions'); + expect(issue?.code).toBe('invalid_type'); + expect(issue?.path).toEqual(['exportOptions']); + expect(issue?.message).toBe(ObjectGridSchema.shape.exportOptions.description); + }); + + it('the note above `OBJECT_GRID_EXPORT_OPTIONS_GUIDANCE` carries the clause, and names no `unknown-prop`', () => { + const src = read(`${SRC}/zod/objectql.zod.ts`); + const decl = src.indexOf('\nconst OBJECT_GRID_EXPORT_OPTIONS_GUIDANCE ='); + expect(decl, 'the guidance constant moved').toBeGreaterThan(-1); + const open = src.lastIndexOf('/**', decl); + const note = joinText(src.slice(open, decl)); + expect(note).toContain('objectui#7762'); + expect(note).toContain(EXPORT_CLAUSE); + for (const old of RETIRED) expect(note).not.toContain(old); + expect(note).not.toContain('`unknown-prop`'); + }); +}); + +describe('objectui#10981 — the TS-face docblocks name what the parser tier answers', () => { + it('`TimelineSchema.events` (objectui#6170) names `unknown-prop`', () => { + const doc = memberDocblock(read(`${SRC}/data-display.ts`), 'TimelineSchema', 'events'); + expect(doc).toContain('RETIRED (objectui#6170'); + expect(doc).toContain(TIMELINE_CLAUSE); + for (const old of RETIRED) expect(doc).not.toContain(old); + }); + + it('`BaseSchema.bind` names the `data-table` console warning and `unknown-prop`', () => { + const doc = memberDocblock(read(`${SRC}/base.ts`), 'BaseSchema', 'bind'); + expect(doc).toContain('`data-table` does NOT'); + expect(doc).toContain(BIND_CLAUSE); + for (const old of RETIRED) expect(doc).not.toContain(old); + }); +}); + +describe('objectui#10981 — the closure walk over this package\'s `src`', () => { + /** Every non-test `.ts` file under `src`, relative to the repo root. */ + const files: string[] = []; + const walk = (dir: string): void => { + for (const name of readdirSync(dir)) { + const abs = join(dir, name); + if (statSync(abs).isDirectory()) { + if (name !== '__tests__') walk(abs); + } else if (name.endsWith('.ts') && !name.endsWith('.test.ts')) { + files.push(relative(REPO_ROOT, abs)); + } + } + }; + walk(join(REPO_ROOT, SRC)); + const texts = files.map((f) => [f, joinText(read(f))] as const); + + /** "no error" and "no warning" paired, in either order — the retired shape. */ + const PAIRED = [ + /\bno (?:[\w-]+ )?errors?\b[^.;]{0,60}?\bno (?:[\w-]+ )?warnings?\b/gi, + /\bno (?:[\w-]+ )?warnings?\b[^.;]{0,60}?\bno (?:[\w-]+ )?errors?\b/gi, + /\bwithout (?:an? |any )?(?:[\w-]+ )?errors? (?:or|nor|and) (?:an? |any )?(?:[\w-]+ )?warnings?\b/gi, + /\bneither (?:an? )?errors? nor (?:an? )?warnings?\b/gi, + ]; + /** "no … error or warning" — the family's corrected shape, which must name the parser tier. */ + const COMBINED = /\bno (?:[\w-]+ )?errors? (?:or|nor|and) (?:[\w-]+ )?warnings?\b/gi; + const PARSER_TIER = "the parser tier's"; + + it('the walk reaches the four sites and both earlier waves (non-vacuity, by name rather than by count)', () => { + const names = files.map((f) => f.slice(SRC.length + 1)); + for (const f of ['base.ts', 'data-display.ts', 'zod/form.zod.ts', 'zod/objectql.zod.ts']) expect(names).toContain(f); + const text = (f: string) => texts.find(([p]) => p === `${SRC}/${f}`)?.[1] ?? ''; + expect(text('zod/navigation.zod.ts')).toContain(UNKNOWN_PROP_TAIL); + expect(text('ai.ts')).toContain("only the parser tier's `not-a-container` warning (objectui#9910) noticed it"); + }); + + it('no file still pairs "no error" with "no warning"', () => { + const hits = texts.flatMap(([f, t]) => PAIRED.flatMap((re) => [...t.matchAll(re)].map((m) => `${f}: «${m[0]}»`))); + expect(hits).toEqual([]); + }); + + it('every "no … error or warning" phrasing names the parser tier right after it', () => { + const bare = texts.flatMap(([f, t]) => + [...t.matchAll(COMBINED)] + .filter((m) => !t.slice(m.index, (m.index ?? 0) + m[0].length + 80).includes(PARSER_TIER)) + .map((m) => `${f}: «${t.slice(m.index, (m.index ?? 0) + m[0].length + 80)}»`), + ); + expect(bare).toEqual([]); + }); +}); diff --git a/packages/types/src/base.ts b/packages/types/src/base.ts index b1abfe909a..a12a28cf1c 100644 --- a/packages/types/src/base.ts +++ b/packages/types/src/base.ts @@ -277,7 +277,10 @@ export interface BaseSchema { * widgets in `plugin-charts` / `plugin-dashboard` (×2) / `plugin-grid` / * `plugin-kanban` / `plugin-list` / `plugin-timeline`. ⚠️ `data-table` does * NOT: a `bind` on it is ignored and the table renders its header over an - * empty body, with no error and no warning (`protocol.md`, and pinned in + * empty body, with no render-time error; nothing on the page says why, but a + * render-time console warning (`[ObjectUI] DataTable bind:`, objectui#6575) + * and the parser tier's `unknown-prop` warning both name it + * (`protocol.md`, and pinned in * `components/src/__tests__/skill-guide-data-table-binding.test.tsx`). * Declaring the key here does not change that, and does not bless it — the * key was already accepted on every node before this declaration existed. diff --git a/packages/types/src/data-display.ts b/packages/types/src/data-display.ts index 4dfce4ac64..33e3a47704 100644 --- a/packages/types/src/data-display.ts +++ b/packages/types/src/data-display.ts @@ -3028,7 +3028,8 @@ export interface TimelineSchema extends BaseSchema { * (`{ time, title, … }`). * * No renderer ever read this key: a timeline authored with `events` drew an - * EMPTY rail, with no error and no warning. objectui#6170's maintainer ruling + * EMPTY rail, with no render-time error or warning; only the parser tier's + * `unknown-prop` warning noticed it. objectui#6170's maintainer ruling * (2026-08-25, 「同意」) sent it, {@link TimelineSchema.orientation} and * {@link TimelineSchema.position} down the ADR-0049 enforce-or-remove route, * and the producer census recorded on objectui#6170 (a dated reading, not diff --git a/packages/types/src/zod/form.zod.ts b/packages/types/src/zod/form.zod.ts index eba1c495f6..3a595a7472 100644 --- a/packages/types/src/zod/form.zod.ts +++ b/packages/types/src/zod/form.zod.ts @@ -998,7 +998,7 @@ export const FormSchema = BaseSchema.extend({ mode: retirementTombstone( 'REFUSED (objectui#10286, ADR-0049; objectui#7759 ruling D1-(ii)) — the `form` node reads no `mode`: ' + 'the key is not in `@objectstack/spec`, the `form` renderer never reads it, and every spelling ' - + 'rendered the same form — no error, no warning. The create / edit / view mode belongs to the ' + + 'rendered the same form — no render-time error or warning; only the parser tier\'s `unknown-prop` warning noticed it. The create / edit / view mode belongs to the ' + '`object-form` node (`ObjectFormSchema.mode`): author `{ "type": "object-form", "objectName": …, ' + '"mode": "edit", "recordId": … }` for it. To make this form non-editable, set `disabled`.', ), diff --git a/packages/types/src/zod/objectql.zod.ts b/packages/types/src/zod/objectql.zod.ts index e2c5617349..4e75a80cfb 100644 --- a/packages/types/src/zod/objectql.zod.ts +++ b/packages/types/src/zod/objectql.zod.ts @@ -230,8 +230,9 @@ const SPEC_EXPORT_OPTIONS_OBJECT_SHAPE: SpecExportOptionsShape = ((): SpecExport * The refusal it carries is objectui#7762's ruling. `ObjectGrid.tsx` reads * `schema.exportOptions?.formats` and nothing else, so a bare format array authored on * an `object-grid` node used to validate green through `BaseSchema`'s `.passthrough()` - * and then lose SILENTLY to the `['csv', 'json']` default — no error, no warning, no - * console line, with the export button still shown. Refusing it by name is that silent + * and then lose SILENTLY to the `['csv', 'json']` default — no render-time error, warning + * or console line (only the parser tier's `type-mismatch` warning noticed it), with the + * export button still shown. Refusing it by name is that silent * no-op made loud; nothing that renders today stops rendering. */ const OBJECT_GRID_EXPORT_OPTIONS_GUIDANCE = From 2063913aa2301ca2e4f13a5c4d2f8e4840c1faea Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 15:42:01 +0000 Subject: [PATCH 2/3] test(components): hold the header-bar refusal's parser-tier half on the live registry (objectui#10981) The objectui#10959 clause says only the parser tier's `unknown-prop` warning noticed a refused `header-bar` key. The types pin can hold the prose only; this holds the behaviour where the registry lives, on `header-bar` and `ui:header-bar`, for every refused key and value shape. Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude --- ...ar-unknown-prop-parser-tier-10981.test.tsx | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 packages/components/src/renderers/__tests__/header-bar-unknown-prop-parser-tier-10981.test.tsx diff --git a/packages/components/src/renderers/__tests__/header-bar-unknown-prop-parser-tier-10981.test.tsx b/packages/components/src/renderers/__tests__/header-bar-unknown-prop-parser-tier-10981.test.tsx new file mode 100644 index 0000000000..b366ad6be0 --- /dev/null +++ b/packages/components/src/renderers/__tests__/header-bar-unknown-prop-parser-tier-10981.test.tsx @@ -0,0 +1,99 @@ +/** + * 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. + */ + +/** + * The parser-tier half of the `header-bar` refusal clause, held on the live + * registry (objectui#10981, the open question from objectui#10959). + * + * objectui#10959 corrected the `HeaderBarSchema` refusals of objectui#10387 + * (`title`, `logo`, `nav`, `left`, `center`, `right`, `sticky`, `height`) and + * objectui#10286 (`variant`) to say that nothing warned at render, and that + * "only the parser tier's `unknown-prop` warning noticed it". The pin for that + * wording lives in `@object-ui/types`, which cannot import the parser, so it + * holds the prose only. The parser-tier behaviour was measured once, on the + * objectui#10959 pull request, and pinned nowhere: `validateTree`'s generic + * undeclared-key branch answers it, and no parser pin names `header-bar`. + * + * This file holds that half where the registry lives. The manifest is built + * the way `container-declaration-ratchet.test.tsx` builds it — every KNOWN + * registry key, through `manifestFromConfigs` — so a registration that starts + * declaring one of these keys as an input turns this red, and so does a change + * to the parser that stops answering an undeclared key with `unknown-prop`. + * Either one makes the published clause false, and this is where it shows. + * + * Both spellings are held: the bare `header-bar` and the namespaced + * `ui:header-bar`. Every value shape the objectui#10387 / objectui#10286 pins + * refuse is held too, because the answer depends only on the key being + * undeclared, and a pin that tried one shape could not show that. + */ + +import { describe, it, expect } from 'vitest'; +import { ComponentRegistry } from '@object-ui/core'; +import { manifestFromConfigs, validateTree } from '@object-ui/sdui-parser'; +import type { Diagnostic, SchemaElement } from '@object-ui/sdui-parser'; + +// Module scope, not a hook: this import IS the registration (AGENTS.md +// §测试纪律 — an unbounded module load must not be billed to a bounded window). +import '../index'; + +/** Every live registry key, as `container-declaration-ratchet.test.tsx` builds it. */ +const diagnose = (schema: unknown): Diagnostic[] => { + const configs = ComponentRegistry.getKnownTypes().map((t) => { + const meta = ComponentRegistry.getMeta(t); + return { type: t, namespace: meta?.namespace, isContainer: meta?.isContainer, inputs: meta?.inputs }; + }); + const manifest = manifestFromConfigs(configs as unknown as Parameters[0]); + return validateTree(schema as SchemaElement, manifest).diagnostics; +}; + +const TEXT = { type: 'text', content: 'x' }; + +/** The refused keys and the values their `@object-ui/types` pins refuse. */ +const RETIRED: Record = { + title: ['My App'], + logo: ['/logo.svg', TEXT, [TEXT]], + nav: [[{ label: 'Docs', href: '/docs' }], []], + left: [TEXT, [TEXT], 'plain text'], + center: [TEXT, [TEXT], 'plain text'], + right: [TEXT, [TEXT]], + sticky: [true, false], + height: ['64px', 64], + variant: ['default', 'bordered', 'floating', 'transparent'], +}; + +const TYPES = ['header-bar', 'ui:header-bar'] as const; +const CRUMBS = [{ label: 'Home', href: '/' }]; + +describe('header-bar — the refused keys draw exactly the parser tier\'s `unknown-prop` (objectui#10981)', () => { + it.each(TYPES)('`%s` is a live registration, and its control draws nothing', (type) => { + expect(ComponentRegistry.getKnownTypes()).toContain(type); + expect(diagnose({ type, crumbs: CRUMBS })).toEqual([]); + }); + + it.each(TYPES)('`%s` — every refused key, in every refused shape, draws one `unknown-prop` naming it', (type) => { + const wrong: string[] = []; + for (const [key, values] of Object.entries(RETIRED)) { + for (const value of values) { + const found = diagnose({ type, crumbs: CRUMBS, [key]: value }); + const one = found.length === 1 ? found[0] : undefined; + if (one?.code !== 'unknown-prop' || !one.message.includes(`"${key}"`)) { + wrong.push(`${key}=${JSON.stringify(value)} → ${JSON.stringify(found.map((d) => d.code))}`); + } + } + } + expect(wrong).toEqual([]); + }); + + it.each(TYPES)('`%s` — lit control: the keys it does declare draw no `unknown-prop`', (type) => { + const declared = (ComponentRegistry.getMeta(type)?.inputs ?? []).map((input) => input.name); + expect(declared).toContain('crumbs'); + for (const key of Object.keys(RETIRED)) expect(declared).not.toContain(key); + const found = diagnose({ type, crumbs: CRUMBS, rightContent: TEXT }); + expect(found.filter((d) => d.code === 'unknown-prop')).toEqual([]); + }); +}); From e3a23eba9ec663c807cadeec0f63ad8d86c5f815 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 20:19:20 +0000 Subject: [PATCH 3/3] test(types): the closure walk looks for the parser tier to the end of the next sentence (objectui#10981) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The walk required "the parser tier's" within 80 characters after every "no … error or warning" phrasing. objectui#9256's `metric-card` refusal (PR #11020) names the parser tier correctly, but in the sentence after, to record why it stays silent in a widget slot. So the merge queue went red on a true text. The bound is now the end of the sentence after the phrasing. `MAX_WINDOW` only guards a text with no sentence end. Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude --- ...e-clause-parser-tier-closure-10981.test.ts | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/packages/types/src/__tests__/silence-clause-parser-tier-closure-10981.test.ts b/packages/types/src/__tests__/silence-clause-parser-tier-closure-10981.test.ts index f4012e9f5e..d146db538e 100644 --- a/packages/types/src/__tests__/silence-clause-parser-tier-closure-10981.test.ts +++ b/packages/types/src/__tests__/silence-clause-parser-tier-closure-10981.test.ts @@ -46,8 +46,9 @@ * refusal with its code and path first, on the TS face read off the member's * docblock. Then the closure walk: over every non-test `.ts` file of this * package's `src`, no joined text still pairs "no error" with "no warning", - * and every "no … error or warning" phrasing is followed by the parser tier's - * clause. The rest of each string is NOT pinned; the sites' own pins read it. + * and every "no … error or warning" phrasing names the parser tier in its own + * sentence or the next one. The rest of each string is NOT pinned; the sites' + * own pins read it. */ import { describe, it, expect } from 'vitest'; @@ -200,6 +201,38 @@ describe('objectui#10981 — the closure walk over this package\'s `src`', () => const COMBINED = /\bno (?:[\w-]+ )?errors? (?:or|nor|and) (?:[\w-]+ )?warnings?\b/gi; const PARSER_TIER = "the parser tier's"; + /** + * Where the parser tier has to be named: anywhere from the phrasing to the + * end of the sentence AFTER the one it sits in. + * + * The phrasing speaks for the render tier only, so the text must go on to say + * what the parser tier did. The family writes that in one of two places. Most + * sites keep it in the same sentence ("…; only the parser tier's + * `unknown-prop` warning noticed it"). A site where the parser tier is + * genuinely silent gives it the next sentence, because it has a reason to + * record: objectui#9256's `metric-card` refusal says that "in a widget slot + * nothing else noticed it either: the parser tier's `not-a-container` warning + * (objectui#9910) walks `children`, never `widgets`". A fixed character count + * after the match is not that rule. It was 80 here, and it failed the + * `metric-card` text, which names the parser tier correctly but a little + * further on. Any count is too short for some honest sentence and long + * enough to reach an unrelated one somewhere else. + * + * A sentence ends at `.`, `!` or `?` followed by whitespace, the end of the + * text, or a closing quote (a zod string ends in `.'`). So the dots in + * `zod/form.zod.ts` or `.describe()` end nothing. `MAX_WINDOW` does not set + * the rule. It only stops a text with no sentence end from reaching the rest + * of its file. + */ + const MAX_WINDOW = 600; + const windowAfter = (t: string, from: number): string => { + const end = /[.!?](?=['"`]?(?:\s|$))/g; + end.lastIndex = from; + const own = end.exec(t); + const next = own ? end.exec(t) : null; + return t.slice(from, Math.min(next ? next.index + 1 : t.length, from + MAX_WINDOW)); + }; + it('the walk reaches the four sites and both earlier waves (non-vacuity, by name rather than by count)', () => { const names = files.map((f) => f.slice(SRC.length + 1)); for (const f of ['base.ts', 'data-display.ts', 'zod/form.zod.ts', 'zod/objectql.zod.ts']) expect(names).toContain(f); @@ -213,11 +246,12 @@ describe('objectui#10981 — the closure walk over this package\'s `src`', () => expect(hits).toEqual([]); }); - it('every "no … error or warning" phrasing names the parser tier right after it', () => { + it('every "no … error or warning" phrasing names the parser tier in the same or the next sentence', () => { const bare = texts.flatMap(([f, t]) => [...t.matchAll(COMBINED)] - .filter((m) => !t.slice(m.index, (m.index ?? 0) + m[0].length + 80).includes(PARSER_TIER)) - .map((m) => `${f}: «${t.slice(m.index, (m.index ?? 0) + m[0].length + 80)}»`), + .map((m) => windowAfter(t, m.index ?? 0)) + .filter((w) => !w.includes(PARSER_TIER)) + .map((w) => `${f}: «${w}»`), ); expect(bare).toEqual([]); });