Skip to content

Commit a3a8bfc

Browse files
committed
fix(types,cli): arm the three plugin-ai node types in AnyComponentSchema, and ratchet the registered types validate refuses
`ai-form-assist`, `ai-recommendations` and `nl-query` are registered, declared on the TypeScript face and taught by the plugin-ai README, and `safeValidateSchema` refused every document naming one with `invalid_union` at `type`. A new `ai.zod.ts` restates the three declarations member for member (the `on*` handler members follow objectui#6182 / #6124 and refuse by name), and `AnyComponentSchema` lists its category union. The new cli ratchet counts the registered bare keys refused at `type`, pinned at the head's 76 (79 on main, minus these three). It only falls. Part of objectui#10859 (batch 1). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
1 parent a60539b commit a3a8bfc

6 files changed

Lines changed: 769 additions & 2 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@object-ui/types': minor
3+
---
4+
5+
`safeValidateSchema` — and so `objectui validate` — accepts the three `@object-ui/plugin-ai` node types, `ai-form-assist`, `ai-recommendations` and `nl-query` (objectui#10859, batch 1).
6+
7+
**Clause-②: yes** — the accept set of `AnyComponentSchema` widens by three `type` literals, and `@object-ui/types/zod` exports eight new schemas. Nothing that parsed before is refused now.
8+
9+
**What it was.** All three types are registered by `@object-ui/plugin-ai`, declared on the published TypeScript face (`AIFormAssistSchema`, `AIRecommendationsSchema`, `NLQuerySchema`) and taught by that package's README. `AnyComponentSchema` carried no arm for any of them, so every document naming one was refused with `invalid_union` at `type` — the README's own `{ "type": "ai-form-assist", "showConfidence": true, "showReasoning": true }` included, which `objectui validate` reported as "Schema validation failed".
10+
11+
**What changed, in observable terms.**
12+
13+
- `@object-ui/types/zod` exports `AIFormAssistSchema`, `AIRecommendationsSchema`, `NLQuerySchema`, their sub-schemas `AIConfigSchema`, `AIFieldSuggestionSchema`, `AIRecommendationItemSchema` and `NLQueryResultSchema`, and the category union `AIComponentSchema`, which `AnyComponentSchema` now lists. The strict authoring face (`StrictAnyComponentSchema`) derives from `AnyComponentSchema` and accepts the same three types, closed to undeclared keys like every other arm.
14+
- Each arm restates its TypeScript declaration member for member. The objectui#8178 retired members (`formId`, `objectName`, `fields`, `autoFill`, `maxResults`) and both content channels (objectui#9256) are refused by name, as they already were on the TypeScript face.
15+
- ⚠️ The `on*` members are the one place the two faces differ. The TypeScript face still types `onApplySuggestion`, `onRejectSuggestion`, `onSelect`, `onDismiss` and `onSubmit` as `string`; the zod face refuses an authored value on each, by name, following objectui#6182 (the handler-expression string dialect is not an authoring form) and objectui#6124 (`onSelect` / `onDismiss` / `onSubmit` are runtime slots a React host fills; the other two have no reader). Author behaviour as a node type — an `action:button` node with a declared action.
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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+
* How many REGISTERED component types does `objectui validate` refuse at
11+
* `type`? A ratchet (objectui#10859).
12+
*
13+
* ## The gap this closes
14+
*
15+
* `objectui validate` judges a document with `safeValidateSchema`, whose root
16+
* is `AnyComponentSchema` — a union discriminated on `type`. A registered type
17+
* with no arm in that union is refused with one `invalid_union` issue at
18+
* `['type']`, whatever else the document says: `ai-form-assist`, which
19+
* `packages/plugin-ai/README.md` teaches, was one of them. The mirror-parity
20+
* ratchet in `@object-ui/types` counts declaration↔mirror PAIRS and the keys
21+
* inside them; a registered type with no mirror at all is in neither, so
22+
* nothing counted this family until this file.
23+
*
24+
* ## What is counted, and against what
25+
*
26+
* The population is the bare (un-namespaced) keys of `KNOWN_SCHEMA_TYPES`
27+
* (`../utils/known-schema-types.ts`), the list generated from the registration
28+
* calls themselves, which `objectui check` already uses. A key is REFUSED when
29+
* `safeValidateSchema({ type: KEY })` fails with an `invalid_union` issue at
30+
* path `['type']` — the "no arm claims this literal" reading. A key whose arm
31+
* exists but wants more than the bare `type` (a required member) is not
32+
* refused here: its `type` is claimed, and the missing member is the
33+
* document's problem, not the union's.
34+
*
35+
* ## The pin moves ONE way
36+
*
37+
* `REFUSED_AT_TYPE` is the head's count. objectui#10859 lowers it batch by
38+
* batch as each registered, declared type gets its arm (or, for a type that is
39+
* not meant to be authored, as the seat rules on it). ⛔ It never rises: a
40+
* registration added without an arm, or an arm removed, turns this file red
41+
* with the refused keys listed, which is where the next author finds out.
42+
* ⛔ Do not raise the constant to make it pass — arm the type, or take the
43+
* registration's authorability to the card.
44+
*/
45+
46+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
47+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
48+
import { dirname, join } from 'node:path';
49+
import { tmpdir } from 'node:os';
50+
import { fileURLToPath } from 'node:url';
51+
import { safeValidateSchema } from '@object-ui/types/zod';
52+
53+
import { KNOWN_SCHEMA_TYPES } from '../utils/known-schema-types.js';
54+
import { validate } from '../commands/validate.js';
55+
56+
/**
57+
* The head's refused count (objectui#10859 batch 1: 79 on `main` before it,
58+
* minus the three `@object-ui/plugin-ai` arms). LOWER it when a batch arms more
59+
* keys; never raise it.
60+
*/
61+
const REFUSED_AT_TYPE = 76;
62+
63+
/** The bare registry keys — the population the card measured. */
64+
const BARE_KEYS = KNOWN_SCHEMA_TYPES.filter((key) => !key.includes(':'));
65+
66+
/** Is `type` unclaimed by every arm of the validator's root union? */
67+
function refusedAtType(type: string): boolean {
68+
const result = safeValidateSchema({ type });
69+
if (result.success) return false;
70+
return result.error.issues.some(
71+
(issue) => issue.code === 'invalid_union' && issue.path.length === 1 && issue.path[0] === 'type',
72+
);
73+
}
74+
75+
describe('registered component types refused at `type` — a ratchet (objectui#10859)', () => {
76+
it('the refused count equals the pin, and only ever falls', () => {
77+
const refused = BARE_KEYS.filter(refusedAtType);
78+
expect(
79+
refused.length,
80+
[
81+
`\`objectui validate\` refuses ${refused.length} registered bare key(s) at \`type\`; the pin is ${REFUSED_AT_TYPE}.`,
82+
refused.length < REFUSED_AT_TYPE
83+
? `Fewer than the pin — an arm landed. LOWER \`REFUSED_AT_TYPE\` to ${refused.length} in this same change (objectui#10859).`
84+
: 'MORE than the pin — a registered key lost its arm or a registration landed without one. '
85+
+ 'Arm it in `@object-ui/types/zod` (or take its authorability to objectui#10859); ⛔ never raise the pin.',
86+
`Refused: ${refused.join(', ')}`,
87+
].join('\n'),
88+
).toBe(REFUSED_AT_TYPE);
89+
});
90+
91+
it('reads the whole generated population, not a fragment of it (non-vacuity)', () => {
92+
// The card's measurement was over this same population; a filter that
93+
// matched nothing would make the count above trivially small.
94+
expect(BARE_KEYS.length).toBeGreaterThan(200);
95+
expect(BARE_KEYS).toContain('timeline');
96+
expect(BARE_KEYS).toContain('ai-form-assist');
97+
});
98+
99+
it('tells a claimed type from an unclaimed one — both instrument controls fire', () => {
100+
// Lit control: an armed, registered key is not refused at `type`.
101+
expect(refusedAtType('timeline')).toBe(false);
102+
// A key whose arm wants a required member fails, but NOT at `type`.
103+
expect(safeValidateSchema({ type: 'chart' }).success).toBe(false);
104+
expect(refusedAtType('chart')).toBe(false);
105+
// And a type no arm claims IS refused there.
106+
expect(refusedAtType('no-such-component-10859')).toBe(true);
107+
});
108+
109+
it('counts the three plugin-ai keys as armed (objectui#10859 batch 1)', () => {
110+
for (const key of ['ai-form-assist', 'ai-recommendations', 'nl-query']) {
111+
expect(BARE_KEYS, key).toContain(key);
112+
expect(refusedAtType(key), key).toBe(false);
113+
}
114+
});
115+
});
116+
117+
/* ── End to end: the README document through `objectui validate` ─────────── */
118+
119+
/** Rooted on this file, never on `process.cwd()`. */
120+
const HERE = dirname(fileURLToPath(import.meta.url));
121+
const PLUGIN_AI_README = join(HERE, '..', '..', '..', 'plugin-ai', 'README.md');
122+
123+
/** See `validate-root-path-line.test.ts` — the escape byte is never spelled. */
124+
const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g');
125+
126+
let dir: string;
127+
let out: string[];
128+
let exitCodes: number[];
129+
let restore: () => void;
130+
131+
beforeEach(() => {
132+
dir = mkdtempSync(join(tmpdir(), 'objectui-validate-10859-'));
133+
out = [];
134+
exitCodes = [];
135+
const originalLog = console.log;
136+
const originalError = console.error;
137+
const capture = (...args: unknown[]) => {
138+
out.push(args.map(String).join(' '));
139+
};
140+
console.log = capture;
141+
console.error = capture;
142+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code?: number) => {
143+
exitCodes.push(code ?? 0);
144+
return undefined as never;
145+
}) as never);
146+
restore = () => {
147+
console.log = originalLog;
148+
console.error = originalError;
149+
exitSpy.mockRestore();
150+
};
151+
});
152+
153+
afterEach(() => {
154+
restore();
155+
rmSync(dir, { recursive: true, force: true });
156+
});
157+
158+
describe('objectui validate — the plugin-ai README document (objectui#10859)', () => {
159+
it('validates the "Schema-Driven Usage" `ai-form-assist` document', async () => {
160+
const readme = readFileSync(PLUGIN_AI_README, 'utf8');
161+
const section = readme.slice(readme.indexOf('## Schema-Driven Usage'));
162+
const fence = /```json\n([\s\S]*?)\n```/.exec(section);
163+
expect(fence, 'no ```json fence under "## Schema-Driven Usage"').not.toBeNull();
164+
const file = join(dir, 'ai-form-assist.json');
165+
writeFileSync(file, (fence as RegExpExecArray)[1], 'utf-8');
166+
// Lit control on the extraction: it is the taught node.
167+
expect(JSON.parse((fence as RegExpExecArray)[1]).type).toBe('ai-form-assist');
168+
169+
await validate(file);
170+
171+
const text = out.join('\n').replace(ANSI, '');
172+
expect(text).not.toContain('Schema validation failed');
173+
expect(text).toContain('Schema is valid');
174+
expect(exitCodes).toEqual([0]);
175+
});
176+
});

0 commit comments

Comments
 (0)