Skip to content

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

Closed

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, groups→sections) 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.

Activity

  1. self-assigned this
    on Jul 16, 2026
  2. os-zhuang commented on Jul 16, 2026

    @os-zhuang
    ContributorAuthor

    Both tracks are merged — closing.

    Residual (tracked elsewhere): the objectui-side schema.buttons/schema.defaults read (objectstack-ai/objectui#2545) — flipping those two ledger entries experimental → live + dropping the spec markers rides on that PR; and the level-2 dead residue noted in the ledger (userActions.buttons, addRecord.mode/formView, tabs[].order) sits on the ADR-0049 enforce-or-remove worklist.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions