From 6febbdfb9ad1ba48bb2fd90dd63121e8f0746373 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Jul 2026 21:26:37 +0000 Subject: [PATCH] chore(spec): flip FormView buttons/defaults to live; close view inverse-drift (#1894) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The structured `FormViewSchema.buttons`/`defaults` shipped under the ADR-0078 escape hatch (declared, but marked `[EXPERIMENTAL — NOT ENFORCED]`) pending an ObjectUI consumer. The objectui `ObjectForm` renderer now folds both onto the flat props it reads (`showSubmit`/`submitText`/`showCancel`/`cancelText`/ `showReset`/`initialValues`), so: - Drop the experimental markers from the two `.describe()` strings + the `FormButtonConfigSchema` leaf doc (the spec liveness gate keys `experimental` off that exact substring). - Flip `view.form.buttons` / `view.form.defaults` in `liveness/view.json` `experimental → live`, citing the objectui consumer; update the `view` row in `liveness/README.md` (70 live / 0 exp / 5 dead). - Update the `view.test.ts` marker assertion to lock in the live state. - Annotate audit cluster #6 + the viewschema audit's inverse-drift line as resolved. Closes the `view` item of #1894 (inverse drift — renderers read undeclared props); the dashboard + app items were already reconciled. Co-lands with the objectui ObjectForm wiring. No shape/parse-behavior change. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01CMaDBhnZEUu1fcw8Rvo6Yq --- .changeset/formview-buttons-defaults-live.md | 19 ++++++++++++ .../2026-06-viewschema-property-liveness.md | 1 + docs/audits/README.md | 2 ++ packages/spec/liveness/README.md | 2 +- packages/spec/liveness/view.json | 4 +-- packages/spec/src/ui/view.test.ts | 9 ++++-- packages/spec/src/ui/view.zod.ts | 30 +++++++++---------- 7 files changed, 46 insertions(+), 21 deletions(-) create mode 100644 .changeset/formview-buttons-defaults-live.md diff --git a/.changeset/formview-buttons-defaults-live.md b/.changeset/formview-buttons-defaults-live.md new file mode 100644 index 0000000000..8dc0b55f37 --- /dev/null +++ b/.changeset/formview-buttons-defaults-live.md @@ -0,0 +1,19 @@ +--- +"@objectstack/spec": patch +--- + +chore(spec): mark FormView `buttons`/`defaults` live now the ObjectUI renderer folds them (#1894) + +The structured `FormViewSchema.buttons` (per-button `submit`/`cancel`/`reset` +visibility + label) and `defaults` (create-mode initial values) shipped under +the ADR-0078 escape hatch — declared, but carrying an `[EXPERIMENTAL — NOT +ENFORCED]` marker because no consumer read them yet. The ObjectUI `ObjectForm` +renderer now folds both onto the flat props it reads +(`showSubmit`/`submitText`/`showCancel`/`cancelText`/`showReset`/ +`initialValues`), so the escape-hatch marker is dropped and the two spec +liveness-ledger entries (`view.form.buttons`, `view.form.defaults`) flip +`experimental → live`. + +No shape or parse-behavior change — both keys were already accepted. This +closes the `view` half of the inverse-drift cleanup (renderers reading +undeclared props), umbrella #1878. diff --git a/docs/audits/2026-06-viewschema-property-liveness.md b/docs/audits/2026-06-viewschema-property-liveness.md index cb39b8531d..47f235723d 100644 --- a/docs/audits/2026-06-viewschema-property-liveness.md +++ b/docs/audits/2026-06-viewschema-property-liveness.md @@ -23,6 +23,7 @@ Variant routing (`tabbed/wizard/split/drawer/modal`) exists in `plugin-form/Obje - `bulkActions` works **only** because `ListView.tsx:1318` remaps it to `batchActions` (ObjectGrid's real key) — a direct `object-grid` caller using `bulkActions` silently no-ops. - `groups` vs `sections` — both alive but on different code paths, not aliased in one place. - Inverse problem: `ObjectView.tsx:809-823`'s form adapter reads keys (`layout,showSubmit,submitText,customFields,title,initialValues,className`) that **don't exist in `FormViewSchema`** — renderer-invented surface with no spec backing. + - **✅ Resolved (#1894 / #2998).** `layout`/`title` are declared on `FormViewSchema`; the button/visibility keys (`showSubmit`/`submitText`/`showCancel`/`cancelText`/`showReset`) and `initialValues` got structured spec homes (`buttons` / `defaults`) that the objectui `ObjectForm` now folds onto its flat props — so the two ledger entries are `live`, and the flat keys survive only as deprecated back-compat. `customFields`/`className`/`fields` stay classified as renderer-only ObjectUI extensions (objectui#2545), not spec surface. ## Recommendation (for ADR) 1. **Migrate the chart renderers to `dataset`/`dimensions`/`values`** (ADR-0021) — pairs with the dashboard/report seed migration; currently the entire chart view variant is dead against the spec. diff --git a/docs/audits/README.md b/docs/audits/README.md index 0bb8192d9b..f5cde6eb0c 100644 --- a/docs/audits/README.md +++ b/docs/audits/README.md @@ -52,6 +52,8 @@ field enhanced-type configs (barcode/qr/slider/rating/color/location) + governan ### 6. Inverse drift — renderer depends on UNDECLARED props dashboard `component`/`data`/`rowField`/`columnField`; view `ObjectView` form-adapter keys; app `accentColor`/`badgeVariant`/`separator`. These break a strict `Schema.parse()`. +**✅ Resolved (#1894).** All three reconciled so strict validation holds: **dashboard** — `component`/`data` + the pre-ADR-0021 inline-analytics keys (`rowField`/`columnField`/…) are quarantined and rejected by the strict `DashboardWidgetSchema` error map (`dashboard.zod.ts`), pointing authors at the `dataset`/`dimensions`/`values` shape (renderer-only extras → `options`). **app** — `accentColor`/`badgeVariant`/`separator` are now declared on `AppSchema` (`app.zod.ts`). **view** — the structured `buttons`/`defaults` were added to `FormViewSchema` (#2998 Track A) and are now consumed: objectui `ObjectForm` folds them onto its flat renderer props (framework#1894 / #2998), so the two liveness-ledger entries flip `experimental → live`; the remaining flat form-adapter keys (`customFields`/`className`/`fields`) are classified renderer-only ObjectUI extensions (objectui#2545). + ### 7. Designer authoring gaps (live prop, no Studio editor) dataset `filter`/`format`/`derived`/`dateGranularity`; skill `triggerConditions` (the activation-critical field); flow `notify` (absent from static palette). diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index a34cdcc880..9ddebcd29b 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -201,7 +201,7 @@ EOF | skill | 10 | – | – | – | fully live | | dataset | 19 | – | 0 | – | `measures.certified` (declared-but-unenforced governance flag) REMOVED in 16.0 (#2377) | | page | 16 | – | – | 1 | fully live + one planned | -| view | 68 | 2 | 5 | – | list/form drilled via `children` (#2998 Track B); dead = list.{responsive,performance} + form.{data,defaultSort,aria}, all but aria authorWarn'd; exp = form.{buttons,defaults} awaiting objectui#2545; audit-era DEAD lines superseded by re-verification (submitBehavior, sharing.lockedBy, list ViewData providers, and the ADR-0021 chart shape — all live now); level-2 dead residue (userActions.buttons, addRecord.mode/formView, tabs[].order) noted on parents — one drill level only | +| view | 70 | 0 | 5 | – | list/form drilled via `children` (#2998 Track B); dead = list.{responsive,performance} + form.{data,defaultSort,aria}, all but aria authorWarn'd; form.{buttons,defaults} now live — objectui ObjectForm folds them onto its flat props (framework#1894 / #2998); audit-era DEAD lines superseded by re-verification (submitBehavior, sharing.lockedBy, list ViewData providers, and the ADR-0021 chart shape — all live now); level-2 dead residue (userActions.buttons, addRecord.mode/formView, tabs[].order) noted on parents — one drill level only | The `dead` set across types is the enforce-or-remove worklist (ADR-0049); every misleading entry carries `authorWarn` so authors hear about it at compile time. diff --git a/packages/spec/liveness/view.json b/packages/spec/liveness/view.json index c3b7ce7447..a4fe305745 100644 --- a/packages/spec/liveness/view.json +++ b/packages/spec/liveness/view.json @@ -99,8 +99,8 @@ }, "sharing": { "status": "live", "evidence": "packages/rest/src/rest-server.ts:4384", "note": "Framework-side consumer: public (anonymous) form endpoints opt in via FormView.sharing — /forms/:slug resolves sharing.publicLink across form + formViews (rest-server.ts:1716, :4366-4405) and grants publicFormGrant. Renderer-side the config is not read (audit L20) — the public form is served, not re-parsed client-side." }, "submitBehavior": { "status": "live", "note": "objectui: WizardForm.tsx:315-316 + ObjectForm.tsx dispatch thank-you/redirect/continue/next-record; covered by ObjectForm.submitBehavior.test.tsx / WizardForm.successBehavior.test.tsx. Supersedes the 2026-06 audit's DEAD line (L20) — wiring landed with the objectui#2545/#2552 reconciliation." }, - "buttons": { "status": "experimental", "note": "#2998 Track A — structured action-button config awaiting the ObjectForm read (objectui#2545); the renderer still reads the flat invented showSubmit/submitText keys (ObjectForm.tsx:147-157). Spec .describe() carries the [EXPERIMENTAL — NOT ENFORCED] marker; drop marker + flip to live when the wiring lands." }, - "defaults": { "status": "experimental", "note": "#2998 Track A — create-mode initial values awaiting the ObjectForm read (objectui#2545); the renderer still reads the flat invented initialValues key (ObjectForm.tsx:364-437). Spec marker as above." }, + "buttons": { "status": "live", "note": "objectui: ObjectForm.tsx foldFormButtons() reads schema.buttons and folds submit/cancel/reset {show,label} onto the flat showSubmit/submitText/showCancel/cancelText/showReset props (framework#1894 / #2998); ObjectView + RecordFormPage forward it. Flat keys kept as deprecated back-compat (explicit flat key wins)." }, + "defaults": { "status": "live", "note": "objectui: ObjectForm.tsx foldFormButtons() reads schema.defaults and folds it into create-mode initialValues (framework#1894 / #2998); an explicit initialValues (URL prefill) still wins." }, "aria": { "status": "dead", "evidence": "no reader in either repo (objectui@fb35e48 grepped 2026-07-16: no schema.aria read in plugin-form/SchemaForm; only ListView applies aria)", diff --git a/packages/spec/src/ui/view.test.ts b/packages/spec/src/ui/view.test.ts index 82601e2185..6ed8e5ed48 100644 --- a/packages/spec/src/ui/view.test.ts +++ b/packages/spec/src/ui/view.test.ts @@ -1933,7 +1933,7 @@ describe('FormViewSchema - defaultSort', () => { }); // ============================================================================ -// FormView structured buttons + defaults (#2998, EXPERIMENTAL until objectui#2545) +// FormView structured buttons + defaults (#2998; live now the objectui ObjectForm folds them, framework#1894) // ============================================================================ describe('FormViewSchema - buttons & defaults', () => { @@ -1987,10 +1987,13 @@ describe('FormViewSchema - buttons & defaults', () => { expect(result.defaults).toBeUndefined(); }); - it('marks both keys experimental until the renderer wiring lands (ADR-0078)', () => { + it('marks both keys live now the ObjectForm renderer folds them (framework#1894 / #2998)', () => { const shape = (FormViewSchema as unknown as z.ZodObject).shape; for (const key of ['buttons', 'defaults'] as const) { - expect(shape[key].description, `${key} .describe()`).toMatch(/EXPERIMENTAL — NOT ENFORCED/); + // The renderer wiring landed (objectui ObjectForm foldFormButtons), so the + // ADR-0078 escape-hatch marker must be gone — the spec liveness gate keys + // `experimental` off this exact substring. + expect(shape[key].description, `${key} .describe()`).not.toMatch(/EXPERIMENTAL — NOT ENFORCED/); } }); }); diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index 5ea99cb665..e12a2ccedd 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -898,7 +898,7 @@ export const FormSectionSchema = lazySchema(() => z.object({ /** * A single form action button (submit / cancel / reset): visibility + label. - * Leaf of the [EXPERIMENTAL] {@link FormViewSchema} `buttons` block (#2998). + * Leaf of the {@link FormViewSchema} `buttons` block (#2998). * `.strict()` per ADR-0089 D3a so a typo'd key errors instead of vanishing. */ export const FormButtonConfigSchema = lazySchema(() => z.object({ @@ -1023,30 +1023,30 @@ export const FormViewSchema = lazySchema(() => z.object({ ]).optional().describe('Post-submit behavior'), /** - * ⚠️ EXPERIMENTAL — NOT ENFORCED (#2998, ADR-0078). Structured action-button - * config: per-button visibility + label for `submit` / `cancel` / `reset`. - * This is the spec home for the flat renderer-invented keys ObjectUI's - * `ObjectForm` reads today (`showSubmit`/`submitText`/`showCancel`/`cancelText`/ - * `showReset`, objectui#2545) — those never existed here and are silently - * stripped by this strip-mode container. Experimental until the ObjectUI - * renderer reads `buttons.*` (the ADR-0078 escape hatch: the contract ships - * ahead of its consumer, marked so authoring it is not a false promise). + * Structured action-button config: per-button visibility + label for + * `submit` / `cancel` / `reset`. The spec home for the flat renderer-invented + * keys ObjectUI's `ObjectForm` used to read (`showSubmit`/`submitText`/ + * `showCancel`/`cancelText`/`showReset`) — now consumed: `ObjectForm` folds + * `buttons.*` down onto those flat props at render (framework#1894, #2998), + * with the flat keys kept only as deprecated back-compat. Authoring `buttons` + * takes effect; a stale flat key still wins where explicitly set. */ buttons: z.object({ submit: FormButtonConfigSchema.optional().describe('Submit button'), cancel: FormButtonConfigSchema.optional().describe('Cancel button'), reset: FormButtonConfigSchema.optional().describe('Reset button'), }).strict().optional() - .describe('[EXPERIMENTAL — NOT ENFORCED, #2998] Form action-button visibility & labels. Renderer wiring pending in ObjectUI (objectui#2545).'), + .describe('Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm (framework#1894 / #2998).'), /** - * ⚠️ EXPERIMENTAL — NOT ENFORCED (#2998, ADR-0078). Initial field values for - * create-mode forms, keyed by field machine name. Spec home for ObjectUI's - * renderer-invented `initialValues` (objectui#2545). Experimental until the - * ObjectUI renderer reads it. + * Initial field values for create-mode forms, keyed by field machine name — + * the spec home for ObjectUI's renderer-invented `initialValues`. Consumed: + * `ObjectForm` folds `defaults` into its create-mode initial values at render + * (framework#1894, #2998); an explicit `initialValues` (e.g. URL prefill) + * still wins. */ defaults: z.record(z.string(), z.unknown()).optional() - .describe('[EXPERIMENTAL — NOT ENFORCED, #2998] Initial field values for create-mode forms (spec home for ObjectUI `initialValues`). Renderer wiring pending (objectui#2545).'), + .describe('Initial field values for create-mode forms (folded into ObjectUI ObjectForm initial values; framework#1894 / #2998).'), /** ARIA accessibility attributes */ aria: AriaPropsSchema.optional().describe('ARIA accessibility attributes for the form view'),