Skip to content

Commit b4f2c59

Browse files
committed
docs(spec): the two ADR-0087 D3 entries and the changeset for the inline grid column carriers
The generated registry regions are not regenerated in this commit: that waits for a main that carries the two in-flight registry changes. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1b1f4ef commit b4f2c59

3 files changed

Lines changed: 116 additions & 0 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
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)
6+
7+
Clause-②: yes (narrowing)
8+
9+
<!-- adr-0087: registered form-view-subform-columns-closed, inline-grid-column-identity-only-currency-scale-refused -->
10+
11+
**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.
17+
18+
## FROM → TO
19+
20+
| you wrote | write instead |
21+
|:--|:--|
22+
| `subforms: [{ childObject: 'invoice_line', columns: [{ field: 'quantity' }] }]` | `subforms: [{ childObject: 'invoice_line', columns: [{ name: 'quantity' }] }]` |
23+
| `subforms: [{ childObject: 'invoice_line', columns: [{ name: 'amount', type: 'currency', scale: 2 }] }]` | `subforms: [{ childObject: 'invoice_line', columns: [{ name: 'amount', type: 'currency' }] }]` |
24+
| `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.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type { SemanticMigration } from '../../types.js';
4+
5+
// #20901 — the form-view carrier of the inline grid column. `subforms[].columns`
6+
// was `z.array(z.any())` while the other carrier, a relationship field's
7+
// `inlineColumns`, has been the strict `InlineGridColumnSchema` since #9227; the
8+
// carrier now REFERENCES that schema, so both carriers are judged by one
9+
// contract. D3 only, deliberately: the one mechanical respelling the family
10+
// had (`field` → `name`, conversion `field-column-lists-canonicalized`) is
11+
// already retired from the load path, and every other refused shape is a
12+
// judgment only the author can make. A stored view whose column fails is
13+
// refused with the column schema's own prescription, never stripped.
14+
export const entry: SemanticMigration = {
15+
id: 'form-view-subform-columns-closed',
16+
surface: 'view.form.subforms[].columns[] and view.formViews.<key>.subforms[].columns[] — the '
17+
+ 'form view\'s inline grid columns, which used to accept any value',
18+
replacement: 'each entry is the strict, name-keyed inline grid column a relationship field\'s '
19+
+ '`inlineColumns` takes — `{ name, label?, type?, … }`, where `{ name }` alone hydrates the rest '
20+
+ 'from the child object\'s field. Write `name` where a column said `field` (or `fieldName`, '
21+
+ '`key`); delete `scale` from a column declaring `type: \'currency\'`; delete any key the '
22+
+ 'column schema does not declare.',
23+
reason: 'Both carriers feed the one console grid, which reads only the keys the column schema '
24+
+ 'declares and keys a column by `name` alone. On the form view the columns were never judged, '
25+
+ 'so a mis-keyed column published clean and drew a blank grid column, and a key the other '
26+
+ 'carrier refuses — `scale` on a currency column, under the maintainer\'s ruling of 2026-09-23 '
27+
+ '(option B, `scale` retired from the currency type) and the remedy ruled on 2026-09-24 '
28+
+ '(option 乙 — a currency\'s ISO 4217 minor unit decides its display) — published green here. '
29+
+ 'The carrier now references the column schema, so every rule it holds applies here too, with '
30+
+ 'its own prescription. NOT mechanically converted: the `field` → `name` respelling this family '
31+
+ 'had is already retired from the load path, and which column an unknown key or a mixed '
32+
+ '`field`/`name` entry meant is the author\'s call — a conversion that dropped the key would '
33+
+ 'accept on every load what the parse now refuses. Population measured at the change, on '
34+
+ 'origin/main cb4c31dd52: zero authored `subforms` in the repository (the showcase derives its '
35+
+ 'master-detail grids from the data model instead), against one authored `inlineColumns` block '
36+
+ 'as the control. Deployed metadata NOT MEASURED.',
37+
acceptanceCriteria: 'Every view in the stack parses: `objectstack validate` and a view parse report '
38+
+ 'no issue on a `subforms[].columns[]` path. Every column entry is an object carrying `name`, '
39+
+ 'no entry carries `field`, `fieldName` or `key`, and no column declaring `type: \'currency\'` '
40+
+ 'carries `scale`. Each column `name` names a field of the subform\'s `childObject`, and the '
41+
+ 'master-detail grid renders a value — not a blank cell — in each column for a row that has one.',
42+
};
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type { SemanticMigration } from '../../types.js';
4+
5+
// #20901 — the reach of `inline-grid-column-currency-scale-refused`, extended to
6+
// the column that declares no `type`. The column schema judges only a DECLARED
7+
// type; an identity-only column takes its type from the child field when the
8+
// console hydrates it, and the child field is a fact the stack holds. So
9+
// `defineStack`'s cross-reference check re-parses such a column as the type it
10+
// renders as and reports the column schema's own refusal — no second scale
11+
// rule. D3 only, for the reason the declared-type entry gives: a conversion
12+
// that dropped the key would accept it on every load, the grace window the
13+
// ruling refused.
14+
export const entry: SemanticMigration = {
15+
id: 'inline-grid-column-identity-only-currency-scale-refused',
16+
surface: 'object.fields.<name>.inlineColumns[].scale and view.form.subforms[].columns[].scale '
17+
+ '(and each formViews entry) on a column that declares NO `type` and whose `name` is a '
18+
+ '`currency` field of the child object — any value, `scale: 0` included. A column declaring '
19+
+ '`type: \'number\'`, and a column over a field of any other type, keep `scale`',
20+
replacement: 'no `scale` on the column. DELETE the key — that is the whole migration: the column '
21+
+ 'renders as a currency column, and a currency amount\'s decimal places are its currency\'s. '
22+
+ 'The currency\'s ISO 4217 minor unit decides how the cell displays the amount and the width a '
23+
+ 'computed amount is rounded to. ⛔ Nothing replaces the key: do not re-declare its value under '
24+
+ 'any other key, and do not add `type: \'number\'` to keep it on a currency amount.',
25+
reason: 'The refusal of `scale` on a currency inline grid column (entry '
26+
+ '`inline-grid-column-currency-scale-refused`, under the maintainer\'s rulings of 2026-09-23, '
27+
+ 'option B, and 2026-09-24, option 乙) reached only a column that DECLARES `type: \'currency\'`, '
28+
+ 'because the column schema cannot see the child field. An identity-only column — the '
29+
+ 'recommended form — over a currency field renders as a currency column all the same, so it '
30+
+ 'published green carrying the refused key, and the console ignored it. `defineStack`\'s '
31+
+ 'cross-reference check, which holds the child object\'s fields, now judges such a column as the '
32+
+ 'type it renders as and refuses it with the column schema\'s own message. Reach: the child '
33+
+ 'object must be declared in the same stack; a column naming no field of it, or a subform whose '
34+
+ 'child object comes from another package, is not judged there. Population measured at the '
35+
+ 'change, on origin/main cb4c31dd52: one authored `inlineColumns` block (the showcase invoice, '
36+
+ 'seven identity-only columns, none carrying `scale`) and zero authored `subforms`. Deployed '
37+
+ 'metadata NOT MEASURED.',
38+
acceptanceCriteria: '`objectstack validate` and `defineStack` report no cross-reference finding on '
39+
+ 'an `inlineColumns[].scale` or `subforms[].columns[].scale` path. A column that carried `scale` '
40+
+ 'over a currency child field no longer declares it, and a diff of the column shows that one line '
41+
+ 'deleted and no key added. Columns over `number` fields, and columns declaring `type: '
42+
+ '\'number\'`, keep their `scale`.',
43+
};

0 commit comments

Comments
 (0)