Skip to content

Commit 797a30f

Browse files
fix(types): the last four "no error, no warning" clauses name what the parser tier answers (objectui#10981) (#11006)
Fixes #10981 Clause-②: no — message, `.describe()` and docblock text only; every accept or refuse verdict stays byte-identical. Implemented by the `os-dev` agent dispatched from the `domain:spec @ objectui` seat, session `https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm` (`mode:subagent`). Base `4b742f41d`. PR #10986 landed on `zod/objectql.zod.ts` meanwhile, so `main` `665025908` was merged in before the final push. Head `ebd156ef5`. ## What changed Only the false clause changes, on four sites. Each clause names the code the parser tier answers for that key and value shape, as measured on the live registry (next section). | site | face | before | after | |---|---|---|---| | `zod/form.zod.ts`, the `FormSchema.mode` refusal (objectui#10286) | zod message and `.describe()`, one string through `retirementTombstone` | "rendered the same form — no error, no warning" | "rendered the same form — no render-time error or warning; only the parser tier's `unknown-prop` warning noticed it" | | `data-display.ts`, the `TimelineSchema.events` docblock (objectui#6170) | TS face, emitted `.d.ts` | "drew an EMPTY rail, with no error and no warning" | "drew an EMPTY rail, with no render-time error or warning; only the parser tier's `unknown-prop` warning noticed it" | | `base.ts`, the `data-table` sentence in the `BaseSchema.bind` docblock | TS face, emitted `.d.ts` | "renders its header over an empty body, with no error and no warning" | "renders its header over an 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" | | `zod/objectql.zod.ts`, the note on `OBJECT_GRID_EXPORT_OPTIONS_GUIDANCE` (objectui#7762) | source note on a non-exported constant; it ships in the emitted JS, in no `.d.ts` | "no error, no warning, no console line" | "no render-time error, warning or console line (only the parser tier's `type-mismatch` warning noticed it)" | - **Why `base.ts` has its own wording.** That site was not silent at render either. `data-table` has logged `[ObjectUI] DataTable bind:` for an authored `bind` since objectui#6575 (`aff3d7aef`, 2026-08-28). The docblock was written a day later (`99a3c2db6`, 2026-08-29). `merge-base --is-ancestor aff3d7a 99a3c2d` answers 0, with the control leg `94d4876df` also answering 0. So "only the parser tier noticed it" would be false there too. - **Why `exportOptions` names `type-mismatch`.** The `object-grid` registration declares `exportOptions` as `type: 'object'`. So `checkType` answers an array, not the undeclared-key branch. - **New pin:** `packages/types/src/__tests__/silence-clause-parser-tier-closure-10981.test.ts`. - **Optional item, taken:** `packages/components/src/renderers/__tests__/header-bar-unknown-prop-parser-tier-10981.test.tsx`, a new file beside `container-declaration-ratchet`. - **Changeset:** `.changeset/10981-silence-clause-parser-tier-closure.md`, `@object-ui/types: patch`. ## The parser tier, measured before each clause was written **Instrument.** The same as PR #10980: a one-shot probe, deleted and never committed, under `packages/components/src/renderers/__tests__/`. - It imported the components renderers, `fields` and 18 of the 19 `plugin-*` packages (all but `plugin-designer`). `plugin-tree` has no vitest alias, so it came in by relative source path. - The manifest was built from every KNOWN registry key through `manifestFromConfigs`, the way `container-declaration-ratchet.test.tsx` builds it. - Each leg ran `validateTree` on a control and on the control plus the one key, and reports only the diagnostics that key added. - Each leg also rendered through the real `SchemaRenderer`, inside `PredicateScopeProvider` and `SchemaRendererProvider` with a stub data source, all inside `act`. `console.warn` and `console.error` were spied, and the HTML was compared with the control's. | site | keys | value shapes | the parser tier adds | render leg | |---|---|---|---|---| | `FormSchema.mode` | `form`, `ui:form`. Bare `form` resolves to `ui:form`; `view:form` is `skipFallback` | `edit`, `read`, `disabled`, `create`, `view` (the objectui#10286 pin's set) | exactly one `unknown-prop` on `mode`, 10 of 10 legs | 0 warnings, 0 errors on every variant. HTML equals the control's except for one `mode` attribute on the form element | | `TimelineSchema.events` | `timeline` (resolves to `view:timeline`), `view:timeline`, `plugin-timeline:timeline` (the `TimelineRenderer` registration `TimelineSchema` describes) | 2 events; `[]`; `items` plus `events` | exactly one `unknown-prop` on `events`, 7 of 7 | byte-identical HTML, 0 warnings, 0 errors | | `BaseSchema.bind` on `data-table` | `data-table`, `ui:data-table` | `bind: 'users'` and `bind: 'app.settings.users'`, each with `data` absent and with `data: []` | exactly one `unknown-prop` on `bind`, 8 of 8 | byte-identical HTML (the "No results" body in both), 0 errors, and exactly 1 console warning on every variant: `[ObjectUI] DataTable bind: … is ignored` | | `exportOptions` | `object-grid`, `plugin-grid:object-grid` | `['csv', 'xlsx']`, `['csv']`, `[]` | exactly one `type-mismatch` ("expected an object"), 6 of 6 | the control is `{ formats: ['csv', 'json'] }`, the default an array loses to. Byte-identical HTML, the export control and the rows present in both, 0 warnings, 0 errors | **Controls.** - `form` with an invented key draws `unknown-prop`. `view:form` with `mode: 'edit'` draws nothing, because that registration declares `mode`. - On `object-grid`: an `exportOptions` object draws nothing, a string draws `type-mismatch`, and an invented key draws `unknown-prop`. So the probe tells the two codes apart on one node. - `accordion` with a `body` list draws `not-a-container`, the third code. - With `data` absent, the `data-table` control already carries `missing-required-prop` on `data`; the table above counts only what `bind` adds. - The first control render in the file logged one `react-i18next` initialisation warning. No variant did. **The same answer before each retirement (source reading, not executed).** The `mode`, `events` and `exportOptions` sentences are past tense. - `FormSchema.mode`, at `a05c35063^`, the parent of the objectui#10286 commit: the `ui:form` registration declared the same 13 inputs as today, none named `mode`. `plugin-form`'s `form` was `namespace: 'view', skipFallback: true`. `validate.ts` had the same base-prop set, without `mode`, and one `unknown-prop` branch. - `TimelineSchema.events`, at `195052fff^`, the parent of the objectui#6170 tombstone commit: neither `plugin-timeline:timeline` nor `view:timeline` declared `events`. A `git log -G` for an `events` input name over `packages` finds no registration that ever declared one, back to the shallow boundary (2026-07-20). - `exportOptions`, at `f08d1a86a^`, the parent of the objectui#7762 refusal: `GRID_QUERY_INPUTS` declared `exportOptions` as `type: 'object'`. It had done so since `375efb402` (2026-08-17), and objectui#7762 was filed on 2026-09-05. `checkType` gave a single non-enum arm `type-mismatch`, as today. - `BaseSchema.bind` is present tense, measured today. - The history reads ran on a deepened checkout (`git fetch --deepen=4000`, shallow boundary 2026-07-20). ## Census over joined text (the closure nail) **Instrument.** A one-shot script. It enumerates the non-test `.ts` files of `packages/types/src` from the ref with `git ls-tree` and reads each one from the ref. It joins JSDoc and line-comment continuations and `'…' + '…'` concatenations, and unescapes quotes. Then it counts: - A: "no (adjective) error" and "no (adjective) warning" within 60 characters, with no sentence break; - B: the same, in reverse order; - C: "no (adjective) error or / nor / and (adjective) warning", the family's corrected shape; - D: "without (an) error or / and warning"; - E: "neither error nor warning"; - F: "silent…" within 80 characters of a negated warning, in either order; - G, the wide net: any negated warning ("no warning", "without a warning", "never warns", …); - H, the wider net over other nouns: "no diagnostic", "no console line", "without complaint", "not a warning". | reading | base `4b742f41d` | main `665025908` | head `ebd156ef5` | |---|---|---|---| | files enumerated | 67 | 67 | 67 | | A | 4 | 4 | 0 | | B, D, E | 0 | 0 | 0 | | C, and how many are followed by "the parser tier's" | 302, 300 | 302, 300 | 302, 302 | | F | 1 | 1 | 0 | | G | 6 | 6 | 2 | | H | 25 | 25 | 24 | | control: the objectui#10928 clause | 282 | 282 | 282 | | control: the objectui#10959 tail | 18 | 18 | 20 | - **Positive control.** C fires on every one of the 300 known objectui#10928 / objectui#10959 control strings. A fires on exactly the four sites the card names, which were found independently. - **Every non-control hit at base, read.** - A is the four sites and nothing else. The two C hits without the parser tier's clause are two of the four (`base.ts`, `data-display.ts`). F is the fourth site again. - G adds two hits that say "no warning window" (`objectql.ts`, `zod/objectql.zod.ts`, objectui#6475). That is a deprecation window, not a silence claim, so they stay. - H is outside the card's phrasing family. It is listed under Acceptance notes and not changed. ## Every other byte, and only comment and string lines - Substitute the old clause back for the new one on joined text: each head file equals its base file, 4 of 4, against `4b742f41d` and again against `665025908`. - The first four ablation legs put the old clause back with the anchor tool. Each mutated blob equals the base blob byte for byte: `eba1c495f`, `4dfce4ac6`, `b1abfe909` and `e2c561734` are the base blobs of the four files. - `git diff -U0` over the four files: every changed line is a ` * ` JSDoc line or a `+ '…'` string-literal line. - The objectui#10286, objectui#6170, objectui#7762 and `bind` pins pass unchanged; they are in the full `types` run. - **Emitted `dist`**, after `pnpm --filter @object-ui/types build` at head: - the old clauses, both spellings: 0 files; - the `mode` clause: `zod/form.zod.js`, `zod/index.zod.js`; - the `events` clause: `data-display.d.ts`; the `bind` clause: `base.d.ts`; - the `exportOptions` note: `zod/objectql.zod.js`, `zod/index.zod.js` (the emitted JS keeps comments). ## Reverse verification (ablation), after the fix was committed **Instrument.** objectstack's `scripts/ablation-replace.mjs` in WRAP mode, run from this worktree. - The anchor must hit exactly once and the on-disk blob must move. The tool proves each restore: blob equals HEAD and `git diff HEAD` is empty. - The driver re-proved each restore independently, and ran inside an EXIT / INT / TERM trap. - The types pin imports `../zod/*` source and reads the files from disk. The components pin resolves the registry from source through the vitest aliases. So no build sits between a mutation and a run. | leg | red | red tests | |---|---|---| | the old clause back into `FormSchema.mode` | 3 / 9 | the `mode` arm, the spelling walk, the pairs walk | | the old clause back into `TimelineSchema.events` | 3 / 9 | the `events` docblock, the pairs walk, the parser-tier walk | | the old clause back into `BaseSchema.bind` | 3 / 9 | the `bind` docblock, both walks | | the old clause back into the `exportOptions` note | 2 / 9 | the note, the pairs walk | | the note names `unknown-prop` in place of `type-mismatch` | 1 / 9 | the note | | `zod/ai.zod.ts`, which the pin does not name: an objectui#10928 clause loses its parser-tier half | 1 / 9 | the parser-tier walk | | `header-bar.tsx` starts declaring `title` (components pin) | 4 / 6 | both population legs, both lit controls | - Every leg restored to its HEAD blob (`3a595a747`, `33e3a4770`, `a12a28cf1`, `4e75a80cf` twice, `a40e67ff6`, `ce3850854`) with an empty `git diff HEAD`. After the run, `git status --porcelain` was empty. - **The first `header-bar` attempt was a no-op.** Its replacement contained the anchor, so the tool refused (anchor count 1 to 1) and ran no test. It was re-run with a replacement that does not contain the anchor. The table shows the second run. ## The pins **`silence-clause-parser-tier-closure-10981.test.ts`.** It follows the house style of PR #10956 and PR #10980. `@object-ui/types` imports no workspace package, so it holds the prose. - **Zod face.** `FormSchema.mode` is read through its refusal: code `invalid_type` and path `['mode']` first, then the clause. The walk covers every spelling of the objectui#10286 pin, and the `.describe()` text. - **`exportOptions`.** The bare array is refused with `invalid_type` at `['exportOptions']`, with exactly the `.describe()` string. Then the note above `OBJECT_GRID_EXPORT_OPTIONS_GUIDANCE` must carry the clause and name no `unknown-prop`. - **TS face.** The `TimelineSchema.events` and `BaseSchema.bind` docblocks. - **The closure walk.** It reads every non-test `.ts` file of `packages/types/src` from disk, with the root taken from the pin's own `import.meta.url`. - No joined text may pair "no error" with "no warning" (patterns A, B, D, E). - Every C phrasing must have "the parser tier's" within 80 characters after it. - Non-vacuity, by name: the four site files are reached, and both earlier waves' clauses are found. **`header-bar-unknown-prop-parser-tier-10981.test.tsx`.** It answers the open question from PR #10956 and PR #10980, on the live components registry. - On `header-bar` and `ui:header-bar`, each of the 9 refused keys draws exactly one `unknown-prop` naming the key. This holds in every value shape the objectui#10387 and objectui#10286 pins refuse. - The control draws nothing. - The lit control shows `crumbs` declared, and no refused key declared. ## Dispatch assumptions, checked 1. **Holds.** There are exactly four sites, one per file, on `4b742f41d` and on `665025908`. The census was widened to patterns A–H above, with controls. 2. **Holds, and made more exact.** - `form` with `mode` draws `unknown-prop`: for all five spellings, on `form` and `ui:form`. - A bare `timeline` with `events` draws `unknown-prop`. The bare key resolves to `view:timeline`. The docblock describes `TimelineRenderer`, which is registered as `plugin-timeline:timeline` (`skipFallback`). That registration also answers `unknown-prop`, so the clause is true for the node the docblock names and for the bare key. - `data-table` with `bind` draws `unknown-prop`. But "only the parser tier noticed it" would be false there, because a render-time console warning fires too (objectui#6575). The site names both. - `object-grid` with an `exportOptions` array draws `type-mismatch`: for three array shapes, on both keys. 3. **Holds**, by source reading at the parent of each retirement. See above. 4. **Changed during the run.** PR #10986 landed (`8aa68b159`) on `zod/objectql.zod.ts`. `main` `665025908` was merged in before the final push, with no conflict. The note edited here is away from that hunk. PR #10997 had not landed at `665025908`. ## Gates, head `ebd156ef5` All ran after the merge commit. Exit codes were captured before any pipe. - `pnpm --filter @object-ui/types build` → 0. Its `check-dist-completeness` verified 134 emitted files. - `pnpm --filter @object-ui/types type-check` → 0. `tsc -p tsconfig.test.json --listFiles` includes the new pin. - `pnpm --filter @object-ui/types lint` → 0, with 0 errors. The 295 warnings are existing `no-explicit-any` ones; none is in the new file. - `pnpm exec vitest run packages/types/` → 0: 272 files and 5993 tests passed. - Outside `types` → 0: 27 files and 931 tests passed. - The script tests that read `packages/types/src`: `body-dialect-producer-scan`, `changeset-polarity-census`, `check-action-forward-parity`, `check-changeset-claims`, `check-component-surface-parity`, `check-doc-component-types`, `check-doc-example-types`, `check-handler-key-read-sites`, `check-readme-exports`, `check-spec-symbol-derivation`, `component-node-vocabulary-7434`, `one-authority-per-exported-name-6273`, `check-pending-changeset-literals`, `spec-symbol-ledger-types-7265`, `unconsumed-widget-option-claim-6186`, `vite-types-zod-lazy`; - `component-fixture-declared-keys` in `examples/schema-catalog`; - the new components pin, `container-declaration-ratchet`, the four `header-bar` component tests, `skill-guide-data-table-binding` and `data-table-bind-diagnostic`; - the parser pins `body-dialect-6771` and `containment-declared-slot-9910`. - `pnpm --filter '@object-ui/components^...' build`, then `pnpm --filter @object-ui/components type-check` → 0. `tsconfig.test.json --listFiles` includes the new components pin. `eslint` over the two new test files → 0, with no findings. - `check-changeset-presence`, `check-changeset-no-major`, `check-changeset-fixed`, `check-changeset-overwrite` → 0. `check-changeset-claims` → 0; it is report-only. - `pnpm check:control-bytes`, `check:new-line-citations` (0 new), `check:pending-changeset-literals`, `check:test-path-roots`, `check:phantom-deps` and `check:self-import` → 0. - `check-governed-queue-guard --test` over the 7 paths → NOT GOVERNED. - Declared to CI: repo-wide `pnpm lint`, full `pnpm test` and the other workflow gates. ## Acceptance notes Not changed here, and left for the seat to route. - **Same false claim, outside this package.** `skills/objectui/rules/protocol.md`, the "Readers only." paragraph, says a `bind` on `data-table` renders its header over an empty body "no error, no warning". The render leg and the parser leg above both contradict it. It is on the governed `skills/**` surface, outside this card. - **Same claim in unpublished test prose.** The headers of `base-bind-declared.test.ts` and `object-grid-export-options-refusal-7762.test.ts` repeat it. `mirror-partial-record-narrowing-8516.test.ts` has a different "no error and no warning" claim, about grid columns. All three are test files, outside the census. - **Pending changesets.** `.changeset/7762-object-grid-export-options-bare-array-refusal.md` and `.changeset/6575-data-table-bind-diagnostic.md` describe the pre-fix behaviour in the old words. Pending changesets are out of scope here. - **Adjacent wording, left in place.** - The `base.ts` heading "the one documented silent failure" stays. The corrected sentence now says in which sense it is silent: nothing on the page. - The `exportOptions` guidance string says an array "is silently dropped for the csv/json default". That is present tense about the renderer, and "silently" alone is outside the phrasing family. - **H, the "no diagnostic" nouns (24 at head).** Most are scoped to a named tier or instrument: "on either face" (`objectql.ts`), "zero unresolved-module diagnostics" from `tsc`, `checkType` by name, the TS index signature. A few are unscoped claims about an authored value: `ai.ts` (`maxResults`), `crud.ts`, `data-display.ts` (three) and `zod/data-display.zod.ts`. They were not measured here. - **Observation.** A `mode` on a `form` reaches the DOM as an inert `mode` attribute on the form element. The visible form is identical. - **Boundary.** `view:form` declares `mode` (enum `create`, `edit`, `view`). It is a different registration and node type, which the `FormSchema` docblock does not describe. - **A parser-tier finding, reported to the seat.** `validateTree` answers `bind` with `unknown-prop` on `list` and `tree-view` too, although both read `bind` through `useDataScope`. No registration declares a `bind` input, and the parser's base-prop set has no `bind`. --- _Generated by [Claude Code](https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f6fb83f commit 797a30f

7 files changed

Lines changed: 379 additions & 5 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@object-ui/types': patch
3+
---
4+
5+
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).
6+
7+
- 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".
8+
- 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`.
9+
- 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".
10+
- 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)".
11+
12+
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.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* The parser-tier half of the `header-bar` refusal clause, held on the live
11+
* registry (objectui#10981, the open question from objectui#10959).
12+
*
13+
* objectui#10959 corrected the `HeaderBarSchema` refusals of objectui#10387
14+
* (`title`, `logo`, `nav`, `left`, `center`, `right`, `sticky`, `height`) and
15+
* objectui#10286 (`variant`) to say that nothing warned at render, and that
16+
* "only the parser tier's `unknown-prop` warning noticed it". The pin for that
17+
* wording lives in `@object-ui/types`, which cannot import the parser, so it
18+
* holds the prose only. The parser-tier behaviour was measured once, on the
19+
* objectui#10959 pull request, and pinned nowhere: `validateTree`'s generic
20+
* undeclared-key branch answers it, and no parser pin names `header-bar`.
21+
*
22+
* This file holds that half where the registry lives. The manifest is built
23+
* the way `container-declaration-ratchet.test.tsx` builds it — every KNOWN
24+
* registry key, through `manifestFromConfigs` — so a registration that starts
25+
* declaring one of these keys as an input turns this red, and so does a change
26+
* to the parser that stops answering an undeclared key with `unknown-prop`.
27+
* Either one makes the published clause false, and this is where it shows.
28+
*
29+
* Both spellings are held: the bare `header-bar` and the namespaced
30+
* `ui:header-bar`. Every value shape the objectui#10387 / objectui#10286 pins
31+
* refuse is held too, because the answer depends only on the key being
32+
* undeclared, and a pin that tried one shape could not show that.
33+
*/
34+
35+
import { describe, it, expect } from 'vitest';
36+
import { ComponentRegistry } from '@object-ui/core';
37+
import { manifestFromConfigs, validateTree } from '@object-ui/sdui-parser';
38+
import type { Diagnostic, SchemaElement } from '@object-ui/sdui-parser';
39+
40+
// Module scope, not a hook: this import IS the registration (AGENTS.md
41+
// §测试纪律 — an unbounded module load must not be billed to a bounded window).
42+
import '../index';
43+
44+
/** Every live registry key, as `container-declaration-ratchet.test.tsx` builds it. */
45+
const diagnose = (schema: unknown): Diagnostic[] => {
46+
const configs = ComponentRegistry.getKnownTypes().map((t) => {
47+
const meta = ComponentRegistry.getMeta(t);
48+
return { type: t, namespace: meta?.namespace, isContainer: meta?.isContainer, inputs: meta?.inputs };
49+
});
50+
const manifest = manifestFromConfigs(configs as unknown as Parameters<typeof manifestFromConfigs>[0]);
51+
return validateTree(schema as SchemaElement, manifest).diagnostics;
52+
};
53+
54+
const TEXT = { type: 'text', content: 'x' };
55+
56+
/** The refused keys and the values their `@object-ui/types` pins refuse. */
57+
const RETIRED: Record<string, unknown[]> = {
58+
title: ['My App'],
59+
logo: ['/logo.svg', TEXT, [TEXT]],
60+
nav: [[{ label: 'Docs', href: '/docs' }], []],
61+
left: [TEXT, [TEXT], 'plain text'],
62+
center: [TEXT, [TEXT], 'plain text'],
63+
right: [TEXT, [TEXT]],
64+
sticky: [true, false],
65+
height: ['64px', 64],
66+
variant: ['default', 'bordered', 'floating', 'transparent'],
67+
};
68+
69+
const TYPES = ['header-bar', 'ui:header-bar'] as const;
70+
const CRUMBS = [{ label: 'Home', href: '/' }];
71+
72+
describe('header-bar — the refused keys draw exactly the parser tier\'s `unknown-prop` (objectui#10981)', () => {
73+
it.each(TYPES)('`%s` is a live registration, and its control draws nothing', (type) => {
74+
expect(ComponentRegistry.getKnownTypes()).toContain(type);
75+
expect(diagnose({ type, crumbs: CRUMBS })).toEqual([]);
76+
});
77+
78+
it.each(TYPES)('`%s` — every refused key, in every refused shape, draws one `unknown-prop` naming it', (type) => {
79+
const wrong: string[] = [];
80+
for (const [key, values] of Object.entries(RETIRED)) {
81+
for (const value of values) {
82+
const found = diagnose({ type, crumbs: CRUMBS, [key]: value });
83+
const one = found.length === 1 ? found[0] : undefined;
84+
if (one?.code !== 'unknown-prop' || !one.message.includes(`"${key}"`)) {
85+
wrong.push(`${key}=${JSON.stringify(value)} → ${JSON.stringify(found.map((d) => d.code))}`);
86+
}
87+
}
88+
}
89+
expect(wrong).toEqual([]);
90+
});
91+
92+
it.each(TYPES)('`%s` — lit control: the keys it does declare draw no `unknown-prop`', (type) => {
93+
const declared = (ComponentRegistry.getMeta(type)?.inputs ?? []).map((input) => input.name);
94+
expect(declared).toContain('crumbs');
95+
for (const key of Object.keys(RETIRED)) expect(declared).not.toContain(key);
96+
const found = diagnose({ type, crumbs: CRUMBS, rightContent: TEXT });
97+
expect(found.filter((d) => d.code === 'unknown-prop')).toEqual([]);
98+
});
99+
});
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* objectui#10981 — the family closure for the published "no error, no warning"
11+
* claims, after objectui#10928 (`not-a-container`) and objectui#10959
12+
* (`unknown-prop`).
13+
*
14+
* Four more sites in this package said an authored value drew no error and no
15+
* warning. The parser tier's `validateTree` (`@object-ui/sdui-parser`) answers
16+
* each of them, with the code the registration's declared inputs decide:
17+
*
18+
* - `FormSchema.mode` (`zod/form.zod.ts`, objectui#10286, zod face):
19+
* `form` declares no `mode` input, so every spelling draws `unknown-prop`.
20+
* - `TimelineSchema.events` (`data-display.ts`, objectui#6170, TS face):
21+
* neither the presentational `TimelineRenderer` registration this type
22+
* describes nor the view registration the bare key resolves to declares
23+
* `events`, so it draws `unknown-prop`.
24+
* - `BaseSchema.bind` on `data-table` (`base.ts`, TS face): `data-table`
25+
* declares no `bind` input, so it draws `unknown-prop`. This one was not
26+
* silent at render either: `data-table` has logged a console warning for
27+
* an authored `bind` since objectui#6575, so the clause names both.
28+
* - The bare `exportOptions` array on `object-grid` (`zod/objectql.zod.ts`,
29+
* objectui#7762, the source note above the refusal string): the
30+
* registration declares `exportOptions` as an object, so an array draws
31+
* `type-mismatch`, not `unknown-prop`.
32+
*
33+
* ## Why this file pins the prose, and where the parser half lives
34+
*
35+
* `@object-ui/types` declares no workspace dependency and its tests import
36+
* none, so `@object-ui/sdui-parser` is not importable here (the same reason as
37+
* the objectui#10928 and objectui#10959 pins). The four readings behind these
38+
* clauses — every key spelling and value shape each sentence describes, over
39+
* the live registry, with a render leg — are on objectui#10981's pull request:
40+
* a record of one run, not re-derived here. The `data-table` render warning is
41+
* pinned in `packages/components`, by `skill-guide-data-table-binding.test.tsx`.
42+
*
43+
* ## What is pinned, and what is deliberately not
44+
*
45+
* One string per site, as a substring: on the zod face read through the
46+
* refusal with its code and path first, on the TS face read off the member's
47+
* docblock. Then the closure walk: over every non-test `.ts` file of this
48+
* package's `src`, no joined text still pairs "no error" with "no warning",
49+
* and every "no … error or warning" phrasing names the parser tier in its own
50+
* sentence or the next one. The rest of each string is NOT pinned; the sites'
51+
* own pins read it.
52+
*/
53+
54+
import { describe, it, expect } from 'vitest';
55+
import { readFileSync, readdirSync, statSync } from 'node:fs';
56+
import { join, relative } from 'node:path';
57+
import { FormSchema } from '../zod/form.zod';
58+
import { ObjectGridSchema } from '../zod/objectql.zod';
59+
60+
/** The clause each site now carries. */
61+
const UNKNOWN_PROP_TAIL = "only the parser tier's `unknown-prop` warning noticed it";
62+
const FORM_CLAUSE = `rendered the same form — no render-time error or warning; ${UNKNOWN_PROP_TAIL}`;
63+
const TIMELINE_CLAUSE = `drew an EMPTY rail, with no render-time error or warning; ${UNKNOWN_PROP_TAIL}`;
64+
const BIND_CLAUSE =
65+
'with no render-time error; nothing on the page says why, but a render-time console warning '
66+
+ "(`[ObjectUI] DataTable bind:`, objectui#6575) and the parser tier's `unknown-prop` warning both name it";
67+
const EXPORT_CLAUSE =
68+
"no render-time error, warning or console line (only the parser tier's `type-mismatch` warning noticed it)";
69+
70+
/** The phrasings they replaced — false about the parser tier. */
71+
const RETIRED = ['no error, no warning', 'no error and no warning'];
72+
73+
/**
74+
* The repo root, derived from THIS FILE's own location and never from the cwd
75+
* (objectui#7799): bare `import.meta.url`, taken apart by hand (the spelling
76+
* landed for objectui#7791, PR #7796).
77+
*/
78+
const SELF_DEPTH_BELOW_REPO_ROOT = 5; // packages / types / src / __tests__ / this file
79+
const REPO_ROOT = decodeURIComponent(new URL(import.meta.url).pathname)
80+
.split('/')
81+
.slice(0, -SELF_DEPTH_BELOW_REPO_ROOT)
82+
.join('/');
83+
const read = (rel: string): string => readFileSync(join(REPO_ROOT, rel), 'utf8');
84+
85+
const SRC = 'packages/types/src';
86+
87+
/**
88+
* Text as a reader sees it: JSDoc and line-comment continuations joined,
89+
* `'…' + '…'` concatenations joined, escaped quotes unescaped.
90+
*/
91+
const joinText = (src: string): string =>
92+
src
93+
.replace(/(['"`])\s*\+\s*\n?\s*(['"`])/g, '')
94+
.replace(/\n\s*\*\/?\s?/g, ' ')
95+
.replace(/\n\s*\/\/\s?/g, ' ')
96+
.replace(/\\(['"`])/g, '$1')
97+
.replace(/[ \t]+/g, ' ');
98+
99+
/** The docblock on `member` inside `export interface name`, joined. */
100+
function memberDocblock(src: string, name: string, member: string): string {
101+
const start = src.indexOf(`export interface ${name} `);
102+
expect(start, `interface ${name} not found`).toBeGreaterThan(-1);
103+
const end = src.indexOf('\n}', start);
104+
const body = src.slice(start, end);
105+
const at = body.search(new RegExp(`\\n\\s*${member}\\?:`));
106+
expect(at, `member ${name}.${member} not found`).toBeGreaterThan(-1);
107+
const before = body.slice(0, at);
108+
const open = before.lastIndexOf('/**');
109+
expect(open, `no docblock before ${name}.${member}`).toBeGreaterThan(-1);
110+
return joinText(before.slice(open));
111+
}
112+
113+
/** The issue a schema raises at exactly `[key]`, if any. */
114+
function issueAt(result: { success: boolean; error?: { issues: Array<{ code: string; path: PropertyKey[]; message: string }> } }, key: string) {
115+
return result.success ? undefined : result.error?.issues.find((i) => i.path.length === 1 && i.path[0] === key);
116+
}
117+
118+
describe('objectui#10981 — the `FormSchema.mode` refusal names the parser tier\'s `unknown-prop`', () => {
119+
const NODE = { type: 'form' as const, fields: [{ name: 'title', label: 'Title', type: 'text' }] };
120+
121+
it('`mode` is refused by name, and the refusal names `unknown-prop`', () => {
122+
const issue = issueAt(FormSchema.safeParse({ ...NODE, mode: 'edit' }), 'mode');
123+
expect(issue?.code).toBe('invalid_type');
124+
expect(issue?.path).toEqual(['mode']);
125+
expect(issue?.message).toContain(FORM_CLAUSE);
126+
for (const old of RETIRED) expect(issue?.message).not.toContain(old);
127+
});
128+
129+
it('every spelling the objectui#10286 pin refuses carries it, and so does the `.describe()` text', () => {
130+
const stale = ['edit', 'read', 'disabled', 'create', 'view'].filter(
131+
(mode) => !issueAt(FormSchema.safeParse({ ...NODE, mode }), 'mode')?.message.includes(FORM_CLAUSE),
132+
);
133+
expect(stale).toEqual([]);
134+
expect(FormSchema.shape.mode.description).toContain(FORM_CLAUSE);
135+
});
136+
});
137+
138+
describe('objectui#10981 — the `exportOptions` note names the parser tier\'s `type-mismatch`', () => {
139+
const NODE = { type: 'object-grid' as const, objectName: 'accounts' };
140+
141+
it('a bare array is refused at `exportOptions` with the one string the note sits above', () => {
142+
const issue = issueAt(ObjectGridSchema.safeParse({ ...NODE, exportOptions: ['csv', 'xlsx'] }), 'exportOptions');
143+
expect(issue?.code).toBe('invalid_type');
144+
expect(issue?.path).toEqual(['exportOptions']);
145+
expect(issue?.message).toBe(ObjectGridSchema.shape.exportOptions.description);
146+
});
147+
148+
it('the note above `OBJECT_GRID_EXPORT_OPTIONS_GUIDANCE` carries the clause, and names no `unknown-prop`', () => {
149+
const src = read(`${SRC}/zod/objectql.zod.ts`);
150+
const decl = src.indexOf('\nconst OBJECT_GRID_EXPORT_OPTIONS_GUIDANCE =');
151+
expect(decl, 'the guidance constant moved').toBeGreaterThan(-1);
152+
const open = src.lastIndexOf('/**', decl);
153+
const note = joinText(src.slice(open, decl));
154+
expect(note).toContain('objectui#7762');
155+
expect(note).toContain(EXPORT_CLAUSE);
156+
for (const old of RETIRED) expect(note).not.toContain(old);
157+
expect(note).not.toContain('`unknown-prop`');
158+
});
159+
});
160+
161+
describe('objectui#10981 — the TS-face docblocks name what the parser tier answers', () => {
162+
it('`TimelineSchema.events` (objectui#6170) names `unknown-prop`', () => {
163+
const doc = memberDocblock(read(`${SRC}/data-display.ts`), 'TimelineSchema', 'events');
164+
expect(doc).toContain('RETIRED (objectui#6170');
165+
expect(doc).toContain(TIMELINE_CLAUSE);
166+
for (const old of RETIRED) expect(doc).not.toContain(old);
167+
});
168+
169+
it('`BaseSchema.bind` names the `data-table` console warning and `unknown-prop`', () => {
170+
const doc = memberDocblock(read(`${SRC}/base.ts`), 'BaseSchema', 'bind');
171+
expect(doc).toContain('`data-table` does NOT');
172+
expect(doc).toContain(BIND_CLAUSE);
173+
for (const old of RETIRED) expect(doc).not.toContain(old);
174+
});
175+
});
176+
177+
describe('objectui#10981 — the closure walk over this package\'s `src`', () => {
178+
/** Every non-test `.ts` file under `src`, relative to the repo root. */
179+
const files: string[] = [];
180+
const walk = (dir: string): void => {
181+
for (const name of readdirSync(dir)) {
182+
const abs = join(dir, name);
183+
if (statSync(abs).isDirectory()) {
184+
if (name !== '__tests__') walk(abs);
185+
} else if (name.endsWith('.ts') && !name.endsWith('.test.ts')) {
186+
files.push(relative(REPO_ROOT, abs));
187+
}
188+
}
189+
};
190+
walk(join(REPO_ROOT, SRC));
191+
const texts = files.map((f) => [f, joinText(read(f))] as const);
192+
193+
/** "no error" and "no warning" paired, in either order — the retired shape. */
194+
const PAIRED = [
195+
/\bno (?:[\w-]+ )?errors?\b[^.;]{0,60}?\bno (?:[\w-]+ )?warnings?\b/gi,
196+
/\bno (?:[\w-]+ )?warnings?\b[^.;]{0,60}?\bno (?:[\w-]+ )?errors?\b/gi,
197+
/\bwithout (?:an? |any )?(?:[\w-]+ )?errors? (?:or|nor|and) (?:an? |any )?(?:[\w-]+ )?warnings?\b/gi,
198+
/\bneither (?:an? )?errors? nor (?:an? )?warnings?\b/gi,
199+
];
200+
/** "no … error or warning" — the family's corrected shape, which must name the parser tier. */
201+
const COMBINED = /\bno (?:[\w-]+ )?errors? (?:or|nor|and) (?:[\w-]+ )?warnings?\b/gi;
202+
const PARSER_TIER = "the parser tier's";
203+
204+
/**
205+
* Where the parser tier has to be named: anywhere from the phrasing to the
206+
* end of the sentence AFTER the one it sits in.
207+
*
208+
* The phrasing speaks for the render tier only, so the text must go on to say
209+
* what the parser tier did. The family writes that in one of two places. Most
210+
* sites keep it in the same sentence ("…; only the parser tier's
211+
* `unknown-prop` warning noticed it"). A site where the parser tier is
212+
* genuinely silent gives it the next sentence, because it has a reason to
213+
* record: objectui#9256's `metric-card` refusal says that "in a widget slot
214+
* nothing else noticed it either: the parser tier's `not-a-container` warning
215+
* (objectui#9910) walks `children`, never `widgets`". A fixed character count
216+
* after the match is not that rule. It was 80 here, and it failed the
217+
* `metric-card` text, which names the parser tier correctly but a little
218+
* further on. Any count is too short for some honest sentence and long
219+
* enough to reach an unrelated one somewhere else.
220+
*
221+
* A sentence ends at `.`, `!` or `?` followed by whitespace, the end of the
222+
* text, or a closing quote (a zod string ends in `.'`). So the dots in
223+
* `zod/form.zod.ts` or `.describe()` end nothing. `MAX_WINDOW` does not set
224+
* the rule. It only stops a text with no sentence end from reaching the rest
225+
* of its file.
226+
*/
227+
const MAX_WINDOW = 600;
228+
const windowAfter = (t: string, from: number): string => {
229+
const end = /[.!?](?=['"`]?(?:\s|$))/g;
230+
end.lastIndex = from;
231+
const own = end.exec(t);
232+
const next = own ? end.exec(t) : null;
233+
return t.slice(from, Math.min(next ? next.index + 1 : t.length, from + MAX_WINDOW));
234+
};
235+
236+
it('the walk reaches the four sites and both earlier waves (non-vacuity, by name rather than by count)', () => {
237+
const names = files.map((f) => f.slice(SRC.length + 1));
238+
for (const f of ['base.ts', 'data-display.ts', 'zod/form.zod.ts', 'zod/objectql.zod.ts']) expect(names).toContain(f);
239+
const text = (f: string) => texts.find(([p]) => p === `${SRC}/${f}`)?.[1] ?? '';
240+
expect(text('zod/navigation.zod.ts')).toContain(UNKNOWN_PROP_TAIL);
241+
expect(text('ai.ts')).toContain("only the parser tier's `not-a-container` warning (objectui#9910) noticed it");
242+
});
243+
244+
it('no file still pairs "no error" with "no warning"', () => {
245+
const hits = texts.flatMap(([f, t]) => PAIRED.flatMap((re) => [...t.matchAll(re)].map((m) => `${f}: «${m[0]}»`)));
246+
expect(hits).toEqual([]);
247+
});
248+
249+
it('every "no … error or warning" phrasing names the parser tier in the same or the next sentence', () => {
250+
const bare = texts.flatMap(([f, t]) =>
251+
[...t.matchAll(COMBINED)]
252+
.map((m) => windowAfter(t, m.index ?? 0))
253+
.filter((w) => !w.includes(PARSER_TIER))
254+
.map((w) => `${f}: «${w}»`),
255+
);
256+
expect(bare).toEqual([]);
257+
});
258+
});

0 commit comments

Comments
 (0)