Skip to content

spec: structured form button/defaults config on FormViewSchema + enroll view in the liveness ledger #2998

Description

@os-zhuang

Follow-up to the viewschema property-liveness audit (docs/audits/2026-06-viewschema-property-liveness.md, the framework#1890 line) and its objectui-side resolution (objectstack-ai/objectui#1763#2545, PR objectstack-ai/objectui#2552).

The objectui work reconciled the renderer-side key drift (bulkActions/batchActions, groupssections) and classified every ObjectFormSchema key as [spec-aligned] / [ObjectUI extension] / [runtime-only]. That surfaced a residue that can only be closed here, in the spec.

Problem

ObjectView/ObjectForm in objectui read a family of form keys that do not exist in FormViewSchema (packages/spec/src/ui/view.zod.ts, FormViewSchema L832-929): showSubmit, submitText, showCancel, cancelText, showReset, initialValues (plus renderer-only className, fields, customFields). This repo's own audit already diagnosed it verbatim (docs/audits/2026-06-viewschema-property-liveness.md:25). These are renderer-invented surface with no spec backing — serializable form config that belongs in the protocol but isn't in it.

FormViewSchema is strip-mode (not .strict() — unlike the leaf FormFieldSchema/FormSectionSchema which are .strict() per ADR-0089 D3a, view.zod.ts L773/L810). So today these keys are silently stripped by a spec .parse() — the "parsed → silently inert" shape ADR-0078 prohibits.

Proposal — two tracks

Track A: add structured buttons + defaults to FormViewSchema (additive, non-breaking)

A new optional top-level key alongside submitBehavior (view.zod.ts L912-925). Sketch:

buttons: z.object({
  submit: z.object({ show: z.boolean().optional(), label: I18nLabelSchema.optional() }).optional(),
  cancel: z.object({ show: z.boolean().optional(), label: I18nLabelSchema.optional() }).optional(),
  reset:  z.object({ show: z.boolean().optional(), label: I18nLabelSchema.optional() }).optional(),
}).optional().describe('Form action-button visibility & labels'),

defaults: z.record(z.string(), z.unknown()).optional()
  .describe('Initial field values for create-mode forms'),  // absorbs objectui `initialValues`

Because the container is strip-mode, this is purely additive (minor/patch changeset, no tombstone). The wizard nextText/prevText pair and showStepIndicator (already in spec) can be folded into buttons or left as-is — open question.

⚠️ ADR-0078 constraint (must be resolved before merge): the framework itself has no runtime consumer of FormViewSchema — it's the protocol contract objectui renders. Adding an authorable buttons block that no code reads is itself parsed-but-inert, i.e. the exact thing ADR-0078 forbids. So Track A must land in one of two shapes:

  • (preferred) co-landed with the objectui renderer wiring — objectui's ObjectForm reads schema.buttons.* / schema.defaults instead of its invented showSubmit/submitText/initialValues. Cross-repo, two worktrees.
  • (fallback) marked [EXPERIMENTAL — not enforced] in the .describe() until the objectui side lands, per ADR-0078's escape hatch.

Note submitBehavior itself is currently classified dead (no renderer consumer) in the audit (L20) — worth wiring alongside, or the new block sits next to already-inert surface.

Track B: enroll view in the liveness ledger (separate, larger)

view is a governed-eligible metadata type (view: ViewSchema in BUILTIN_METADATA_TYPE_SCHEMAS, packages/spec/src/kernel/metadata-type-schemas.ts:78) — it round-trips through /api/v1/meta/types/:type and Studio admin forms. It is simply not yet in the GOVERNED rollout (packages/spec/scripts/liveness/check-liveness.mts:56; README lists it as rollout-pending, liveness/README.md:208). That rollout gap is the systemic reason the #1763/#2545 class of drift survived undetected.

Enrolling it: add 'view' to GOVERNED, --dump view, and author packages/spec/liveness/view.json classifying every top-level prop of ViewSchema (list/form/listViews/formViews/protection; the gate drills one level via children, so FormViewSchema's props — including a new buttons — classify under form.children.*). The existing audit already pre-computed much of the live/dead split and would seed view.json. This is a standalone effort, not a same-PR adjunct to Track A — flagging so it's scoped as its own PR.

Suggested sequencing

  1. objectui PR (independent, ready now): bump @objectstack/spec pin to ^15 + fix the /ui zod-schema value-export erasure (tracked in ObjectFormSchema ↔ @objectstack/spec FormViewSchema alignment (ObjectView form adapter) objectui#2545).
  2. Track A as a cross-repo pair: spec buttons/defaults + objectui ObjectForm wiring, so neither side is inert.
  3. Track B as a dedicated liveness-enrollment PR seeded by the 2026-06 audit.

Grounding ADRs: 0078-no-silently-inert-metadata, 0049-no-unenforced-security-properties, 0050-form-layout-vs-presentation, 0089-unify-visibility-predicate-naming. Refs objectstack-ai/objectui#2545, objectstack-ai/objectui#1763.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions