diff --git a/.changeset/action-callback-retired-7068.md b/.changeset/action-callback-retired-7068.md new file mode 100644 index 000000000..0ed6b08f8 --- /dev/null +++ b/.changeset/action-callback-retired-7068.md @@ -0,0 +1,77 @@ +--- +'@object-ui/types': minor +--- + +**Breaking for authored metadata:** the legacy `ActionSchema`'s Phase-2 callback +pair — `onSuccess` / `onFailure`, each carrying an `ActionCallback` object — is +RETIRED, and the `ActionCallback` type and its Zod mirror `ActionCallbackSchema` +(with the inferred `ActionCallbackSchemaType`) are DELETED from `@object-ui/types` +and `@object-ui/types/zod` (objectui#7068; maintainer ruling option 1 of +2026-09-05, immediate, no deprecation window; ADR-0049 enforce-or-remove). + +**What an author who wrote the shape sees now.** A `{ type: 'action', … }` +document authoring `onSuccess: { type: 'toast', message: '…' }` (or any +`onFailure` callback) no longer validates: the parse fails loudly on the +`onSuccess` / `onFailure` path (`invalid_type`, expected `never`) with the +explanation and the migration in the message, and the TypeScript members are +`?: never` tombstones so the same document is a `tsc` error at the authoring +site. `import type { ActionCallback } from '@object-ui/types'` and +`import { ActionCallbackSchema } from '@object-ui/types/zod'` fail to resolve. + +**What was measured, on this branch's base (`900f8d99`).** `ActionCallback` +(`{ type: 'toast' | 'message' | 'redirect' | 'reload' | 'custom' | 'ajax' | +'dialog', message?, url?, api?, method?, dialog?, handler? }`) was declared in +`crud.ts`, mirrored in `zod/crud.zod.ts`, re-exported by both barrels, and +carried on the legacy `ActionSchema` as `onSuccess?` / `onFailure?`. Producers: +the package's own `phase2-schemas.test.ts` fixture and three `ts` fences in +`content/docs/core/enhanced-actions.mdx` — nothing else (`git grep -l +ActionCallback` over `packages content skills` hit the five `packages/types` +files; positive control `SchemaNodeSchema` hit 22). Runtime readers: none — +`ActionRunner` imports `UIActionSchema`, never this interface, and its own +`ActionDef.onFailure` is a different (runner-native) meaning. It was the THIRD +meaning of one key: objectui#5934 had already retired the runner's callback +meaning of `onSuccess` and converged it on the spec's block. + +**Why authored JSON that passed publish is unaffected.** `@objectstack/spec`'s +`ActionSchema` (installed pin 17.2.0) already refused the callback shape at +publish — `invalid_type` at `onSuccess.navigate` plus `unrecognized_keys` on the +`onSuccess` block, and `onFailure` refused as an unrecognized key on the action — +so no published or saved metadata could carry it. Only TypeScript code that +typed a callback against the legacy interface, or JSON validated solely through +`@object-ui/types/zod`, meets the new refusal. + +**Where the live meaning lives.** Post-success navigation is the spec's +`onSuccess` block, `{ navigate, openIn }`, declared on `UIActionSchema` +(`ui-action.ts`) and forwarded to the runner (objectui#5934). A success or +failure notice is `successMessage` / `errorMessage` — adjacent keys on the same +legacy `ActionSchema`, NOT retired, and still accepted on both faces. + +**Two published faces, one retirement — tombstone on the keys, deletion of the +type.** `BaseSchema` is `.passthrough()` on the mirror and carries an index +signature on the interface, so DELETING the two keys would have ADMITTED an +authored callback unchecked on both faces; they stay declared as `?: never` / +`retirementTombstone()` (the PR #7761 / #7769 shape) and the base-vs-extended +contrast is pinned. The standalone `ActionCallback` / `ActionCallbackSchema` have +no such escape hatch and are deleted outright, the route objectui#7664 / PR #7743 +took for the `DeclarativeKanban*` trio; the parity ledger drops the pair +(`EXPECTED_MIRROR_PAIRS` 159 → 158) and the absence is pinned in +`action-callback-retired-7068.test.ts`. + +**Docs, same change.** `content/docs/core/enhanced-actions.mdx` — the three +`onSuccess` / `onFailure` fences author `successMessage` / `errorMessage` +instead, and the "Callbacks" section is a "Post-success behaviour" note pointing +at the spec block (no fence: the legacy type carries no spec-derived block). +`content/docs/guide/schema-overview.md` — the fragment line, the feature bullet +and the checklist row are rewritten to the truth (the ✅ claim is now a +retirement note). + +**Migration:** delete `onSuccess` / `onFailure` from any legacy `ActionSchema` +document or fixture; write `successMessage` / `errorMessage` for notices, put +follow-up work in `chain`, and author post-success navigation as the spec's +`onSuccess: { navigate, openIn }` block on `UIActionSchema`. + +Graded `minor`, not `patch`: this narrows the accepted input set on both faces +and removes two exports, which is breaking for any consumer who wrote the shape. +It is not `major` per this repo's fixed-group convention (objectui's own breaking +changes ship as `minor`; the group's major tracks `@objectstack` — AGENTS.md +版本号策略, mechanically enforced by `scripts/check-changeset-no-major.mjs`). diff --git a/content/docs/core/enhanced-actions.mdx b/content/docs/core/enhanced-actions.mdx index 208716ebb..21f1c1a46 100644 --- a/content/docs/core/enhanced-actions.mdx +++ b/content/docs/core/enhanced-actions.mdx @@ -1,6 +1,6 @@ --- title: "Enhanced Actions" -description: "Advanced action system with AJAX calls, chaining, conditions, and callbacks" +description: "Advanced action system with AJAX calls, chaining, conditions, and tracking" --- import { SchemaExample } from '@/app/components/ComponentDemo'; @@ -15,7 +15,7 @@ The enhanced `ActionSchema` provides: - **New action types**: `ajax`, `confirm`, `dialog` - **Action chaining**: Execute multiple actions sequentially or in parallel - **Conditional execution**: a `condition` predicate gates whether an action runs -- **Callbacks**: Success and failure handlers +- **Notices**: `successMessage` / `errorMessage` strings - **Tracking**: Event logging and analytics - **Retry logic**: Automatic retry with configurable backoff @@ -54,14 +54,8 @@ const ajaxAction: ActionSchema = { data: { filter: 'active' }, - onSuccess: { - type: 'toast', - message: 'Data loaded successfully' - }, - onFailure: { - type: 'message', - message: 'Failed to load data' - } + successMessage: 'Data loaded successfully', + errorMessage: 'Failed to load data' }; ``` @@ -302,47 +296,25 @@ for an either/or. > diagnostic at runtime. The shape is now refused by `ActionSchema`'s zod schema > instead of being accepted and ignored. -## Callbacks - -Handle success and failure scenarios: +## Post-success behaviour -```ts -import type { ActionSchema } from '@object-ui/types'; +This legacy `ActionSchema` has no callback slot. The Phase-2 pair it used to +declare — `onSuccess` / `onFailure` carrying an `ActionCallback` object +(`{ type: 'toast' | 'message' | 'redirect' | 'reload' | 'custom' | 'ajax' | 'dialog', message, url, api, … }`) +— was RETIRED (objectui#7068): nothing ever read it (the runner never consumed +the shape), and `@objectstack/spec`'s `ActionSchema` refuses it at publish. Both +keys are `never` on the TypeScript face and named refusals on the Zod mirror, so an +authored callback fails at the authoring site with the migration in the message. -const actionWithCallbacks: ActionSchema = { - type: 'action', - label: 'Submit', - actionType: 'ajax', - api: '/api/submit', - method: 'POST', - - onSuccess: { - type: 'toast', - message: 'Submitted successfully!' - }, - - onFailure: { - type: 'dialog', - dialog: { - type: 'dialog', - title: 'Submission Failed', - content: { - type: 'text', - content: 'Please try again or contact support.' - } - } - } -}; -``` +What to write instead: -**Callback Types:** -- `toast` - Show toast notification -- `message` - Show message dialog -- `redirect` - Navigate to URL -- `reload` - Reload data -- `ajax` - Execute another API call -- `dialog` - Open dialog -- `custom` - Custom handler +- **A notice** — `successMessage` / `errorMessage`, plain strings (the runner + surfaces `successMessage` as a toast after a successful action). +- **Post-success navigation** — the spec's `onSuccess` block, `{ navigate, openIn }`, + declared on `UIActionSchema` and forwarded to the runner (objectui#5934). It is + a spec key, not a member of this legacy type, so it is not shown in a fence here. +- **Follow-up work** — `chain` (see [Action Chaining](#action-chaining)): declared + actions, not callbacks. ## Action Tracking @@ -446,22 +418,9 @@ const complexAction: ActionSchema = { ], chainMode: 'sequential', - // Callbacks - onSuccess: { - type: 'toast', - message: 'Order processed successfully!' - }, - onFailure: { - type: 'dialog', - dialog: { - type: 'dialog', - title: 'Order Processing Failed', - content: { - type: 'text', - content: 'Unable to process order. Please try again.' - } - } - }, + // Notices + successMessage: 'Order processed successfully!', + errorMessage: 'Unable to process order. Please try again.', // Tracking tracking: { @@ -514,7 +473,7 @@ Enhanced Actions are ideal for: - **API integration** - Connect to backend services and external APIs - **Multi-step processes** - Execute complex workflows with multiple stages -- **Form submissions** - Handle form data with validation and callbacks +- **Form submissions** - Handle form data with validation and chained follow-up actions - **Confirmation dialogs** - Add safety checks for critical operations - **Event tracking** - Monitor user interactions for analytics - **Batch operations** - Process multiple items in sequence or parallel @@ -522,7 +481,7 @@ Enhanced Actions are ideal for: ## Best Practices 1. **Use confirm for destructive actions** - Always confirm delete, archive, etc. -2. **Provide clear feedback** - Use callbacks to inform users of success/failure +2. **Provide clear feedback** - Set `successMessage` / `errorMessage` so users learn what happened 3. **Chain related operations** - Group logically related API calls 4. **Track important events** - Enable tracking for business-critical actions 5. **Set appropriate timeouts** - Don't let users wait indefinitely diff --git a/content/docs/guide/schema-overview.md b/content/docs/guide/schema-overview.md index 288d00fbc..c47e96b4a 100644 --- a/content/docs/guide/schema-overview.md +++ b/content/docs/guide/schema-overview.md @@ -70,9 +70,9 @@ turns it into the CSS variables your components already read. ### Advanced Actions #### [Enhanced Actions](/docs/core/enhanced-actions) -Powerful action system with AJAX calls, chaining, conditions, and callbacks. +Powerful action system with AJAX calls, chaining, conditions, and tracking. - + ```typescript const action: ActionSchema = { @@ -81,7 +81,6 @@ const action: ActionSchema = { api: '/api/submit', chain: [...], condition: '${...}', - onSuccess: {...}, tracking: {...} }; ``` @@ -94,7 +93,7 @@ const action: ActionSchema = { **Key Features:** - Action chaining (sequential/parallel) - Conditional execution (a `condition` predicate gates whether an action runs) -- Success/failure callbacks +- Success / failure notices (`successMessage` / `errorMessage`) - Event tracking - Retry logic @@ -274,7 +273,7 @@ The `ActionSchema` provides comprehensive action handling: - ✅ Action types: `ajax`, `confirm`, `dialog` - ✅ Action chaining via the `chain` array (sequential or parallel) - ✅ Conditional execution with the `condition` property -- ✅ Success/failure callbacks: `onSuccess` and `onFailure` +- ❌ Success/failure callbacks: `onSuccess` / `onFailure` were RETIRED (objectui#7068) — both faces refuse them; write `successMessage` / `errorMessage` for notices, and the spec's `onSuccess` block `{ navigate, openIn }` on `UIActionSchema` for post-success navigation (objectui#5934) - ✅ Event tracking with the `tracking` configuration - ✅ Automatic retry logic @@ -300,7 +299,7 @@ ObjectUI includes enhanced view components: 3. **Set up theming** - Hand a `Theme` document to `ThemeProvider` for consistent styling (optional) -4. **Implement actions** - Use advanced action features like `confirm` and callbacks +4. **Implement actions** - Use advanced action features like `confirm` and chaining 5. **Test your application** - Verify all functionality works as expected diff --git a/packages/types/src/__tests__/action-callback-retired-7068.test.ts b/packages/types/src/__tests__/action-callback-retired-7068.test.ts new file mode 100644 index 000000000..444baf2e0 --- /dev/null +++ b/packages/types/src/__tests__/action-callback-retired-7068.test.ts @@ -0,0 +1,360 @@ +/** + * 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. + */ + +/** + * Retirement pin — the legacy `ActionSchema`'s Phase-2 `onSuccess` / `onFailure` + * callback keys are REFUSED on both published faces, and the `ActionCallback` / + * `ActionCallbackSchema` names are GONE (objectui#7068, ADR-0049 enforce-or-remove; + * maintainer ruling option 1, 2026-09-05, immediate, no deprecation window). + * + * ## What was measured before the retirement (on `900f8d99`) + * + * `crud.ts` declared `ActionCallback` (`{ type: 'toast' | 'message' | 'redirect' | + * 'reload' | 'custom' | 'ajax' | 'dialog', message?, url?, api?, method?, dialog?, + * handler? }`) and the legacy `ActionSchema` carried it on `onSuccess?` / + * `onFailure?`; `zod/crud.zod.ts` mirrored both. Producers: this package's own + * `phase2-schemas.test.ts` fixture and three `ts` fences in + * `content/docs/core/enhanced-actions.mdx` — nothing else (`git grep -l + * ActionCallback` over `packages content skills` hit the five `packages/types` + * files; positive control `SchemaNodeSchema` hit 22). Readers: none — + * `ActionRunner` imports `UIActionSchema`, never this interface, and its own + * `ActionDef.onFailure` is the SECOND meaning of the key (objectui#5934 retired the + * runner's callback meaning of `onSuccess` and converged it on the spec block). + * This was the THIRD meaning. + * + * ## Why a tombstone on the keys, and a deletion of the type + * + * `BaseSchema` is `.passthrough()` on the mirror and carries `[key: string]: any` + * on the interface, so DELETING the two keys would ADMIT an authored callback + * unchecked on both faces — kept, inert, and green. The keys therefore stay + * declared as `?: never` / `retirementTombstone()` (the PR #7761 / #7769 shape), + * and the base-vs-extended contrast is measured below on both faces. The + * standalone `ActionCallback` type and its mirror have no such escape hatch and + * are DELETED outright — the route objectui#7664 / PR #7743 took for the + * `DeclarativeKanban*` trio — and their absence is pinned. + * + * ## The comparison pin — the two doors agree + * + * `@objectstack/spec`'s `ActionSchema` (through the installed pin) refuses the same + * callback shape at `onSuccess.navigate` + `unrecognized_keys`, refuses `onFailure` + * as an unrecognized key, and ACCEPTS `{ navigate, openIn }`. An author now meets + * the same answer at the authoring site and at publish. + * + * The `@ts-expect-error` directives are REAL enforcement: this package type-checks + * its tests through `tsconfig.test.json`, so re-widening a key fails the build on + * the unused directive. A green `vitest` run is NOT evidence about them — type + * assertions are erased before it runs. + */ + +import { readFileSync, readdirSync, statSync } from 'node:fs'; +import { join, relative, resolve } from 'node:path'; +import { describe, it, expect } from 'vitest'; +import ts from 'typescript'; +import { ActionSchema as SpecActionSchema } from '@objectstack/spec/ui'; +import * as crudZod from '../zod/crud.zod.js'; +import * as zodBarrel from '../zod/index.zod.js'; +import { ActionSchema, DetailSchema } from '../zod/crud.zod.js'; +import { BaseSchema } from '../zod/base.zod.js'; +import { safeValidateSchema } from '../zod/index.zod.js'; +import type { ActionSchema as ActionSchemaTS } from '../crud.js'; +import type { BaseSchema as BaseSchemaTS } from '../base.js'; + +const ROOT = resolve(__dirname, '../../../..'); +const CRUD_TS = resolve(__dirname, '../crud.ts'); + +/** + * The FULL guidance strings, pinned as literals so the derived assertions below + * cannot all drift together. The first sentence of each is the contract an author + * acts on: the retired key, and where the live meaning lives. + */ +const ON_SUCCESS_GUIDANCE = + 'RETIRED (objectui#7068) — `onSuccess` is no longer part of this legacy ActionSchema; nothing reads ' + + 'it. It carried a Phase-2 `ActionCallback` object (`{ type: \'toast\' | \'message\' | \'redirect\' | ' + + '\'reload\' | \'custom\' | \'ajax\' | \'dialog\', message?, url?, api?, method?, dialog?, handler? }`) that ' + + 'no renderer or runner ever consumed — the THIRD meaning of this key — and that `@objectstack/spec`\'s ' + + 'ActionSchema refuses at publish (`invalid_type` at `onSuccess.navigate` plus `unrecognized_keys`). ' + + 'Post-success navigation is the spec\'s `onSuccess` block, `{ navigate, openIn }`, declared on ' + + 'UIActionSchema (objectui#5934); a success notice is `successMessage`. Retired under ADR-0049 ' + + 'enforce-or-remove with no deprecation window (maintainer ruling option 1, 2026-09-05).'; +const ON_FAILURE_GUIDANCE = + 'RETIRED (objectui#7068) — `onFailure` is no longer part of this legacy ActionSchema; nothing reads ' + + 'it. It carried the same Phase-2 `ActionCallback` object `onSuccess` carried, and ' + + '`@objectstack/spec`\'s ActionSchema declares no `onFailure` at all (an authored one is refused at ' + + 'publish as an unrecognized key). A failure notice is `errorMessage`. Retired under ADR-0049 ' + + 'enforce-or-remove with no deprecation window (maintainer ruling option 1, 2026-09-05).'; + +/** The values an author would plausibly have written on the retired keys. */ +const CALLBACKS: readonly [string, unknown][] = [ + ['toast', { type: 'toast', message: 'Data loaded successfully' }], + ['message', { type: 'message', message: 'Failed to load data' }], + ['dialog', { type: 'dialog', dialog: { type: 'dialog', title: 'Failed', content: { type: 'text', content: 'Try again' } } }], + ['redirect', { type: 'redirect', url: '/done' }], + ['empty object', {}], +]; + +const LEGACY_ACTION = { type: 'action', label: 'Load Data', actionType: 'ajax', api: '/api/data', method: 'GET' }; + +type Issue = { code: string; path: PropertyKey[]; message: string; expected?: string; errors?: Issue[][] }; +/** Flatten a union refusal so the arm-level issues are addressable by path. */ +const flatIssues = (issues: Issue[]): Issue[] => + issues.flatMap((i) => (i.code === 'invalid_union' && i.errors ? i.errors.flat().flatMap((e) => flatIssues([e])) : [i])); + +/** The lazy mirror's object shape — `ActionSchema` is `z.lazy(() => BaseSchema.extend(…))`. */ +const actionShape = (): Record => + (ActionSchema as unknown as { _def: { getter: () => { shape: Record } } })._def.getter().shape; + +/* ── the Zod half: refused BY NAME, with the guidance in the message ─────── */ + +describe.each([ + ['onSuccess', ON_SUCCESS_GUIDANCE], + ['onFailure', ON_FAILURE_GUIDANCE], +] as const)('legacy ActionSchema.%s is RETIRED — the Zod half of the tombstone (objectui#7068)', (key, guidance) => { + it.each(CALLBACKS)('REFUSES a `%s` callback, naming the retired key in the path and carrying the guidance', (_label, value) => { + // The pin. Before the retirement this document parsed GREEN (`ActionCallbackSchema + // .optional()`). Asserting the ENVELOPE — code, path, expected and the exact + // message — so the pin cannot be satisfied by an unrelated rejection, and a + // WELL-FORMED callback so it is the KEY that is refused, not its members. + const result = ActionSchema.safeParse({ ...LEGACY_ACTION, [key]: value }); + expect(result.success, `an authored \`${key}\` was ACCEPTED`).toBe(false); + if (result.success) return; + + const issue = result.error.issues.find((i) => i.path[0] === key); + expect(issue, `parse failed, but not on the \`${key}\` path`).toBeTruthy(); + expect(issue?.code).toBe('invalid_type'); + expect((issue as { expected?: string } | undefined)?.expected).toBe('never'); + expect(issue?.path).toEqual([key]); + expect(issue?.message).toBe(guidance); + expect(issue?.message).not.toContain('Invalid input: expected never'); + }); + + it('is refused at the nested path through a parent that embeds an action — `DetailSchema.actions`', () => { + const result = DetailSchema.safeParse({ type: 'detail', actions: [{ ...LEGACY_ACTION, [key]: CALLBACKS[0][1] }] }); + expect(result.success).toBe(false); + if (result.success) return; + const issue = result.error.issues.find((i) => i.path.join('.') === `actions.0.${key}`); + expect(issue, `no issue at \`actions.0.${key}\`: ${JSON.stringify(result.error.issues.map((i) => i.path))}`).toBeTruthy(); + expect(issue?.code).toBe('invalid_type'); + expect(issue?.message).toBe(guidance); + }); + + it('is refused through `safeValidateSchema` too — the `AnyComponentSchema` union arm carries the tombstone', () => { + const result = safeValidateSchema({ ...LEGACY_ACTION, [key]: CALLBACKS[0][1] }); + expect(result.success).toBe(false); + if (result.success) return; + const hits = flatIssues(result.error.issues as Issue[]).filter((i) => i.path.length === 1 && i.path[0] === key); + expect(hits.length, 'the union door did not surface the tombstone').toBeGreaterThan(0); + for (const hit of hits) expect(hit.message).toBe(guidance); + }); + + it('BASE CONTROL: `BaseSchema` alone ACCEPTS the same document — the extended tombstone shadows the passthrough', () => { + // The trap this pin keeps visible: a DELETION would have fallen through to + // exactly this acceptance, kept the callback unvalidated, and stayed green. + const doc = { ...LEGACY_ACTION, [key]: CALLBACKS[0][1] }; + expect(BaseSchema.safeParse(doc).success).toBe(true); + expect(ActionSchema.safeParse(doc).success).toBe(false); + }); + + it('keeps the key DECLARED, with the same guidance on `.describe()` — one string, both channels', () => { + const shape = actionShape(); + expect(Object.keys(shape)).toContain(key); + expect(shape[key]?.description).toBe(guidance); + }); +}); + +/* ── the retirement narrows exactly the two keys — the neighbours stay legal ── */ + +describe('the retirement narrows exactly `onSuccess` / `onFailure` (objectui#7068)', () => { + it('a legacy action WITHOUT the two keys still parses, and its values survive', () => { + const result = ActionSchema.safeParse(LEGACY_ACTION); + expect(result.success).toBe(true); + if (!result.success) return; + expect(result.data.api).toBe('/api/data'); + }); + + it('control: the adjacent `successMessage` / `errorMessage` — NOT retired — still parse and survive', () => { + const result = ActionSchema.safeParse({ ...LEGACY_ACTION, successMessage: 'Loaded', errorMessage: 'Failed' }); + expect(result.success).toBe(true); + if (!result.success) return; + expect(result.data.successMessage).toBe('Loaded'); + expect(result.data.errorMessage).toBe('Failed'); + }); + + it('control: the mirror did not stop validating — a wrong-typed `successMessage` is still refused', () => { + const result = ActionSchema.safeParse({ ...LEGACY_ACTION, successMessage: 42 }); + expect(result.success).toBe(false); + if (result.success) return; + expect(result.error.issues.map((i) => i.path.join('.'))).toContain('successMessage'); + }); + + it('control: `chain` and `redirect` — the declared follow-up and post-action keys — still parse', () => { + const result = ActionSchema.safeParse({ ...LEGACY_ACTION, chain: [{ type: 'action', label: 'Next' }], redirect: '/done' }); + expect(result.success).toBe(true); + }); + + it('an UNDECLARED key still rides `.passthrough()` — the contrast the tombstones exist for, measured live', () => { + const result = ActionSchema.safeParse({ ...LEGACY_ACTION, notAKeyAtAll: 'anything' }); + expect(result.success).toBe(true); + if (!result.success) return; + expect((result.data as Record).notAKeyAtAll).toBe('anything'); + }); +}); + +/* ── the standalone names are GONE — deletion, not a tombstone (the #7664 route) ── */ + +describe('`ActionCallback` / `ActionCallbackSchema` are DELETED, not tombstoned (objectui#7068, the objectui#7664 route)', () => { + it('`ActionCallbackSchema` is exported from neither `crud.zod.ts` nor the `@object-ui/types/zod` barrel', () => { + expect('ActionCallbackSchema' in crudZod).toBe(false); + expect('ActionCallbackSchema' in zodBarrel).toBe(false); + // Non-vacuity: the neighbours the barrel still carries. + expect('ActionSchema' in crudZod).toBe(true); + expect('ActionExecutionModeSchema' in zodBarrel).toBe(true); + }); + + function topLevelTypeNames(file: string): string[] { + const sf = ts.createSourceFile(file, readFileSync(file, 'utf8'), ts.ScriptTarget.ESNext, false, ts.ScriptKind.TS); + return sf.statements + .filter((s): s is ts.InterfaceDeclaration | ts.TypeAliasDeclaration => ts.isInterfaceDeclaration(s) || ts.isTypeAliasDeclaration(s)) + .map((s) => s.name.text); + } + + it('`crud.ts` declares no `ActionCallback` interface or alias any more — read off the AST, so the docblocks that tell the story do not count', () => { + const names = topLevelTypeNames(CRUD_TS); + expect(names).not.toContain('ActionCallback'); + // Non-vacuity: the reader sees the declarations that ARE there. + expect(names).toContain('ActionSchema'); + expect(names).toContain('ActionExecutionMode'); + }); + + it('neither barrel names it — `index.ts` and `zod/index.zod.ts` carry zero `ActionCallback` tokens', () => { + for (const file of ['../index.ts', '../zod/index.zod.ts']) { + const src = readFileSync(resolve(__dirname, file), 'utf8'); + expect(src.match(/\bActionCallback(Schema)?\b/g) ?? [], `${file} still exports the retired name`).toEqual([]); + expect(src).toMatch(/\bActionExecutionMode(Schema)?\b/); // the neighbour survives + } + }); + + it('the two legacy keys are `?: never` on the interface — tombstones, not deletions (the AST reading #7664 established)', () => { + const sf = ts.createSourceFile(CRUD_TS, readFileSync(CRUD_TS, 'utf8'), ts.ScriptTarget.ESNext, false, ts.ScriptKind.TS); + const decl = sf.statements.find( + (s): s is ts.InterfaceDeclaration => ts.isInterfaceDeclaration(s) && s.name.text === 'ActionSchema', + ); + if (!decl) throw new Error(`no top-level interface ActionSchema in ${CRUD_TS}`); + const members = decl.members.filter(ts.isPropertySignature).map((m) => ({ + name: ts.isIdentifier(m.name) || ts.isStringLiteral(m.name) ? m.name.text : m.name.getText(sf), + never: m.type?.kind === ts.SyntaxKind.NeverKeyword, + })); + const byName = Object.fromEntries(members.map((m) => [m.name, m.never])); + expect(byName.onSuccess).toBe(true); + expect(byName.onFailure).toBe(true); + expect(byName.confirm).toBe(true); // the tombstone that established the convention (objectui#4314) + expect(byName.successMessage).toBe(false); + expect(byName.errorMessage).toBe(false); + }); +}); + +/* ── comparison pin: the spec door agrees ───────────────────────────────── */ + +describe('comparison pin — `@objectstack/spec` ActionSchema refuses the same callback shape (objectui#7068)', () => { + const SPEC_ACTION = { name: 'load', label: 'Load', type: 'script', target: 'loadFn' }; + + it('refuses `onSuccess: { type, message }` at `onSuccess.navigate` and as unrecognized keys on the block', () => { + const result = SpecActionSchema.safeParse({ ...SPEC_ACTION, onSuccess: { type: 'toast', message: 'x' } }); + expect(result.success).toBe(false); + if (result.success) return; + const paths = result.error.issues.map((i) => `${i.code}@${i.path.join('.')}`); + expect(paths).toContain('invalid_type@onSuccess.navigate'); + expect(paths).toContain('unrecognized_keys@onSuccess'); + }); + + it('refuses `onFailure` as an unrecognized key — the spec declares no such key', () => { + const result = SpecActionSchema.safeParse({ ...SPEC_ACTION, onFailure: { type: 'message', message: 'x' } }); + expect(result.success).toBe(false); + if (result.success) return; + const issue = result.error.issues.find((i) => i.code === 'unrecognized_keys' && i.path.length === 0); + expect(issue?.message).toContain('`onFailure`'); + }); + + it('ACCEPTS the live meaning — the `{ navigate, openIn }` block (the shape objectui#5934 converged the runner on)', () => { + const result = SpecActionSchema.safeParse({ ...SPEC_ACTION, onSuccess: { navigate: '/done', openIn: 'self' } }); + expect(result.success).toBe(true); + if (!result.success) return; + expect((result.data as { onSuccess?: unknown }).onSuccess).toEqual({ navigate: '/done', openIn: 'self' }); + }); +}); + +/* ── the docs no longer teach the shape ─────────────────────────────────── */ + +describe('no docs fence authors the retired callback shape any more (objectui#7068)', () => { + const DOCS = resolve(ROOT, 'content/docs'); + const walk = (dir: string): string[] => + readdirSync(dir).flatMap((name) => { + const p = join(dir, name); + return statSync(p).isDirectory() ? walk(p) : /\.mdx?$/.test(name) ? [p] : []; + }); + const CALLBACK_SHAPE = /\bon(Success|Failure):\s*\{\s*(\r?\n\s*)?type\s*:/; + + it('no `onSuccess: { type: … }` / `onFailure: { type: … }` under content/docs — the three fences and the fragment are gone', () => { + const offenders = walk(DOCS).filter((f) => CALLBACK_SHAPE.test(readFileSync(f, 'utf8'))).map((f) => relative(ROOT, f)); + expect(offenders).toEqual([]); + }); + + it('the scan can see the corpus and the pattern can match (non-vacuity)', () => { + expect(walk(DOCS).length).toBeGreaterThan(100); + expect(CALLBACK_SHAPE.test("onSuccess: {\n type: 'toast',")).toBe(true); + expect(CALLBACK_SHAPE.test('onSuccess: { navigate: "/done" }')).toBe(false); + }); +}); + +/* ── the TS half ────────────────────────────────────────────────────────── */ + +describe('legacy ActionSchema.onSuccess / onFailure are RETIRED — the TS half of the tombstone (objectui#7068)', () => { + it('refuses both retired keys at compile time, in the form authors actually write — and beats the inherited index signature', () => { + // On the pre-fix tree both assignments were LEGAL (`ActionCallback | undefined`), + // so each directive would be unused and `tsc -p tsconfig.test.json` fails the + // build with TS2578 naming the line — red before the fix in `type-check`, not + // in vitest. `BaseSchema` carries `[key: string]: any`; a declared `never` + // member wins over it, which is why this is a tombstone and not a deletion. + const retired: ActionSchemaTS = { + type: 'action', + label: 'Load', + // @ts-expect-error — `onSuccess` is RETIRED (objectui#7068): declared `?: never`, so no callback object is authorable. + onSuccess: { type: 'toast', message: 'ok' }, + // @ts-expect-error — `onFailure` is RETIRED (objectui#7068): declared `?: never`. + onFailure: { type: 'message', message: 'no' }, + }; + + // The migrated document — notices as strings, follow-ups as `chain` — still type-checks. + const migrated: ActionSchemaTS = { + type: 'action', + label: 'Load', + successMessage: 'ok', + errorMessage: 'no', + chain: [{ type: 'action', label: 'Next' }], + redirect: '/done', + }; + + // BASE CONTROL on the TS face: the same literal IS a legal `BaseSchema` — the + // acceptance a deleted member would have fallen through to. + const base: BaseSchemaTS = { type: 'action', onSuccess: { type: 'toast', message: 'ok' } }; + + expect([retired, migrated, base]).toHaveLength(3); + }); + + it('refuses them through the indexed member type and through a WIDENED value too', () => { + // @ts-expect-error — `onSuccess` is RETIRED (objectui#7068): the member type is `undefined`, never a callback. + const viaKey: ActionSchemaTS['onSuccess'] = { type: 'toast' }; + + // Excess-property checking only reaches a FRESH literal; the declared `never` + // makes the assignment itself ill-typed, so freshness stops mattering. + const raw = { type: 'action' as const, label: 'Load', onFailure: { type: 'message' as const } }; + // @ts-expect-error — `onFailure` is RETIRED (objectui#7068), reached through a non-fresh value. + const widened: ActionSchemaTS = raw; + + expect([viaKey, widened.type]).toHaveLength(2); + }); +}); diff --git a/packages/types/src/__tests__/component-docs-retired-handler-keys-7340.test.ts b/packages/types/src/__tests__/component-docs-retired-handler-keys-7340.test.ts index bf6525592..716d86d70 100644 --- a/packages/types/src/__tests__/component-docs-retired-handler-keys-7340.test.ts +++ b/packages/types/src/__tests__/component-docs-retired-handler-keys-7340.test.ts @@ -270,15 +270,20 @@ describe('the retired population is measured off the shipped tree (objectui#7340 // 22 from objectui#6124; objectui#7344 (the objectui#6182 string-dialect // ruling, same shape) added `AppAction.onClick`, `ReportBuilderSchema.onSave` // / `.onCancel` and `CRUDDialogSchema.onClose` — a ruled move of the - // population, recorded here rather than waved through. + // population, recorded here rather than waved through. objectui#7068 added + // the legacy `ActionSchema.onSuccess` / `.onFailure` (crud.ts 1 → 3, total + // 26 → 28): NOT #6124 handler keys — they carried a callback OBJECT + // (`ActionCallback`, deleted), the third meaning of `onSuccess` — but the + // same `on*?: never` shape this name-shaped census reads, so the move is + // recorded here too (maintainer ruling option 1, 2026-09-05). const split: Record = {}; for (const m of RETIRED) split[m.file] = (split[m.file] ?? 0) + 1; expect({ total: RETIRED.length, split }).toEqual({ - total: 26, + total: 28, split: { 'app.ts': 1, 'complex.ts': 4, - 'crud.ts': 1, + 'crud.ts': 3, 'data-display.ts': 4, 'feedback.ts': 1, 'form.ts': 8, @@ -305,6 +310,11 @@ describe('the retired population is measured off the shipped tree (objectui#7340 'onColumnAdd', 'onConfirm', 'onExpandChange', + // objectui#7068: the legacy `ActionSchema.onFailure` callback object — no + // shipped interface declares an `onFailure` at all any more. `onSuccess` + // is NOT here: `UIActionSchema.onSuccess` is LIVE (the spec's navigation + // block), so that name stays ambiguous and is resolved by the pair rule. + 'onFailure', 'onSave', 'onSelectChange', 'onSendMessage', diff --git a/packages/types/src/__tests__/phase2-schemas.test.ts b/packages/types/src/__tests__/phase2-schemas.test.ts index 162127579..1305d8de3 100644 --- a/packages/types/src/__tests__/phase2-schemas.test.ts +++ b/packages/types/src/__tests__/phase2-schemas.test.ts @@ -363,14 +363,12 @@ describe('Phase 2: Enhanced ActionSchema Zod Validation', () => { headers: { 'Authorization': 'Bearer token', }, - onSuccess: { - type: 'toast', - message: 'Data loaded successfully', - }, - onFailure: { - type: 'message', - message: 'Failed to load data', - }, + // `onSuccess` / `onFailure` — Phase-2 `ActionCallback` objects — were RETIRED by + // objectui#7068; this fixture was their only in-repo producer. Both faces refuse + // them now, so this accept case authors the notices instead; the refusal and + // its controls are pinned in `action-callback-retired-7068.test.ts`. + successMessage: 'Data loaded successfully', + errorMessage: 'Failed to load data', }; const result = ActionSchema.safeParse(ajaxAction); diff --git a/packages/types/src/__tests__/zod-mirror-parity.test.ts b/packages/types/src/__tests__/zod-mirror-parity.test.ts index e87df8ebc..3d0726477 100644 --- a/packages/types/src/__tests__/zod-mirror-parity.test.ts +++ b/packages/types/src/__tests__/zod-mirror-parity.test.ts @@ -57,7 +57,12 @@ * written-down constant `EXPECTED_MIRROR_PAIRS` by the runtime census at the * bottom of this file (objectui#7433); `assertionRegistryHalvesAgree` already pins * it equal to `keyof Declared`. ⛔ Read the constant, not this sentence — a digit - * here is the artefact that rotted four times. 157 until objectui#7664 retired the + * here is the artefact that rotted four times. 159 until objectui#7068 RETIRED the + * `crud.zod.ts#ActionCallbackSchema` pair — the const and its `ActionCallback` + * declaration both DELETED (the objectui#7664 route for a standalone retired pair; + * the legacy `ActionSchema.onSuccess` / `onFailure` keys that carried it are `never` + * / `retirementTombstone()` on the two faces), a pair with no entry in any ledger, + * so no other count moved; 157 until objectui#7664 retired the * three `DeclarativeKanban*` pairs and registered the five plugin-dialect ones * (`KanbanCardSchema`, `KanbanColumnSchema`, `KanbanSchema`, `CardTemplateSchema`, * `ColumnWidthConfigSchema` — the `'kanban'` arm rewritten to the shape the @@ -224,7 +229,7 @@ import type { z } from 'zod'; import { AppActionSchema, AppComponentSchema, MenuItemSchema as AppMenuItemSchema, NavigationAreaSchema, NavigationItemSchema } from '../zod/app.zod.js'; import { BaseSchema, ComponentConfigSchema, ComponentInputSchema, ComponentMetaSchema, KeyedI18nLabelSchema, SchemaNodeSchema } from '../zod/base.zod.js'; import { CalendarEventSchema, CalendarViewSchema, CarouselItemSchema, CarouselSchema, ChatbotSchema, ChatbotEnhancedSchema, ChatbotFloatingSchema, ChatMessageSchema, ChatMessageSourceSchema, ChatToolInvocationSchema, DashboardComponentSchema, DashboardConfigSchema, DashboardWidgetConfigSchema, DashboardWidgetLayoutSchema, DashboardWidgetSchema, FilterBuilderSchema, FilterFieldSchema, KanbanCardSchema, KanbanColumnSchema, KanbanSchema, CardTemplateSchema, ColumnWidthConfigSchema, FilterBuilderConditionSchema, FilterGroupSchema } from '../zod/complex.zod.js'; -import { ActionCallbackSchema, ActionSchema, CRUDDialogSchema, DetailSchema } from '../zod/crud.zod.js'; +import { ActionSchema, CRUDDialogSchema, DetailSchema } from '../zod/crud.zod.js'; import { AlertSchema, AvatarSchema, BadgeSchema, BarChartSchema, ChartDataSeriesSchema, ChartSchema, DataTableSchema, DrillDownConfigSchema, HtmlSchema, KbdSchema, ListItemSchema, ListSchema, MarkdownSchema, StaticTableColumnSchema, StatisticSchema, TableColumnSchema, TableSchema, TimelineEventSchema, TimelineSchema, TreeNodeSchema, TreeViewSchema } from '../zod/data-display.zod.js'; import { AccordionItemSchema, AccordionSchema, CollapsibleSchema, ToggleGroupItemSchema, ToggleGroupSchema } from '../zod/disclosure.zod.js'; import { EmptySchema, LoadingSchema, ProgressSchema, SkeletonSchema, SonnerSchema, SpinnerSchema, ToasterSchema, ToastSchema } from '../zod/feedback.zod.js'; @@ -240,7 +245,7 @@ import type { AppAction as Ts_AppAction, AppComponentSchema as Ts_AppComponentSc import type { BaseSchema as Ts_BaseSchema, ComponentConfig as Ts_ComponentConfig, ComponentInput as Ts_ComponentInput, ComponentMeta as Ts_ComponentMeta, KeyedI18nLabel as Ts_KeyedI18nLabel } from '../base'; import type { CalendarEvent as Ts_CalendarEvent, CalendarViewSchema as Ts_CalendarViewSchema, CarouselItem as Ts_CarouselItem, CarouselSchema as Ts_CarouselSchema, ChatbotSchema as Ts_ChatbotSchema, ChatbotEnhancedSchema as Ts_ChatbotEnhancedSchema, ChatbotFloatingSchema as Ts_ChatbotFloatingSchema, ChatMessage as Ts_ChatMessage, ChatMessageSource as Ts_ChatMessageSource, ChatToolInvocation as Ts_ChatToolInvocation, DashboardComponentSchema as Ts_DashboardComponentSchema, DashboardWidgetLayout as Ts_DashboardWidgetLayout, DashboardWidgetSchema as Ts_DashboardWidgetSchema, FilterBuilderSchema as Ts_FilterBuilderSchema, FilterField as Ts_FilterField, KanbanCard as Ts_KanbanCard, KanbanColumn as Ts_KanbanColumn, KanbanSchema as Ts_KanbanSchema, CardTemplate as Ts_CardTemplate, ColumnWidthConfig as Ts_ColumnWidthConfig } from '../complex'; import type { DashboardConfig as Ts_DashboardConfig, DashboardWidgetConfig as Ts_DashboardWidgetConfig } from '../designer'; -import type { ActionCallback as Ts_ActionCallback, CRUDDialogSchema as Ts_CRUDDialogSchema, DetailSchema as Ts_DetailSchema } from '../crud'; +import type { CRUDDialogSchema as Ts_CRUDDialogSchema, DetailSchema as Ts_DetailSchema } from '../crud'; import type { AlertSchema as Ts_AlertSchema, AvatarSchema as Ts_AvatarSchema, BadgeSchema as Ts_BadgeSchema, BarChartSchema as Ts_BarChartSchema, ChartDataSeries as Ts_ChartDataSeries, ChartSchema as Ts_ChartSchema, DataTableSchema as Ts_DataTableSchema, DrillDownConfig as Ts_DrillDownConfig, HtmlSchema as Ts_HtmlSchema, KbdSchema as Ts_KbdSchema, ListItem as Ts_ListItem, ListSchema as Ts_ListSchema, MarkdownSchema as Ts_MarkdownSchema, StaticTableColumn as Ts_StaticTableColumn, StatisticSchema as Ts_StatisticSchema, TableColumn as Ts_TableColumn, TableSchema as Ts_TableSchema, TimelineEvent as Ts_TimelineEvent, TimelineSchema as Ts_TimelineSchema, TreeViewSchema as Ts_TreeViewSchema, BreadcrumbItem as Ts_BreadcrumbItem, BreadcrumbSchema as Ts_BreadcrumbSchema } from '../data-display'; import type { AccordionItem as Ts_AccordionItem, AccordionSchema as Ts_AccordionSchema, CollapsibleSchema as Ts_CollapsibleSchema, ToggleGroupItem as Ts_ToggleGroupItem, ToggleGroupSchema as Ts_ToggleGroupSchema } from '../disclosure'; import type { EmptySchema as Ts_EmptySchema, LoadingSchema as Ts_LoadingSchema, ProgressSchema as Ts_ProgressSchema, SkeletonSchema as Ts_SkeletonSchema, SonnerSchema as Ts_SonnerSchema, SpinnerSchema as Ts_SpinnerSchema, ToasterSchema as Ts_ToasterSchema, ToastSchema as Ts_ToastSchema } from '../feedback'; @@ -594,7 +599,6 @@ const MIRRORS = { 'complex.zod.ts#KanbanSchema': KanbanSchema, 'complex.zod.ts#CardTemplateSchema': CardTemplateSchema, 'complex.zod.ts#ColumnWidthConfigSchema': ColumnWidthConfigSchema, - 'crud.zod.ts#ActionCallbackSchema': ActionCallbackSchema, 'crud.zod.ts#CRUDDialogSchema': CRUDDialogSchema, 'crud.zod.ts#DetailSchema': DetailSchema, 'data-display.zod.ts#AlertSchema': AlertSchema, @@ -757,7 +761,6 @@ interface Declared { 'complex.zod.ts#KanbanSchema': Ts_KanbanSchema; 'complex.zod.ts#CardTemplateSchema': Ts_CardTemplate; 'complex.zod.ts#ColumnWidthConfigSchema': Ts_ColumnWidthConfig; - 'crud.zod.ts#ActionCallbackSchema': Ts_ActionCallback; 'crud.zod.ts#CRUDDialogSchema': Ts_CRUDDialogSchema; 'crud.zod.ts#DetailSchema': Ts_DetailSchema; 'data-display.zod.ts#AlertSchema': Ts_AlertSchema; @@ -2293,7 +2296,7 @@ const ZOD_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'zod'); * MINUEND under it had moved. Nothing failed on any of those days, because nothing * compared the registry to a number. objectui#7433 is that absence, not the digits. */ -const EXPECTED_MIRROR_PAIRS = 159; +const EXPECTED_MIRROR_PAIRS = 158; /** This file, so the census can read its own type-level ledgers. */ const SELF = fileURLToPath(import.meta.url); diff --git a/packages/types/src/crud.ts b/packages/types/src/crud.ts index d43c2d63f..30f6e4d5f 100644 --- a/packages/types/src/crud.ts +++ b/packages/types/src/crud.ts @@ -23,39 +23,12 @@ import type { BaseSchema, SchemaNode } from './base.js'; */ export type ActionExecutionMode = 'sequential' | 'parallel'; -/** - * Action callback configuration - */ -export interface ActionCallback { - /** - * Callback type - */ - type: 'toast' | 'message' | 'redirect' | 'reload' | 'custom' | 'ajax' | 'dialog'; - /** - * Message to display - */ - message?: string; - /** - * Redirect URL - */ - url?: string; - /** - * API endpoint for ajax callback - */ - api?: string; - /** - * HTTP method for ajax callback - */ - method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; - /** - * Dialog schema to open - */ - dialog?: SchemaNode; - /** - * Custom callback handler expression - */ - handler?: string; -} +// `ActionCallback` — the Phase-2 callback object the legacy `ActionSchema.onSuccess` / +// `onFailure` keys carried — was DELETED by objectui#7068 (the objectui#7664 route for +// a standalone retired type: name gone from both faces and from the barrels, absence +// pinned). The two keys below stay declared as `?: never` tombstones so an authored +// callback is a `tsc` error at the site instead of a silent index-signature admit; +// `__tests__/action-callback-retired-7068.test.ts` pins both faces. /** * Action button configuration for CRUD operations @@ -158,13 +131,38 @@ export interface ActionSchema extends BaseSchema { */ errorMessage?: string; /** - * Success callback (Phase 2) - */ - onSuccess?: ActionCallback; - /** - * Failure callback (Phase 2) - */ - onFailure?: ActionCallback; + * RETIRED (objectui#7068, ADR-0049 enforce-or-remove) — the Phase-2 success + * callback, an `ActionCallback` object (`{ type: 'toast' | 'message' | 'redirect' + * | 'reload' | 'custom' | 'ajax' | 'dialog', message?, url?, api?, method?, + * dialog?, handler? }`). Measured before the retirement: zero producers outside + * this package's own test fixture and two docs pages, and zero runtime readers — + * `ActionRunner` never consumed this shape (its retired channel wanted full + * `ActionDef`s, and `{ type: 'toast' }` is not a registered action type), and + * `@objectstack/spec`'s `ActionSchema` refuses it at publish (`invalid_type` at + * `onSuccess.navigate` plus `unrecognized_keys` on the block). It was the THIRD + * meaning of this key; objectui#5934 had already converged the runner on the + * spec's post-success block. Maintainer ruling option 1 (2026-09-05), immediate, + * no deprecation window. + * + * Where the live meaning lives: the spec's `onSuccess` block `{ navigate, openIn }`, + * declared on `UIActionSchema` (`ui-action.ts`) and forwarded to the runner. A + * success notice is {@link ActionSchema.successMessage}. `?: never` rather than a + * deletion because {@link BaseSchema} carries an index signature that would ADMIT + * a deleted key unchecked; the zod twin (`zod/crud.zod.ts`) refuses it by name. + * @deprecated Not part of this contract — write the spec's `onSuccess` block on `UIActionSchema`, or `successMessage`. + */ + onSuccess?: never; + /** + * RETIRED (objectui#7068, ADR-0049 enforce-or-remove) — the Phase-2 failure + * callback, the same `ActionCallback` object shape {@link ActionSchema.onSuccess} + * carried. Zero producers, zero runtime readers (measured, see `onSuccess`), and + * `@objectstack/spec`'s `ActionSchema` declares no `onFailure` at all — an + * authored one is refused at publish as an unrecognized key. Maintainer ruling + * option 1 (2026-09-05), immediate, no deprecation window. A failure notice is + * {@link ActionSchema.errorMessage}; the zod twin refuses this key by name. + * @deprecated Not part of this contract — write `errorMessage`. + */ + onFailure?: never; /** * Action chaining - actions to execute after this one (Phase 2) */ diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 57ee75342..98b6c5fad 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -889,7 +889,6 @@ export type { export type { // Enhanced Action System (Phase 2) ActionExecutionMode, - ActionCallback, } from './crud.js'; /** diff --git a/packages/types/src/zod/crud.zod.ts b/packages/types/src/zod/crud.zod.ts index 2cfc2363a..6dd88e95a 100644 --- a/packages/types/src/zod/crud.zod.ts +++ b/packages/types/src/zod/crud.zod.ts @@ -27,18 +27,12 @@ import { handlerKeyRefusal, retirementTombstone } from './tombstone.zod.js'; */ export const ActionExecutionModeSchema = z.enum(['sequential', 'parallel']).describe('Action execution mode for chaining'); -/** - * Action Callback Schema - */ -export const ActionCallbackSchema = z.object({ - type: z.enum(['toast', 'message', 'redirect', 'reload', 'custom', 'ajax', 'dialog']).describe('Callback type'), - message: z.string().optional().describe('Message to display'), - url: z.string().optional().describe('Redirect URL'), - api: z.string().optional().describe('API endpoint for ajax callback'), - method: z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']).optional().describe('HTTP method for ajax callback'), - dialog: SchemaNodeSchema.optional().describe('Dialog schema to open'), - handler: z.string().optional().describe('Custom callback handler expression'), -}); +// `ActionCallbackSchema` — the mirror of the Phase-2 `ActionCallback` object the legacy +// `ActionSchema.onSuccess` / `onFailure` keys carried — was DELETED by objectui#7068 +// (the objectui#7664 route for a standalone retired pair: const, TS declaration and +// barrel exports gone, parity-ledger rows removed, absence pinned in +// `../__tests__/action-callback-retired-7068.test.ts`). The two keys below stay +// declared as named refusals — see their comment. /** * The wire shape of an action's execution gate: a boolean, a bare CEL string @@ -95,8 +89,32 @@ export const ActionSchema: z.ZodType = z.lazy(() => BaseSchema.extend({ }).optional().describe('Dialog configuration (for dialog actions)'), successMessage: z.string().optional().describe('Success message after execution'), errorMessage: z.string().optional().describe('Error message on failure'), - onSuccess: ActionCallbackSchema.optional().describe('Success callback'), - onFailure: ActionCallbackSchema.optional().describe('Failure callback'), + // ADR-0049 RETIREMENT TOMBSTONES (objectui#7068, maintainer ruling option 1 of + // 2026-09-05). Both keys carried a Phase-2 `ActionCallback` object that no renderer + // or runner ever read and that the spec refuses at publish (`onSuccess`: wrong + // block shape; `onFailure`: no such key). A plain deletion here would NOT refuse + // them: `BaseSchema` is `.passthrough()`, so an authored callback would be KEPT + // unvalidated and silently inert. The tombstones refuse BY NAME — one string, both + // channels (parse-time message and `.describe()`), see `./tombstone.zod.ts`; the + // TS twins are `?: never` (`../crud.ts`). Pinned in + // `../__tests__/action-callback-retired-7068.test.ts`. + onSuccess: retirementTombstone( + 'RETIRED (objectui#7068) — `onSuccess` is no longer part of this legacy ActionSchema; nothing reads ' + + 'it. It carried a Phase-2 `ActionCallback` object (`{ type: \'toast\' | \'message\' | \'redirect\' | ' + + '\'reload\' | \'custom\' | \'ajax\' | \'dialog\', message?, url?, api?, method?, dialog?, handler? }`) that ' + + 'no renderer or runner ever consumed — the THIRD meaning of this key — and that `@objectstack/spec`\'s ' + + 'ActionSchema refuses at publish (`invalid_type` at `onSuccess.navigate` plus `unrecognized_keys`). ' + + 'Post-success navigation is the spec\'s `onSuccess` block, `{ navigate, openIn }`, declared on ' + + 'UIActionSchema (objectui#5934); a success notice is `successMessage`. Retired under ADR-0049 ' + + 'enforce-or-remove with no deprecation window (maintainer ruling option 1, 2026-09-05).', + ), + onFailure: retirementTombstone( + 'RETIRED (objectui#7068) — `onFailure` is no longer part of this legacy ActionSchema; nothing reads ' + + 'it. It carried the same Phase-2 `ActionCallback` object `onSuccess` carried, and ' + + '`@objectstack/spec`\'s ActionSchema declares no `onFailure` at all (an authored one is refused at ' + + 'publish as an unrecognized key). A failure notice is `errorMessage`. Retired under ADR-0049 ' + + 'enforce-or-remove with no deprecation window (maintainer ruling option 1, 2026-09-05).', + ), chain: z.array(ActionSchema).optional().describe('Action chaining - actions to execute after this one'), chainMode: ActionExecutionModeSchema.optional().default('sequential').describe('Chain execution mode'), condition: ActionConditionPredicateSchema.optional().describe('Execution gate — the action runs only while this predicate holds'), @@ -184,7 +202,6 @@ export const CRUDComponentSchema = z.union([ * Export type inference helpers */ export type ActionExecutionModeSchemaType = z.infer; -export type ActionCallbackSchemaType = z.infer; export type ActionSchemaType = z.infer; export type DetailSchemaType = z.infer; export type CRUDDialogSchemaType = z.infer; diff --git a/packages/types/src/zod/index.zod.ts b/packages/types/src/zod/index.zod.ts index 0cfb26c67..d7db88be2 100644 --- a/packages/types/src/zod/index.zod.ts +++ b/packages/types/src/zod/index.zod.ts @@ -284,7 +284,6 @@ export { // ============================================================================ export { ActionExecutionModeSchema, - ActionCallbackSchema, ActionSchema, DetailSchema, CRUDDialogSchema, diff --git a/scripts/check-doc-component-types.mjs b/scripts/check-doc-component-types.mjs index 927ad28c2..ddba2f99b 100644 --- a/scripts/check-doc-component-types.mjs +++ b/scripts/check-doc-component-types.mjs @@ -474,7 +474,6 @@ const DOC_TYPE_EXEMPTIONS = { action: 'ActionSchema discriminant. This page documents the action vocabulary end to end, so every ' + '`type: \'action\'` here is an action definition rather than a node.', - message: 'ActionSchema discriminant for the message/toast action under `onFailure`.', }, 'content/docs/core/report-schema.mdx': { line: 'Chart series kind under a report section\'s `chart.series`, not a node type.',