Skip to content

Commit c30c8dd

Browse files
fix(types)!: the AI node declarations stop offering members no runtime honours (objectui#10874) (#10890)
Fixes #10874 Clause-②: yes Implemented on `claude/issue-10874-ai-node-faces-agree` by session `session_014mXUNuFomfj24w7s1pZzhN` (`domain:ui` seat 2 dispatch, claim `5862305527`). Serial after PR objectui#10870, which has landed. Head `ce2958f243`: fix round 1 on the contract review, which reset the handler members per key by the objectui#6124 rule. ## What changed - **TypeScript face** (`packages/types/src/ai.ts`), settled per key on `AIFormAssistSchema`, `AIRecommendationsSchema` and `NLQuerySchema`: - **Runtime slots, now callable.** `AIRecommendationsSchema.onSelect` and `.onDismiss` become `(item: AIRecommendationItem) => void`, and `NLQuerySchema.onSubmit` becomes `(query: string) => void`. Each was typed `string`. - Why: each node type is registered to its raw component, `SchemaRenderer` spreads the node's own keys onto the component's props, and `AIRecommendations` and `NLQueryInput` call these keys. - Each carries a RUNTIME SLOT docblock in the objectui#7344 form, like `DetailViewSchema.onBack`. - **Four keys become `?: never` tombstones, on seven member slots**, each with a RETIRED docblock, its prescription and `@deprecated`: - `onApplySuggestion` and `onRejectSuggestion`: nothing reads either; - `config` on all three node types; - `context` on form assist and recommendations. - The `AIConfig` docblock, and the retired-insights tombstone docblock in the same file, carry a dated note: no node member takes `AIConfig` now, and the type stays exported. - **zod face** (`packages/types/src/zod/ai.zod.ts`). - `config` and `context` become `retirementTombstone` members. The guidance names the key and the node, and says: "configure the model in your host's own AI service (ObjectUI has no AI-provider API; these components call no model), and hand its output to the node as" the right key for that node. - The five `on*` arms keep their `handlerKeyRefusal`, unchanged. For the three slots that is the objectui#6124 shape again: a named refusal on the JSON face beside a callable twin. - The module docblock says so. - **`packages/plugin-ai/README.md`** gains a migration table for the four retired keys (seven member slots), and a paragraph on the three callable slots. - **Changesets.** - New `.changeset/10874-ai-node-faces-agree.md`: `'@object-ui/types': minor`, a BREAKING (authoring) banner that says which members became callable and which became `never`, the Clause-② line and the migration. - Dated notes in the objectui#10533 form go on `.changeset/10859-ai-node-zod-arms.md` and `.changeset/8800-retire-ai-insights.md`, each on a sentence this change made false. The frontmatter md5 is unchanged on both. - **Tests.** - New: `ai-node-faces-agree-10874.test.ts`. - `ai-zod-arms-10859.test.ts`: `assertionHandlerDivergence` becomes `assertionHandlerFacesSettled`, and the member-type rows are updated. - `zod-mirror-parity.test.ts`: one stale entry deleted, two entries reworded, and the header figures moved. - `component-docs-retired-handler-keys-7340.test.ts`: the census moves. No renderer changes. `AIFormAssist`, `AIRecommendations` and `NLQueryInput` keep their props interfaces untouched. `tombstone.zod.ts` is untouched: round 1's docblock edit there is reverted, because the three slots no longer need an exception. ## H1: the disagreement, reproduced on base `1c5ee334a2` The probes were temporary: never committed, and deleted after the reading. - **TypeScript face.** Three literals, each annotated with its declaration, were compiled by `tsc` under `packages/types/tsconfig.test.json`: `{ type: 'ai-recommendations', onSelect: 'x' }`, `{ type: 'ai-form-assist', config: { provider: 'openai', model: 'gpt-4' }, context: { record: 1 } }` and `{ type: 'nl-query', config: { temperature: 0.2 }, onSubmit: 'go' }`. Result: exit 0, the probe file listed by `--listFiles`, zero diagnostics. - **zod face, same base.** - `safeValidateSchema({ type: 'ai-recommendations', onSelect: 'x' })` is refused: code `custom`, path `onSelect`, and a message naming `onSelect` a RUNTIME SLOT. The strict face refuses it too. - These three were ACCEPTED by both `safeValidateSchema` and `StrictAnyComponentSchema`: - `ai-form-assist` with `config` and `context`; - `ai-recommendations` with `config` and `context`; - `nl-query` with `config`. - **Nothing reads `config` or `context`.** - Source: `git grep -w -E 'config|context' -- packages/plugin-ai/src ':!*.test.*'` gives 0 hits. The control `showScores`, in the same scope, gives 3. - Run time, through the real `SchemaRenderer` and registry. Each of the three nodes was rendered with and without `config` and `context`. - The markup was byte-identical on all three: `identical-with-config-context true` ×3. - A lit control changed the markup on all three: `lit-control-differs true` ×3. The controls were `showConfidence`, `showScores` and `placeholder`. - **The three slots ARE read.** The seat re-read this on main during the review: - the registrations are the raw components; - `SchemaRenderer`'s `outgoingComponentProps` spread strips no `on*` key; - `AIRecommendations` calls `onSelect?.(item)` and `onDismiss?.(item)`; - `NLQueryInput` calls `onSubmit`. - So a function on the node is called, and a string was called as a function and threw. ## H2: host census, zero readers of `config` / `context` - **Direct reads.** `git grep -E '(schema|node|props)\??\.(config|context)\b'` over non-test `packages/*/src/**` and `apps/*/src/**` gives 32 hits, all on other carriers: - 30 are a flow node's `config` in the app-shell metadata-admin code; - 2 are the chart node's `config` in `plugin-charts`' `ChartRenderer`. - None is on an AI node. - The control `(schema|node|props)\??\.columns\b`, in the same scope, gives 88. - The bracket form gives 0. - **Which tracked files name an AI node type at all:** plugin-ai sources and tests, types sources and tests, cli `known-schema-types.ts` and its ratchet test, the `ai.*` strings in i18n `en.ts`, and changesets. Nothing in `apps/`, `examples/`, `content/` or `skills/` names one. ## H3: teaching census No doc, README, example or skill authors a retired member on an AI node, or a string on one of the three slots. - `packages/plugin-ai/README.md` teaches `onApply`, `onSelect` and `onSubmit` as function props only. - The `apps/console` UI proposal has an `onSelect` hit, but it is a command-palette item, not an AI node. Nothing needed correcting. The README gains the migration material (AGENTS.md #2). ## Pins - **`ai-node-faces-agree-10874.test.ts` (new).** - Type level, read by `tsc -p tsconfig.test.json`: - `assertionRetiredMembersReadAsTombstones`, 7 rows; - `assertionRuntimeSlotsAreCallable`, 3 rows: the exact function types; - `assertionZodFaceRefusesConfigAndContext`, 5 rows; - `assertionLiveMembersKeepTheirTypes`, the control; - 13 `@ts-expect-error` rows: the retired members, plus a STRING on each runtime slot on fresh literals and widened values; - one accepting row: a function on each slot compiles and is callable. - Runtime: - 5 `safeValidateSchema` rows, each asserting code `invalid_type`, path equal to the key, and a message that names the key, `objectui#10874` and "host's own AI service"; - 5 strict-face rows; - the control: each node without them is accepted on both faces. - **`ai-zod-arms-10859.test.ts`.** - `assertionHandlerFacesSettled` (formerly `assertionHandlerDivergence`): the two retired keys read `undefined` on both faces; the three slots read their function types on the TS face and `undefined` on the zod face. - `assertionMemberTypesAgree` excepts exactly `'onSelect' | 'onDismiss'` and `'onSubmit'`, the objectui#6124 exception. It excepts nothing on form assist. - **`component-docs-retired-handler-keys-7340.test.ts`.** - The census goes from 26 to 28, with `ai.ts` at 2. - `onApplySuggestion` and `onRejectSuggestion` join the names no shipped interface declares callable. **Red-then-green method.** The implementation was committed first. Then `packages/types/src/ai.ts` and `packages/types/src/zod/ai.zod.ts` were checked out from each reference ref in turn. A trap restored them from `HEAD` afterwards and proved the restore by blob hash: `RESTORED: git diff HEAD empty; blobs match HEAD`. The on-disk landing was confirmed before each leg. **Leg: base `1c5ee334a2`** (`onSelect?: string` present once). - `tsc` exits 2 with 44 errors. In the new file: 15 TS2344, 13 TS2578, 3 TS2322, 3 TS2349 and 3 TS7006. In `ai-zod-arms-10859.test.ts`: 6 TS2344. In the parity file: 1 TS2322 ledger mismatch. - vitest over the four files: `Tests 12 failed | 85 passed (97)`. **Leg: round-1 head `15ec3ece42`** (`onSelect?: never` present once). - `tsc` exits 2 with 18 errors: the function-type rows and the accepting row fail against `never`, and the parity ledger mismatches. - vitest: `Tests 7 failed | 90 passed (97)`. Red: the 7340 census, and the five rendering-face rows, whose prescription anchor moved to the host's AI service. **Head `ce2958f243`:** see Gates. ## The parity census - The `KnownDrift` ledger holds **48 entries and 87 keys**. That was counted two ways: - by an independent parse of the ledger, which also lists the two AI rows; - by the file's own header pins, green at head. - The figures were 49 / 89 before this change. Only `ai.zod.ts#AIFormAssistSchema` LEFT, with its two keys: both faces refuse them now. - `ai.zod.ts#AIRecommendationsSchema` (`onDismiss`, `onSelect`) and `ai.zod.ts#NLQuerySchema` (`onSubmit`) STAY, unchanged in content and reworded as ordinary objectui#6124 rows. The ratchet section's restatement reads 48, and says those two pairs joined the #6124 class. - `tsc -p packages/types/tsconfig.test.json` exits 0 at head, so the ledger equals the measured drift. - No other ledger row. `config` and `context` are retired on both faces and agree. `EXPECTED_MIRROR_PAIRS` is unchanged. ## Gates, head `ce2958f243` - **vitest:** `pnpm exec vitest run packages/types/ packages/plugin-ai/ packages/cli/` plus 14 script tests gives `Test Files 298 passed (298)` and `Tests 6361 passed (6361)`, with lock verdict `command-exit 0`. That is 284 package files plus these 14: - `check-handler-key-read-sites`; - the text pins over changesets and READMEs: `changeset-polarity-census`, `body-dialect-census`, `dollar-dialect-alias-census`, `check-changeset-claims`, `check-changeset-no-major`, `check-changeset-presence`, `check-changeset-overwrite`, `check-changeset-fixed`, `check-pending-changeset-literals`, `render-changeset-claims-comment`, `doc-version-claims`, `markdown-test-inputs` and `check-control-bytes`. - The package tests that read the parity file, `ai.ts`, `ai.zod.ts` or the plugin-ai README ran inside the package scope, all green: `app-hidden-catalogue-flag-7542`, `combobox-command-optional-6033`, `wrapper-class-declared-7722`, `ai-insights-retired-8800`, `ai-zero-read-members-retired-8178`, `ai-zod-arms-10859`, `component-docs-retired-handler-keys-7340` and cli `registered-types-validate-ratchet-10859`. - **type-check and handler reads:** `pnpm --filter @object-ui/types type-check` and `pnpm check:handler-key-reads` both pass, with lock verdict `command-exit 0`. The handler-reads gate prints: "OK 108 arm(s), 211 registration(s) … 49 judged … every judged read is a declared member of its arm." - **Changeset gates:** presence exits 0 ("6 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/10874-ai-node-faces-agree.md"). `no-major`, `fixed`, `overwrite` (report-only), `claims` (report-only) and `pending-changeset-literals` each exit 0. - **Other gates:** `check-control-bytes` OK; `check-new-cross-file-line-citations` 0 new; `check-type-check-coverage` OK; `check-test-path-roots` 0; `check-doc-links` 0; `check-governed-queue-guard --test` NOT GOVERNED (10 paths). - **CI on round-1 head `15ec3ece42`:** all 43 check-runs are complete. Test shards 1/8, 2/8 and 6/8 are `success`, as are the other test shards, Type Check, Lint, README Export Check and Doc Snippet Type Check. The two coverage jobs were `skipped`. ## Serial - PR objectui#10870 has landed. - Open PR objectui#10888 (objectui#10872) also edits `zod-mirror-parity.test.ts`, adding `EXCLUSIONS` rows only. Round 1 measured `git merge-tree --write-tree` against its head `4679a31209`, and against main `b2683a2c0e`: clean both ways. - This round restores two `KnownDrift` rows next to where they stood, and touches no `EXCLUSIONS` row. ## Acceptance notes 1. **Round 1 made the three runtime slots `?: never`, following the order; the contract review measured that as wrong.** A function on the node IS called. This round types them as the callables the components invoke, per the objectui#6124 per-key rule and the `DetailViewSchema.onBack` precedent. 2. **The runtime refusal's remedy for the `on*` keys.** The shared remedy in `handlerKeyRefusal` reads "Author behaviour as a NODE TYPE … an action:button node". That does not fit these three nodes, which host no child node. - Left unchanged, because the order keeps `handlerKeyRefusal`. - The TypeScript docblocks, the README and the changeset carry the right prescription. - Carrier: none. 3. **`AIConfig`, `AIConfigSchema`, `AIProvider` and `AIModelType` stay exported, but no node member takes them.** Retiring a published type is a separate decision, so this is not filed: it is zero-pull residue. 4. **ObjectUI has no AI-provider API.** The prescription therefore names the host's own AI service. The exported `AIProvider` is a string union, not an API. --- _Generated by [Claude Code](https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 35d68c4 commit c30c8dd

10 files changed

Lines changed: 609 additions & 79 deletions

‎.changeset/10859-ai-node-zod-arms.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,13 @@
1313
- `@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.
1414
- 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.
1515
- ⚠️ 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). An authored string was never a working handler: `onSelect`, `onDismiss` and `onSubmit` are called only as function props a React host passes, and `onApplySuggestion` / `onRejectSuggestion` are read by nothing — delete the key from the document. The TypeScript face's `string` typing is objectui#10874's to correct.
16+
17+
⚠️ **Dated note, 2026-09-28 — the TypeScript face now types the `on*` members per key, and `config` / `context` are refused — objectui#10874.**
18+
Later in this same release the TypeScript face settles the five `on*` members by the objectui#6124
19+
rule, so the `string` typing described above is gone. `onSelect`, `onDismiss` and `onSubmit` are typed
20+
as the callables their components invoke: the objectui#6124 shape, a callable twin beside the named
21+
refusal this entry added. `onApplySuggestion` and `onRejectSuggestion` are `?: never`, so both faces
22+
refuse those two. The same change retires `config` (all three types) and `context` (`ai-form-assist`,
23+
`ai-recommendations`) on both faces, so `safeValidateSchema` — and `objectui validate` — refuses a
24+
document that carries one of them, by name, where the arms this entry added accepted it. The rest of
25+
this entry is kept as the reading of this change; the objectui#10874 entry states what ships.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
'@object-ui/types': minor
3+
---
4+
5+
**BREAKING (authoring)** — the three `@object-ui/plugin-ai` node declarations stop offering members that no runtime honours, and type the handler slots their components call (objectui#10874, ADR-0049, the objectui#6124 per-key rule).
6+
7+
**Clause-②: yes** — authorable members of published `@object-ui/types` declarations narrow. Three members become callable: `string` to a function type (`onSelect`, `onDismiss`, `onSubmit`). `config` and `context` become `never` on both faces, five members across the three declarations. `onApplySuggestion` and `onRejectSuggestion` become `never` on the TypeScript face, and the zod face already refused them. Scored `minor`, not `major`: this repository scores its own breaking changes `minor` and spells the breaking semantics out in the body (`check:changeset-no-major`).
8+
9+
**What changes, on which face.**
10+
11+
| Member | Was on | TypeScript face | zod face |
12+
|---|---|---|---|
13+
| `onSelect`, `onDismiss` | `AIRecommendationsSchema` | CALLABLE: `(item: AIRecommendationItem) => void` (was `string`) | refused by name, unchanged |
14+
| `onSubmit` | `NLQuerySchema` | CALLABLE: `(query: string) => void` (was `string`) | refused by name, unchanged |
15+
| `onApplySuggestion`, `onRejectSuggestion` | `AIFormAssistSchema` | `?: never` (was `string`) | refused by name, unchanged |
16+
| `config` | `AIFormAssistSchema`, `AIRecommendationsSchema`, `NLQuerySchema` | `?: never` (was `AIConfig`) | refused by name (was accepted) |
17+
| `context` | `AIFormAssistSchema`, `AIRecommendationsSchema` | `?: never` (was a string-keyed record) | refused by name (was accepted) |
18+
19+
**Why.**
20+
21+
- `onSelect`, `onDismiss` and `onSubmit` are RUNTIME SLOTS. Each node type is registered to its raw component, `SchemaRenderer` spreads the node's own keys onto the component's props, and `AIRecommendations` and `NLQueryInput` call these keys as functions. So a function on the node was always called, and a string there was called as a function and threw at click. The TypeScript face now declares the callable the component invokes, as objectui#6124 rules for a key a runtime consumer calls. The zod face keeps refusing an authored value, because JSON has no function value.
22+
- `onApplySuggestion` and `onRejectSuggestion` were read by nothing: `AIFormAssist` takes `onApply` and `onRefresh`, so a string there did nothing.
23+
- `config` and `context` were read by nothing in `@object-ui/plugin-ai`. The three components are presentation only and call no model, so a `provider`, `model`, `systemPrompt` or context written on the node did nothing and changed nothing on screen.
24+
- Before this change the zod face refused all five `on*` strings (objectui#10859) while the TypeScript face offered them, so `onSelect: 'x'` passed `tsc` and was then refused by the validator. Now `tsc` refuses the string too.
25+
26+
**Migration.**
27+
28+
- `onSelect`, `onDismiss`, `onSubmit`: pass a function, never a string — either as the component prop of the same name, or on a node you build in TypeScript. `AIRecommendations` and `NLQueryInput` take the same signatures as props, unchanged.
29+
- `onApplySuggestion`: delete it, and use the `onApply` prop of the `AIFormAssist` component. `onRejectSuggestion`: delete it; the component has no reject callback.
30+
- `config`: delete it. Configure the model in your host's own AI service (ObjectUI has no AI-provider API; these components call no model), and hand its output to the node as `suggestions`, `recommendations` or `result`.
31+
- `context`: delete it, and pass the context to your host's own AI service when you ask it for suggestions or recommendations.
32+
33+
```tsx
34+
// before: compiled; `config` did nothing, and the string was called as a function and threw at click
35+
const picks: AIRecommendationsSchema = {
36+
type: 'ai-recommendations',
37+
recommendations,
38+
config: { model: 'gpt-4' },
39+
onSelect: 'openItem',
40+
};
41+
42+
// after
43+
const picks: AIRecommendationsSchema = {
44+
type: 'ai-recommendations',
45+
recommendations,
46+
onSelect: (item) => openItem(item),
47+
};
48+
```
49+
50+
**What does not change.** The renderers are untouched, so a stored document renders exactly as it did: `config` and `context` were ignored and still are, and a function reaching `onSelect`, `onDismiss` or `onSubmit` is still called. What moves is the verdict. `tsc` refuses the four retired keys, on all seven member slots that carried them, and a string on the three slots. The validator refuses `config` and `context` by name, where the arms objectui#10859 added in this same release accepted them. The exported `AIConfig` type and `AIConfigSchema` stay; no node member takes them any more.
51+
52+
⚠️ **The census behind "read by nothing" is the in-repo half.** Every package, app, example, doc and skill in this repository was searched: none reads `config` or `context` off an AI node, and none authors a changed member. Customer applications and published documents outside it were not enumerated. A TypeScript consumer that authored one of these keys gets a compile error naming it.

‎.changeset/8800-retire-ai-insights.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ support types — each has a reader among those three, so none is orphaned. The
4949
seven `?: never` member tombstones on those three schemas are objectui#8178's
5050
retirement and do not move with this one.
5151

52+
⚠️ **Dated note, 2026-09-28 — `AIConfig` no longer has a reader among those three — objectui#10874.**
53+
Later in this same release the `config` members of `AIFormAssistSchema`,
54+
`AIRecommendationsSchema` and `NLQuerySchema` retire as `?: never`, so no node member takes the
55+
shared `AIConfig` type any more; the type itself stays exported. The paragraph above is kept as the
56+
reading of this change; the objectui#10874 entry states what ships.
57+
5258
⚠️ **The zero this rests on is the IN-REPO HALF, and the ruling was taken with
5359
that limit attached.** This repository's tracked files were enumerated and the
5460
spelling occurred exactly once — inside the declaration itself — against lit

‎packages/plugin-ai/README.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,27 @@ is a compile error rather than a silent no-op:
197197
| `autoFill` | `AIFormAssistSchema` | apply the suggestions you want from `onApply` |
198198
| `maxResults` | `AIRecommendationsSchema` | slice `recommendations` before handing it over — **every item is rendered** |
199199

200+
Four more keys went the same way (objectui#10874), on seven member slots. `config` and `context`
201+
were read by nothing: none of these components calls a model, so neither
202+
configured anything. `onApplySuggestion` and `onRejectSuggestion` were typed
203+
`string` and read by nothing either. All four are now refused by the schema
204+
types and by the validator alike:
205+
206+
| Key | Was on | Instead |
207+
|---|---|---|
208+
| `config` | all three schemas | configure the model in your host's own AI service (ObjectUI has no AI-provider API; these components call no model), and hand its output to the node as `suggestions`, `recommendations` or `result` |
209+
| `context` | `AIFormAssistSchema`, `AIRecommendationsSchema` | pass it to your host's own AI service when you ask it for suggestions or recommendations |
210+
| `onApplySuggestion` | `AIFormAssistSchema` | the `onApply` component prop |
211+
| `onRejectSuggestion` | `AIFormAssistSchema` | delete it — the component has no reject callback |
212+
213+
`onSelect` and `onDismiss` (`AIRecommendationsSchema`) and `onSubmit`
214+
(`NLQuerySchema`) were typed `string` too, but the components DO call them:
215+
a string there was called as a function and threw at click. They are now typed
216+
as the callbacks the components invoke — `(item) => void` and
217+
`(query) => void` — so pass a function, either as the component prop of the same
218+
name or on a node you build in TypeScript. The validator still refuses them in
219+
a JSON document, because JSON has no function value.
220+
200221
## Links
201222

202223
- 📚 [Documentation](https://www.objectui.org/docs/plugins/plugin-ai)

0 commit comments

Comments
 (0)