You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit bee75ce
Browse filesBrowse the repository at this point in the historyBrowse files
feat(spec)!: a form view's subform columns are the inline grid column contract, and an identity-only column is judged as the type it renders (#20901) (#20927)
Closes#20901
Clause-②: yes (narrowing)
- `FormViewSchema.subforms[].columns` references
`InlineGridColumnSchema` (was `z.array(z.any())`): the card's typed
`currency` + `scale` column and its `zzz_not_a_key` column are refused
at the view parse.
- `defineStack`'s cross-reference check
(`packages/spec/src/stack.zod.ts#collectHydratedInlineColumnErrors`,
called from `validateCrossReferences`) re-parses a column that declares
no `type` as the type it renders (`currency` over a `currency` child
field) through `InlineGridColumnSchema`, on both carriers. The card's
identity-only column with `scale` is refused there with the column
schema's own message; there is no second `scale` rule.
- ADR-0087: D3 entries `form-view-subform-columns-closed` and
`inline-grid-column-identity-only-currency-scale-refused`; the registry
was regenerated after merging a `main` that carries #20903 and #20911.
Evidence at `feba1a99bd`: pins
`packages/spec/src/inline-grid-column-carriers.test.ts` 13/13;
`@objectstack/spec` suite 584 files / 17179 tests green;
`@objectstack/spec` typecheck green; 114/114 derived gates exit 0.
Ablations, each restored with `git diff HEAD` empty: reference removed,
5 red; cross-reference call removed, 3 red. `os validate` on a probe
stack: identity-only column, exit 1 `STACK_CROSS_REFERENCE_INVALID`;
typed and bogus columns, exit 1 `STACK_SCHEMA_INVALID`; valid columns,
exit 0.
## Acceptance notes
- No check read `subforms[].childObject` before this change
(`validateCrossReferences` read only `form.data.object`), so the
child-object lookup is new here.
- The identity-only check runs in `defineStack` only. A view saved
through the metadata door, or a subform whose child object lives in
another package, gets the schema half alone (NOT MEASURED at the save
door). objectui's `@object-ui/types` mirror is still `z.any()`, and the
render-time warning stays the backstop there.
- `field.zod.ts`'s `scale` describe still names only the declared-type
refusal. PR #20908 holds that file.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
feat(spec)!: a form view's subform columns are the inline grid column contract, and a column that declares no `type` is judged as the type it renders (#20901)
**BREAKING** — an accept-set narrowing on two published authoring surfaces, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. The console's master-detail grid reads one column shape from two carriers: a relationship field's `inlineColumns` and a form view's `subforms[].columns`. Only the first was judged, and only by the type a column declares.
12
+
13
+
**`@objectstack/spec`**
14
+
15
+
-**`FormViewSchema.subforms[].columns`** now references `InlineGridColumnSchema`, the strict, name-keyed column a relationship field's `inlineColumns` already takes. It was `z.array(z.any())`, so every column published clean, including a key the grid never reads and a key the other carrier refuses. Every rule the column schema holds now applies on the form view too, with its own message: an unknown key is named; the retired `field` spelling (and `fieldName`, `key`) is refused with the prescription naming `name`; a column without `name` is refused; `scale` on a column declaring `type: 'currency'` is refused with the currency ruling's remedy. This reaches `view.form` and every `view.formViews` entry, wherever a view is parsed against the spec: `defineStack`, `objectstack validate`, and the `view` metadata type's registered schema (`ViewMetadataSchema`).
16
+
-**`defineStack`'s cross-reference check** now judges a column that declares no `type` as the type it renders. The console fills such a column's type from the child field, so an identity-only column over a `currency` field renders as a currency column. The check resolves the child field, re-parses the column with that type through `InlineGridColumnSchema`, and reports that schema's own refusal (`STACK_CROSS_REFERENCE_INVALID`, 422). Today that means `scale` on an identity-only column over a `currency` child field. Both carriers are walked: `inlineColumns` resolves against the object that owns the relationship field, and `subforms[].columns` against the subform's `childObject`. A child object the stack does not declare, or a column naming no field of it, is not judged.
|`columns: [{ name: 'amount', scale: 2 }]` where `amount` is a `currency` field of the child object (either carrier) |`columns: [{ name: 'amount' }]`|
25
+
| a column carrying a key the column schema does not declare | the column without that key |
26
+
27
+
The one-line fix: write each form-view subform column as `{ name, … }` using only the keys a relationship field's `inlineColumns` accepts, and delete `scale` from any column that renders as a currency column, whether it declares `type: 'currency'` or takes it from a `currency` child field. Nothing replaces `scale` there: the currency's ISO 4217 minor unit decides the displayed decimals.
28
+
29
+
## Who is affected, measured
30
+
31
+
On `origin/main``cb4c31dd52`: zero authored `subforms` in the repository, and one authored `inlineColumns` block (the showcase invoice, seven identity-only columns, none carrying `scale`). No example, template or test fixture outside this change's own pins changes verdict. Deployed metadata was not measured.
|**defaultSort**|`never`| optional |[REMOVED]`form.defaultSort` was removed in @objectstack/spec 17.0.0 (audit close-out) — nothing read it: a related list inside a form sorts by its own list view's `sort`. Delete the key and set the sort on the related list view instead. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. |
1740
1740
|**sharing**|`{ enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … }`| optional | Public sharing configuration for this form |
1741
1741
|**submitBehavior**|`{ kind: 'thank-you'; title?: string; message?: string } \| { kind: 'redirect'; url: string; delayMs?: integer } \| { kind: 'continue' } \| { kind: 'next-record' }`| optional | Post-submit behavior. On the `redirect` arm, `url` is relative-only and interpolates only declared record fields as `{{record.field_name}}`, URL-escaped (ruled 2026-08-11). |
@@ -1820,7 +1820,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
|**defaultSort**|`never`| optional |[REMOVED]`form.defaultSort` was removed in @objectstack/spec 17.0.0 (audit close-out) — nothing read it: a related list inside a form sorts by its own list view's `sort`. Delete the key and set the sort on the related list view instead. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. |
1825
1825
|**sharing**|`{ enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … }`| optional | Public sharing configuration for this form |
1826
1826
|**submitBehavior**|`{ kind: 'thank-you'; title?: string; message?: string } \| { kind: 'redirect'; url: string; delayMs?: integer } \| { kind: 'continue' } \| { kind: 'next-record' }`| optional | Post-submit behavior. On the `redirect` arm, `url` is relative-only and interpolates only declared record fields as `{{record.field_name}}`, URL-escaped (ruled 2026-08-11). |
|**defaultSort**|`never`| optional |[REMOVED]`form.defaultSort` was removed in @objectstack/spec 17.0.0 (audit close-out) — nothing read it: a related list inside a form sorts by its own list view's `sort`. Delete the key and set the sort on the related list view instead. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. |
437
437
|**sharing**|`{ enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … }`| optional | Public sharing configuration for this form |
438
438
|**submitBehavior**|`{ kind: 'thank-you'; title?: string; message?: string } \| { kind: 'redirect'; url: string; delayMs?: integer } \| { kind: 'continue' } \| { kind: 'next-record' }`| optional | Post-submit behavior. On the `redirect` arm, `url` is relative-only and interpolates only declared record fields as `{{record.field_name}}`, URL-escaped (ruled 2026-08-11). |
@@ -508,7 +508,7 @@ Form-view select option — the object-field option shape minus the per-option `
508
508
| :--- | :--- | :--- | :--- |
509
509
|**childObject**|`string`| ✅ | Child object whose records are entered inline |
510
510
|**relationshipField**|`string`| optional | FK on the child pointing back to the parent (auto-detected when omitted) |
511
-
|**columns**|`any[]`| optional | Editable grid columns (derived from the child object when omitted) |
511
+
|**columns**|`{ name: string; label?: string; type?: Enum<'text' \| 'number' \| 'currency' \| 'date' \| 'datetime' \| 'time' \| 'select' \| 'lookup' \| 'file'>; width?: number; … }[]`| optional | Editable grid columns (derived from the child object when omitted). Each entry is the strict, name-keyed inline grid column a relationship field's `inlineColumns` takes (`{ name, label?, type?, … }` — objectui GridColumn); identity-only entries (`{ name }`) hydrate everything else from the child object's fields. Unknown keys and the retired `field` spelling are refused at parse.|
512
512
|**amountField**|`string`| optional | Numeric child column summed for the running total |
513
513
|**totalField**|`string`| optional | Parent field to receive the rolled-up sum |
514
514
|**title**|`string`| optional | Section title |
@@ -1849,7 +1849,7 @@ Tab configuration for multi-tab view interface
|**defaultSort**|`never`| optional |[REMOVED]`form.defaultSort` was removed in @objectstack/spec 17.0.0 (audit close-out) — nothing read it: a related list inside a form sorts by its own list view's `sort`. Delete the key and set the sort on the related list view instead. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. |
1854
1854
|**sharing**|`{ enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … }`| optional | Public sharing configuration for this form |
1855
1855
|**submitBehavior**|`{ kind: 'thank-you'; title?: string; message?: string } \| { kind: 'redirect'; url: string; delayMs?: integer } \| { kind: 'continue' } \| { kind: 'next-record' }`| optional | Post-submit behavior. On the `redirect` arm, `url` is relative-only and interpolates only declared record fields as `{{record.field_name}}`, URL-escaped (ruled 2026-08-11). |
@@ -1934,7 +1934,7 @@ Tab configuration for multi-tab view interface
|**defaultSort**|`never`| optional |[REMOVED]`form.defaultSort` was removed in @objectstack/spec 17.0.0 (audit close-out) — nothing read it: a related list inside a form sorts by its own list view's `sort`. Delete the key and set the sort on the related list view instead. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. |
1939
1939
|**sharing**|`{ enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … }`| optional | Public sharing configuration for this form |
1940
1940
|**submitBehavior**|`{ kind: 'thank-you'; title?: string; message?: string } \| { kind: 'redirect'; url: string; delayMs?: integer } \| { kind: 'continue' } \| { kind: 'next-record' }`| optional | Post-submit behavior. On the `redirect` arm, `url` is relative-only and interpolates only declared record fields as `{{record.field_name}}`, URL-escaped (ruled 2026-08-11). |
@@ -2209,7 +2209,7 @@ This schema accepts one of the following structures:
|**defaultSort**|`never`| optional |[REMOVED]`form.defaultSort` was removed in @objectstack/spec 17.0.0 (audit close-out) — nothing read it: a related list inside a form sorts by its own list view's `sort`. Delete the key and set the sort on the related list view instead. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. |
2214
2214
|**sharing**|`{ enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … }`| optional | Public sharing configuration for this form |
2215
2215
|**submitBehavior**|`{ kind: 'thank-you'; title?: string; message?: string } \| { kind: 'redirect'; url: string; delayMs?: integer } \| { kind: 'continue' } \| { kind: 'next-record' }`| optional | Post-submit behavior. On the `redirect` arm, `url` is relative-only and interpolates only declared record fields as `{{record.field_name}}`, URL-escaped (ruled 2026-08-11). |
@@ -2399,7 +2399,7 @@ This schema accepts one of the following structures:
|**defaultSort**|`never`| optional |[REMOVED]`form.defaultSort` was removed in @objectstack/spec 17.0.0 (audit close-out) — nothing read it: a related list inside a form sorts by its own list view's `sort`. Delete the key and set the sort on the related list view instead. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. |
2404
2404
|**sharing**|`{ enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … }`| optional | Public sharing configuration for this form |
2405
2405
|**submitBehavior**|`{ kind: 'thank-you'; title?: string; message?: string } \| { kind: 'redirect'; url: string; delayMs?: integer } \| { kind: 'continue' } \| { kind: 'next-record' }`| optional | Post-submit behavior. On the `redirect` arm, `url` is relative-only and interpolates only declared record fields as `{{record.field_name}}`, URL-escaped (ruled 2026-08-11). |
0 commit comments