diff --git a/.changeset/action-param-default-value-contract.md b/.changeset/action-param-default-value-contract.md new file mode 100644 index 0000000000..fe99057ba7 --- /dev/null +++ b/.changeset/action-param-default-value-contract.md @@ -0,0 +1,49 @@ +--- +"@objectstack/spec": minor +--- + +fix(spec): an action param's `defaultValue` is validated against the param's own value contract (#6970) + +`ActionParamSchema.defaultValue` was `z.unknown().optional()`, so a default that +could never satisfy its own param was accepted at authoring time with no warning, +prefilled into the dialog control, and refused only at submit — on a field the +user never touched, by a message that named the param but not the author's +default as the cause. + +The default is now checked at parse time through the **same** `valueSchemaFor` +the dispatcher already runs at submit (ADR-0104 D2, `validateActionParams`) — +one rule set, two moments, no second vocabulary. `datetime` was the loudest +instance (`'2026-08-10T15:00'`, a wall clock `datetime-local` happily displays +and `InstantValueSchema` refuses), but the hole was every type: `number` + +`'abc'`, `select` + a non-member, a `multiple` param + a scalar. + +The rejection names the param, its type, the offending literal, and why it +matters: + +``` +Action param "start" (datetime): the default "2026-08-10T15:00" cannot satisfy +this param's own value contract — expected an ISO-8601 instant with explicit +zone (e.g. 2026-03-15T14:30:00.000Z). The dialog would PREFILL this value and +the submit would then be refused with that same message (ADR-0104 D2), for a +field the user never touched … +``` + +**Acceptance tightening — what is NOT judged.** The gate only answers what the +declaration itself can answer, because an authoring gate that guesses rejects +valid metadata. A param with no `type` of its own keeps an open value shape (the +same default `validateActionParams` applies to an unresolvable type); a +field-backed param that inherits its arity or its option set is not held to +either; and `null` / `''` defaults are skipped exactly as the dispatcher's own +presence check skips them. + +**Stock compatibility.** Already-stored action metadata carrying a nonconforming +default keeps loading and keeps working: the read path (`DatabaseLoader.rowToData`) +replays the ADR-0087 conversion chain but runs no Zod validation, and +`MetadataManager.validate` is deliberately a structural check only. Authoritative +spec validation lives on the WRITE path (`protocol.saveMetaItem`) and is surfaced +on reads as the advisory `_diagnostics` envelope — which now reports the +nonconforming default instead of staying silent about it. So this is loud at +authoring, non-fatal at rest, and no conversion is owed: there is no mechanical +rewrite for "the author meant some other instant", and inventing one would pick a +timezone the metadata never declared (the ambiguity #5061 refused to resolve +consumer-side). diff --git a/packages/spec/api-surface/ui.json b/packages/spec/api-surface/ui.json index d53ff887f8..122b21ace4 100644 --- a/packages/spec/api-surface/ui.json +++ b/packages/spec/api-surface/ui.json @@ -389,6 +389,7 @@ "diagnoseViewMetadata (function)", "expandViewContainer (function)", "expandViewContainerWithDiagnostics (function)", + "isActionParamValuePresent (function)", "isAggregatedViewContainer (function)", "isRecordContextBlockType (function)", "normalizeFilterOperator (function)", diff --git a/packages/spec/export-origins/ui.json b/packages/spec/export-origins/ui.json index 9f93d415b1..02d8a9e6c3 100644 --- a/packages/spec/export-origins/ui.json +++ b/packages/spec/export-origins/ui.json @@ -389,6 +389,7 @@ "diagnoseViewMetadata": "src/ui/view.zod.ts#diagnoseViewMetadata (function)", "expandViewContainer": "src/ui/view.zod.ts#expandViewContainer (function)", "expandViewContainerWithDiagnostics": "src/ui/view.zod.ts#expandViewContainerWithDiagnostics (function)", + "isActionParamValuePresent": "src/ui/action-params.zod.ts#isActionParamValuePresent (function)", "isAggregatedViewContainer": "src/ui/view.zod.ts#isAggregatedViewContainer (function)", "isRecordContextBlockType": "src/ui/react-blocks.ts#isRecordContextBlockType (function)", "normalizeFilterOperator": "src/ui/view.zod.ts#normalizeFilterOperator (function)", diff --git a/packages/spec/src/ui/action-param-default-value.test.ts b/packages/spec/src/ui/action-param-default-value.test.ts new file mode 100644 index 0000000000..a5d61e3dff --- /dev/null +++ b/packages/spec/src/ui/action-param-default-value.test.ts @@ -0,0 +1,220 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * #6970 — an action param's `defaultValue` must satisfy the param's OWN value + * contract at AUTHORING time, checked through the same `valueSchemaFor` the + * dispatcher runs at submit (ADR-0104 D2). + * + * Before this, `defaultValue` was `z.unknown().optional()`: a default that + * could never satisfy its own param parsed clean, prefilled the control, and + * 400'd at submit on a field the user never touched. + */ + +import { describe, it, expect } from 'vitest'; + +import { ActionParamSchema } from './action.zod'; +import { validateActionParams } from './action-params.zod'; +import { getMetadataTypeSchema } from '../kernel/metadata-type-schemas'; + +/** Parse a param and return its first `defaultValue` issue, or `null`. */ +function defaultValueIssue(param: Record) { + const r = ActionParamSchema.safeParse(param); + if (r.success) return null; + return r.error.issues.find((i) => i.path.join('.') === 'defaultValue') ?? null; +} + +/** What the ADR-0104 D2 dispatcher does with this default AS A SUBMITTED VALUE. */ +function submitIssue(param: Record) { + const issues = validateActionParams( + [{ + name: param.name as string, + type: param.type as string | undefined, + multiple: param.multiple as boolean | undefined, + options: param.options as never, + }], + { [param.name as string]: param.defaultValue }, + ); + return issues[0] ?? null; +} + +/** + * The cases the issue names, plus the deliberate NON-rejections. `accepted` + * is the verdict for BOTH moments — that equivalence is the point (see the + * parity test at the bottom), so the table carries one column, not two. + */ +const CASES: Array<{ label: string; param: Record; accepted: boolean }> = [ + // ── The hole, per type ──────────────────────────────────────────────────── + { + label: "datetime + a wall-clock literal (the issue's example)", + param: { name: 'start', type: 'datetime', defaultValue: '2026-08-10T15:00' }, + accepted: false, + }, + { label: "number + 'abc'", param: { name: 'qty', type: 'number', defaultValue: 'abc' }, accepted: false }, + { + label: 'select + a value not in its own options', + param: { + name: 'tier', + type: 'select', + options: [{ label: 'Gold', value: 'gold' }, { label: 'Silver', value: 'silver' }], + defaultValue: 'platinum', + }, + accepted: false, + }, + { + label: 'multiple: true + a scalar default', + param: { name: 'owners', type: 'user', multiple: true, defaultValue: 'usr_1' }, + accepted: false, + }, + { + label: 'date + a full instant (the mirror of the datetime case)', + param: { name: 'due', type: 'date', defaultValue: '2026-08-10T15:00:00.000Z' }, + accepted: false, + }, + { label: 'boolean + a string', param: { name: 'notify', type: 'boolean', defaultValue: 'yes' }, accepted: false }, + { + label: 'lookup + an embedded record object instead of an id', + param: { name: 'owner', type: 'lookup', reference: 'sys_user', defaultValue: { id: 'usr_1', name: 'Ada' } }, + accepted: false, + }, + + // ── Valid defaults of each type: untouched ──────────────────────────────── + { + label: 'VALID datetime (ISO instant with zone)', + param: { name: 'start', type: 'datetime', defaultValue: '2026-08-10T15:00:00.000Z' }, + accepted: true, + }, + { label: 'VALID number', param: { name: 'qty', type: 'number', defaultValue: 7 }, accepted: true }, + { + label: 'VALID select member', + param: { name: 'tier', type: 'select', options: [{ label: 'Gold', value: 'gold' }], defaultValue: 'gold' }, + accepted: true, + }, + { + label: 'VALID multiple array', + param: { name: 'owners', type: 'user', multiple: true, defaultValue: ['usr_1'] }, + accepted: true, + }, + { label: 'VALID date', param: { name: 'due', type: 'date', defaultValue: '2026-08-10' }, accepted: true }, + { label: 'VALID boolean', param: { name: 'notify', type: 'boolean', defaultValue: true }, accepted: true }, + { + label: 'json — an explicitly OPEN value contract, so any default rides', + param: { name: 'blob', type: 'json', defaultValue: { anything: ['at', 'all'] } }, + accepted: true, + }, + { + label: 'no `type` — the value shape is unresolvable, so it stays open', + param: { name: 'loose', defaultValue: 'whatever' }, + accepted: true, + }, + + // ── Presence parity: the dispatcher treats these as ABSENT ──────────────── + { + label: "empty-string default on a datetime — ABSENT at submit, so not judged here either", + param: { name: 'start', type: 'datetime', defaultValue: '' }, + accepted: true, + }, + { + label: 'null default on a number — ABSENT at submit', + param: { name: 'qty', type: 'number', defaultValue: null }, + accepted: true, + }, +]; + +describe('#6970 ActionParamSchema.defaultValue — authored defaults meet the param value contract', () => { + for (const { label, param, accepted } of CASES) { + it(`${accepted ? 'accepts' : 'rejects'}: ${label}`, () => { + const issue = defaultValueIssue(param); + if (accepted) { + expect(issue).toBeNull(); + return; + } + // Rejection pin: this is a pure Zod parse (no error envelope), so the + // assertion set is issue PATH + message shape — never a bare + // `success === false`, which cannot tell this rejection from the + // schema refusing the param for some unrelated reason. + expect(issue).not.toBeNull(); + expect(issue!.path).toEqual(['defaultValue']); + expect(issue!.code).toBe('custom'); + // Names the param, its type, and the offending literal — the three + // things the submit-time 400 could not say. + expect(issue!.message).toContain(`"${param.name as string}"`); + expect(issue!.message).toContain(`(${param.type as string})`); + expect(issue!.message).toContain(JSON.stringify(param.defaultValue)); + }); + } + + it("names the author's default as the cause, not just the param", () => { + const issue = defaultValueIssue({ name: 'start', type: 'datetime', defaultValue: '2026-08-10T15:00' })!; + // The underlying reason is carried verbatim from the shared value contract, + // so authoring and submit read identically. + expect(issue.message).toContain('expected an ISO-8601 instant with explicit zone'); + expect(issue.message).toContain('cannot satisfy this param'); + expect(issue.message).toContain('PREFILL'); + }); + + it('reports through the real authoring door with a full params path', () => { + const r = getMetadataTypeSchema('action')!.safeParse({ + name: 'schedule_visit', + label: 'Schedule Visit', + type: 'script', + params: [ + { name: 'note', type: 'text' }, + { name: 'start', type: 'datetime', defaultValue: '2026-08-10T15:00' }, + ], + }); + expect(r.success).toBe(false); + const paths = r.error!.issues.map((i) => i.path.join('.')); + expect(paths).toContain('params.1.defaultValue'); + }); + + // ── The design's two deliberate non-rejections ──────────────────────────── + it('does NOT judge arity it cannot know — a field-backed param inherits `multiple`', () => { + // `{ field: 'owners' }` inherits `multiple: true` from the referenced + // field, which is invisible at parse time. Rejecting this array would be + // the authoring gate guessing, and guessing wrong rejects valid metadata. + expect(defaultValueIssue({ field: 'owners', type: 'user', defaultValue: ['usr_1', 'usr_2'] })).toBeNull(); + // The scalar spelling of the same inherited-arity param is equally legal. + expect(defaultValueIssue({ field: 'owners', type: 'user', defaultValue: 'usr_1' })).toBeNull(); + }); + + it('still judges arity when the param STATES it, field-backed or not', () => { + // `multiple` declared → the declaration answers the question, so it binds. + expect(defaultValueIssue({ field: 'owners', type: 'user', multiple: true, defaultValue: 'usr_1' })) + .not.toBeNull(); + // Inline (no `field`) → nothing to inherit from, so silence means scalar. + expect(defaultValueIssue({ name: 'owners', type: 'user', defaultValue: ['usr_1'] })).not.toBeNull(); + }); + + it('does NOT judge option membership it cannot know — inherited option sets stay open', () => { + // No inline `options`: the set comes from the referenced field, so + // `valueSchemaFor` degrades to free-form and any string default rides. + expect(defaultValueIssue({ field: 'tier', type: 'select', defaultValue: 'gold' })).toBeNull(); + }); + + it('leaves params WITHOUT a defaultValue completely untouched', () => { + for (const type of ['datetime', 'number', 'select', 'user', 'date', 'boolean']) { + expect(ActionParamSchema.safeParse({ name: 'x', type }).success).toBe(true); + } + }); + + /** + * The ruling's actual claim: `defaultValue` goes through the SAME + * `valueSchemaFor` machinery the dispatcher uses — no second rule set. This + * is the pin that would catch a future edit re-implementing the check by + * hand, which is how the two ends drift into two dialects. + */ + it('agrees with the dispatcher on every case — one rule set, two moments', () => { + for (const { label, param } of CASES) { + // Arity/membership the AUTHORING side deliberately cannot resolve are + // excluded: the dispatcher is fed the RESOLVED param, so for those rows + // the two sides are answering different questions by design. + if (param.field !== undefined) continue; + const authoringRejects = defaultValueIssue(param) !== null; + const submitRejects = submitIssue(param) !== null; + expect( + { case: label, authoringRejects }, + `authoring and submit must agree for: ${label}`, + ).toEqual({ case: label, authoringRejects: submitRejects }); + } + }); +}); diff --git a/packages/spec/src/ui/action-params.zod.ts b/packages/spec/src/ui/action-params.zod.ts index 47abc07bc0..4040ee8d14 100644 --- a/packages/spec/src/ui/action-params.zod.ts +++ b/packages/spec/src/ui/action-params.zod.ts @@ -114,10 +114,24 @@ const BUILTIN_PARAM_ORIGINS: ReadonlyMap = new Map([ /** Fallback origin sentence for a built-in supplied via `opts.builtinKeys`. */ const GENERIC_BUILTIN_ORIGIN = 'the dispatcher supplies it.'; -function isPresent(v: unknown): boolean { +/** + * Whether a value counts as PRESENT for action-param purposes — the one + * definition of "there is a value here to check". + * + * Exported because the AUTHORING gate on `ActionParamSchema.defaultValue` + * (#6970) must skip exactly what this dispatch path skips. An authored default + * of `null` or `''` never reaches {@link valueSchemaFor} at submit — it is + * treated as no value, and `required` decides the outcome — so a parse-time + * check that rejected `''` for not being an ISO instant would be a SECOND rule + * set, stricter than the contract it claims to enforce. Sharing the predicate + * makes that parity structural instead of remembered. + */ +export function isActionParamValuePresent(v: unknown): boolean { return v !== undefined && v !== null && !(typeof v === 'string' && v.trim() === ''); } +const isPresent = isActionParamValuePresent; + /** * The tail appended to an `unknown_field` message when the rejected key is one * leading underscore away from a built-in — `''` when it is not (an ordinary diff --git a/packages/spec/src/ui/action.zod.ts b/packages/spec/src/ui/action.zod.ts index 5b4091cc60..14836efc71 100644 --- a/packages/spec/src/ui/action.zod.ts +++ b/packages/spec/src/ui/action.zod.ts @@ -3,6 +3,12 @@ import { z } from 'zod'; import { retiredKey } from '../shared/retired-key'; import { FieldType } from '../data/field.zod'; +// #6970 — the authoring gate on `defaultValue` runs the SAME value contract the +// dispatcher runs at submit. Imported file-directly (never via a barrel): +// `field-value.zod` reaches only `shared/` + `data/`, and `action-params.zod` +// only `data/` + `api/` + `shared/`, so neither can close a cycle back to `ui/`. +import { MULTI_CAPABLE_TYPES, isMultiValueField, valueSchemaFor } from '../data/field-value.zod'; +import { isActionParamValuePresent } from './action-params.zod'; import { SnakeCaseIdentifierSchema } from '../shared/identifiers.zod'; import { ExpressionInputSchema } from '../shared/expression.zod'; import { I18nLabelSchema, AriaPropsSchema } from './i18n.zod'; @@ -316,7 +322,19 @@ export const ActionParamSchema = lazySchema(() => strictObject( placeholder: z.string().optional(), /** Help/description override. */ helpText: z.string().optional(), - /** Default value for the dialog input. */ + /** + * Default value for the dialog input — prefilled into the control when the + * dialog opens, and SUBMITTED VERBATIM if the user does not touch the field + * (objectui `ActionParamDialog` seeds its state with `p.defaultValue` and + * resolves no tokens against it). + * + * Because it is submitted verbatim it must satisfy this param's own value + * contract — the shape checked below through the SAME `valueSchemaFor` the + * dispatcher runs at submit. This is a LITERAL, not an expression surface: + * unlike a FIELD's `defaultValue`, which the ObjectQL engine resolves for + * runtime tokens (`current_user`, CEL `today()`; ROADMAP §M9.9b), nothing on + * the action-param path interprets this value. + */ defaultValue: z.unknown().optional(), /** * Widget config for inline params (field-backed params inherit these from @@ -383,7 +401,62 @@ export const ActionParamSchema = lazySchema(() => strictObject( message: 'ActionParam with type "lookup"/"master_detail" requires "reference" (the target object) when declared inline — without it the param dialog degrades to a raw record-id text input. Set `reference: \'\'`, or use a field-backed param (`{ field: \'\' }`) to inherit it.', }, -).transform((p, ctx) => lowerRequiresFeature(p, ctx))); +).superRefine((p, ctx) => { + // #6970 — an authored `defaultValue` is checked against the param's OWN + // declared value contract, through the SAME `valueSchemaFor` the dispatcher + // runs at submit (ADR-0104 D2, `validateActionParams`). One rule set, two + // moments: whatever the dispatcher would refuse from a user is refused from + // an AUTHOR, at the moment it is written. + // + // The gap this closes: `defaultValue` was `z.unknown()`, so a default that + // can never satisfy its own param parsed clean, prefilled the control, and + // 400'd at submit on a field the user never touched — with a message naming + // the param but not the author's default as the cause. `datetime` is the + // loudest instance (a human-readable wall clock, `2026-08-10T15:00`, which + // `datetime-local` happily displays and `InstantValueSchema` refuses) but the + // hole was every type: `number` + `'abc'`, `select` + a non-member, a + // `multiple` param + a scalar. That is the "AI writes it wrong in bulk and + // nothing says so" shape ADR-0078 / ADR-0049 exist to prevent. + // + // Checked ONLY where the declaration can answer the question — see the two + // skips below. An authoring gate that guessed at what a field-backed param + // inherits would reject valid metadata, which is worse than the silence it + // replaces. + if (!isActionParamValuePresent(p.defaultValue)) return; + // `type` is the param's own override; absent it is inherited from the + // referenced field at runtime and is not visible here (the same "leaves the + // value shape open" default `validateActionParams` applies to an + // unresolvable type). + if (!p.type) return; + + const def = { type: p.type, multiple: p.multiple, options: p.options }; + const result = valueSchemaFor(def, 'stored').safeParse(p.defaultValue); + if (result.success) return; + + // ARITY is knowable only when the param states it. A field-backed param + // inherits `multiple` from its field, so `{ field: 'owners', type: 'user', + // defaultValue: ['a','b'] }` is a legal declaration whose array default this + // gate must not call wrong. When the param is field-backed AND silent on + // `multiple` AND the type is one whose arity `multiple` decides, accept + // either arity and check only the ELEMENT shape. + if (p.field && p.multiple === undefined && MULTI_CAPABLE_TYPES.has(p.type)) { + const flipped = valueSchemaFor({ ...def, multiple: !isMultiValueField(def) }, 'stored'); + if (flipped.safeParse(p.defaultValue).success) return; + } + + const detail = result.error.issues[0]?.message ?? 'invalid value'; + const key = p.name ?? p.field ?? ''; + ctx.addIssue({ + code: 'custom', + path: ['defaultValue'], + message: + `Action param "${key}" (${p.type}): the default ${JSON.stringify(p.defaultValue)} cannot ` + + `satisfy this param's own value contract — ${detail}. The dialog would PREFILL this value ` + + 'and the submit would then be refused with that same message (ADR-0104 D2), for a field the ' + + 'user never touched — so the 400 names the param but not this default, which is the real ' + + "cause. Write the default in the param's declared value shape, or drop `defaultValue`.", + }); +}).transform((p, ctx) => lowerRequiresFeature(p, ctx))); /** * Action type enum values. diff --git a/skills/objectstack-data/references/_index.md b/skills/objectstack-data/references/_index.md index fb0a57be17..83bb9accae 100644 --- a/skills/objectstack-data/references/_index.md +++ b/skills/objectstack-data/references/_index.md @@ -19,6 +19,7 @@ from `node_modules` — there is no local copy in the skill bundle. ## Transitive dependencies +- `node_modules/@objectstack/spec/src/api/errors.zod.ts` — Standardized Error Codes Protocol - `node_modules/@objectstack/spec/src/automation/flow-function.zod.ts` — The contract for a **named handler function a `script` node invokes** — - `node_modules/@objectstack/spec/src/data/driver.zod.ts` — Common Driver Options - `node_modules/@objectstack/spec/src/data/driver/common.zod.ts` — Shared building blocks for the per-driver `datasource.config` shapes (#4410). @@ -29,6 +30,7 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/data/driver/postgres.zod.ts` — PostgreSQL driver configuration — the `config` slot of a `datasource` whose - `node_modules/@objectstack/spec/src/data/driver/sqlite.zod.ts` — SQLite driver configuration — the `config` slot of a `datasource` whose - `node_modules/@objectstack/spec/src/data/driver/turso.zod.ts` — Turso / libSQL Driver Protocol (#6345). +- `node_modules/@objectstack/spec/src/data/field-value.zod.ts` — Field runtime VALUE-shape contract (ADR-0104 D1). - `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification - `node_modules/@objectstack/spec/src/data/hook-body.zod.ts` — Capability tokens a script body may request. - `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node @@ -40,6 +42,7 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/shared/identifiers.zod.ts` — System Identifier Schema - `node_modules/@objectstack/spec/src/shared/protection.zod.ts` — Package-level metadata protection (ADR-0010 §3.7 — Phase 4.3) - `node_modules/@objectstack/spec/src/shared/suggestions.zod.ts` — "Did you mean?" Suggestion Utilities +- `node_modules/@objectstack/spec/src/ui/action-params.zod.ts` — The action DISPATCH contract: what the platform validates on the way in, and - `node_modules/@objectstack/spec/src/ui/action.zod.ts` — Action Parameter Schema - `node_modules/@objectstack/spec/src/ui/bulk-action.zod.ts` — Bulk Action Schemas - `node_modules/@objectstack/spec/src/ui/chart.zod.ts` — Unified Chart Type Taxonomy diff --git a/skills/objectstack-platform/references/_index.md b/skills/objectstack-platform/references/_index.md index 03b7f573e1..ac907b5636 100644 --- a/skills/objectstack-platform/references/_index.md +++ b/skills/objectstack-platform/references/_index.md @@ -21,6 +21,7 @@ from `node_modules` — there is no local copy in the skill bundle. ## Transitive dependencies +- `node_modules/@objectstack/spec/src/api/errors.zod.ts` — Standardized Error Codes Protocol - `node_modules/@objectstack/spec/src/data/driver/common.zod.ts` — Shared building blocks for the per-driver `datasource.config` shapes (#4410). - `node_modules/@objectstack/spec/src/data/driver/config-registry.zod.ts` — The driver-id → `datasource.config` shape registry (#4410). - `node_modules/@objectstack/spec/src/data/driver/memory.zod.ts` — Memory Driver Configuration Schema @@ -29,6 +30,7 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/data/driver/postgres.zod.ts` — PostgreSQL driver configuration — the `config` slot of a `datasource` whose - `node_modules/@objectstack/spec/src/data/driver/sqlite.zod.ts` — SQLite driver configuration — the `config` slot of a `datasource` whose - `node_modules/@objectstack/spec/src/data/driver/turso.zod.ts` — Turso / libSQL Driver Protocol (#6345). +- `node_modules/@objectstack/spec/src/data/field-value.zod.ts` — Field runtime VALUE-shape contract (ADR-0104 D1). - `node_modules/@objectstack/spec/src/data/field.zod.ts` — Field Type Enum - `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification - `node_modules/@objectstack/spec/src/data/hook-body.zod.ts` — Capability tokens a script body may request. @@ -42,6 +44,7 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/shared/protection.zod.ts` — Package-level metadata protection (ADR-0010 §3.7 — Phase 4.3) - `node_modules/@objectstack/spec/src/shared/suggestions.zod.ts` — "Did you mean?" Suggestion Utilities - `node_modules/@objectstack/spec/src/system/tenant.zod.ts` — Tenant Schema (Multi-Tenant Architecture) +- `node_modules/@objectstack/spec/src/ui/action-params.zod.ts` — The action DISPATCH contract: what the platform validates on the way in, and - `node_modules/@objectstack/spec/src/ui/action.zod.ts` — Action Parameter Schema - `node_modules/@objectstack/spec/src/ui/app.zod.ts` — Base Navigation Item Schema - `node_modules/@objectstack/spec/src/ui/i18n.zod.ts` — Display-label and ARIA-label primitives shared by every `ui/` shape. diff --git a/skills/objectstack-ui/references/_index.md b/skills/objectstack-ui/references/_index.md index 5edc00bb54..c5034313ba 100644 --- a/skills/objectstack-ui/references/_index.md +++ b/skills/objectstack-ui/references/_index.md @@ -23,8 +23,10 @@ from `node_modules` — there is no local copy in the skill bundle. ## Transitive dependencies +- `node_modules/@objectstack/spec/src/api/errors.zod.ts` — Standardized Error Codes Protocol - `node_modules/@objectstack/spec/src/data/date-macros.zod.ts` — Date Macro Tokens — the declarative placeholders the UI substitutes - `node_modules/@objectstack/spec/src/data/feed.zod.ts` — Activity-timeline UI config enums. +- `node_modules/@objectstack/spec/src/data/field-value.zod.ts` — Field runtime VALUE-shape contract (ADR-0104 D1). - `node_modules/@objectstack/spec/src/data/field.zod.ts` — Field Type Enum - `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification - `node_modules/@objectstack/spec/src/data/hook-body.zod.ts` — Capability tokens a script body may request. @@ -36,6 +38,7 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/shared/identifiers.zod.ts` — System Identifier Schema - `node_modules/@objectstack/spec/src/shared/protection.zod.ts` — Package-level metadata protection (ADR-0010 §3.7 — Phase 4.3) - `node_modules/@objectstack/spec/src/shared/suggestions.zod.ts` — "Did you mean?" Suggestion Utilities +- `node_modules/@objectstack/spec/src/ui/action-params.zod.ts` — The action DISPATCH contract: what the platform validates on the way in, and - `node_modules/@objectstack/spec/src/ui/bulk-action.zod.ts` — Bulk Action Schemas - `node_modules/@objectstack/spec/src/ui/i18n.zod.ts` — Display-label and ARIA-label primitives shared by every `ui/` shape. - `node_modules/@objectstack/spec/src/ui/responsive.zod.ts` — Breakpoint Name Enum