Skip to content

Commit 0b10763

Browse files
committed
Merge origin/main into claude/issue-21647-echo-never-in-memory-bucket
Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
2 parents 1ef5b5e + fea6706 commit 0b10763

27 files changed

Lines changed: 2238 additions & 242 deletions
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: an `object-master-detail-form` detail entry's `sortField` is retired — the console derives the line-position field from the child object and reads no authored value (#21589)
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: registered object-master-detail-form-detail-sort-field-removed, object-master-detail-form-detail-sort-field-retired -->
10+
11+
**BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for accept-set narrowings.
12+
13+
`ComponentPropsMap['object-master-detail-form'].details[].sortField` named the child field the line grid stamps with each line's position on drag-reorder. The console stopped reading it: the field it stamps is derived from the child object, and the pinned console crossed that change while the spec still declared the key. So an authored `sortField` went through `os validate` clean and was dropped, and a drag-reorder stamped the derived field, or none (ADR-0049 enforce-or-remove).
14+
15+
### FROM → TO
16+
17+
| before | what to write instead |
18+
| --- | --- |
19+
| `details: [{ childObject: 'crm_invoice_line', sortField: 'line_no' }]` | delete `sortField`. The grid stamps the child object's first field named `position`, `sort_order`, `sequence`, `line_no`, `line_number` or `sort`. |
20+
| `sortField` naming a field outside that list | give the child object one of those fields; the line order is kept there. |
21+
| an entry that names `relationshipField` and at least one column and gives every column a `type` | unchanged: the renderer keeps that entry exactly as authored, loads no child schema for it, and stamps no line position, before and after the upgrade alike. |
22+
23+
**The one-line fix: delete `sortField` from every `object-master-detail-form` detail entry.** `os migrate meta --from 17` lists the mechanical edits for existing sources; apply them by hand.
24+
25+
**What an author now sees.** Writing the key fails `tsc` (its input type is the retired-key mark), and `os validate`, `os build` and `os lint` report it as a `component-props-invalid` warning carrying the prescription at `properties.details.N.sortField`. A page that carries it still saves and loads: a page component's `properties` is not parsed on the metadata save or load path.
26+
27+
### The retirement kit
28+
29+
- **Tombstone.** `sortField` is a `retiredKey()` on the strict detail entry. Its prescription prints the derived field names from their one declaration, a module reached by relative import only (`data/inline-grid-sort-fields.ts`), which the derived inline-grid columns read too.
30+
- **D2 conversion `object-master-detail-form-detail-sort-field-removed`** (step 18, retired from the load path): a lossless delete of `sortField` from every `properties.details[]` entry of an `object-master-detail-form`, scoped by component type and by position. Stored `sys_metadata` pages and built artifacts replay it, one notice per entry.
31+
- **D3 entry `object-master-detail-form-detail-sort-field-retired`** carries the judgment the delete cannot make: whether the child object declares the field the line order is kept in.
32+
- **`RETIRED_KEYS_BY_MAJOR[18]`** registers the nested key `ui/ObjectMasterDetailFormProps:details.sortField`.
33+
- **`record:line_items`' answer to `sortField`** no longer sends the author to the detail entry: no block takes an authored `sortField` any more.
34+
- **No deprecation window**: the writer census is zero.
35+
36+
**Measured producers: none.** On origin/main 9a4182a752, no `object-master-detail-form` detail entry in `examples/`, `apps/`, `packages/`, `skills/` or `content/docs/` writes `sortField`, against the sibling detail-entry key `addLabel` on the showcase project workspace's entry as the control, through the same instrument. At the objectui pin `89cad75d5570` the only detail entries that write it are probes asserting that nothing reads it. Deployed metadata NOT MEASURED.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/service-automation': patch
3+
---
4+
5+
fix(service-automation): a flow's `create_record`, `update_record` and `delete_record` nodes refuse a stored-metadata table as their target (#21624)
6+
7+
Clause-②: no
8+
9+
The two stored-metadata tables (the current metadata bodies and their version history) have one writer for app-authored work: the metadata protocol, where a change is validated and its provenance is recorded. A flow's write nodes wrote those tables directly, outside it. Under `runAs: 'system'` the write ran elevated, so the security middleware never judged it; under `runAs: 'user'` only a composition with the security plugin refused it, as a routable runtime failure with no code. A write node's `filter` was also evaluated against the stored rows, so whether the write acted answered a predicate over the stored body.
10+
11+
**What changes.** A `create_record`, `update_record` or `delete_record` node whose `objectName` is either table is refused before it resolves its filter or its field values and before any engine write, under either run identity. The refusal names the metadata API as the way to change metadata and carries the standard `PERMISSION_DENIED` code, the code the data door answers a non-platform principal's write to these tables with. It is a guard failure: the run fails, nothing downstream of the node runs, and a `fault` edge does not route it. A `try_catch` catch region reads the code on `{$error.code}`. Metadata is changed through the metadata API (`PUT /api/v1/meta/:type/:name`), never through a flow's data nodes.
12+
13+
**What does not change.** Every other object is created, updated and deleted exactly as before. `get_record` keeps serving these tables projected and keyed.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@objectstack/metadata-protocol': minor
3+
---
4+
5+
The runtime save door refuses a view container whose save name, or any name its expansion produces, is a name already served from elsewhere; a package-less container row named after a view item a package ships belongs to no package
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) A validity narrowing at one runtime write door over existing keys: no key of `ViewSchema` or of any other metadata schema is removed, renamed or re-shaped, so there is no tombstone and nothing mechanical for `objectstack migrate meta` to rewrite. Whether a refused container was meant as a member of the container that already serves the name, as a view item of that name, or as a member under a key of its own is authoring intent no conversion entry can decide. New saves are refused with the remedy; a row stored before this change keeps its bytes, and no stored row is re-saved. The census, taken first over the set the ruling names, found no writer that saves a second container of one object, a container under a name another container expands, or a container under the name of a view item a package ships on purpose: the platform checklist's live view-authoring item saves one container per object (`qa_repair_asset_views` on `repair_asset`); the save door's own name rulings (P2, P2b) save one container; a container on another package's object expands under its own name; and Studio at the objectui pin creates view items (the metadata-admin create body, `createView`, `setViewConfig`) and re-saves a stored body under the name it carries. Package duplication can copy a container whose object is outside the copied package into a second container of that object: before this change the copy silently took the source package's view, and it is now reported as a failed item. The source registrars never reach this door, and the example apps seed no `sys_metadata` view rows; hosted tenants and the cloud AI author were not measured. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers this rule and this diff adds none (not registered / already-registered); and the change narrows what a runtime write door accepts, not a runtime interface or a type surface alone (not runtime-interface-only / type-surface-only). -->
10+
11+
**BREAKING** accept-set narrowing at the runtime save door, shipped as `minor` under the repo's launch-window convention for breaking changes, the grade the same door's earlier container-name refusals shipped with.
12+
13+
**One rule.** `saveMetaItem`, which `PUT /api/v1/meta/view/:name` and the dispatcher's metadata save both call, now refuses an aggregated view container (`list` / `form` / `listViews` / `formViews`) when its save name, or any name its expansion produces, is already served from elsewhere: by another stored container's expansion in the caller's selection (environment-wide rows plus the caller's organization's), whatever that container's object, or by a view item (a body carrying `viewKind`) a package ships. A name the container's own expansion produces is refused as its save name too. Every name is judged where the read doors place it, so every member kind, the expander's de-duplicated names and a container on another package's object (which expands under its own name) are all covered. The refusal is `VALIDATION_ERROR` / 400, in draft and in publish mode, before anything is stored or registered, and it names the other owner: the stored container, the shipping package, or the container's own expansion.
14+
15+
**Before and after, per shape** (with `{ name: 'crm_lead', object: 'crm_lead', list, listViews: { pipeline } }` stored where a sibling is named):
16+
17+
- A container bound to **another object**, saved under a sibling's expanded name (`{ object: 'crm_account', list }` as `crm_lead.pipeline`). Before: accepted; the sibling's `crm_lead.pipeline` view was no longer served on either door, and the by-name read answered the raw container. After: refused, naming the container `crm_lead`.
18+
- An **unbound** container (`{ list }`) under the same name. Before and after: as above.
19+
- A **second container of one object** whose bare `list` takes `crm_lead.default`, under a free name (`{ object: 'crm_lead', list }` as `lead_other_views`), or the object-named container saved after such a one. Before: accepted; whichever container was read last replaced the other's default on both doors, and nothing said why. After: refused, naming the stored container that already serves the name.
20+
- A container saved under the name of a **view item a package ships** (`{ name: 'showcase_task.in_progress', object: 'showcase_task', list }` as `showcase_task.in_progress`), package-less, organization-scoped or in a writable package. Before: accepted; the packaged view was no longer served on the object door and the by-name read answered the raw container. Package-less, the row was also judged a container of the shipping package, so its bare `list` replaced the packaged `showcase_task.default` on both doors, wearing that package's `_packageId`. After: refused, naming the shipping package.
21+
- An overlay of a package's own container whose new member takes the name of a view item **the package ships on its own**. Before: accepted; the member replaced that packaged view on both doors. After: refused, naming the package.
22+
- A container under a name its own expansion produces, or under a name another stored container of the same object expands. Refused before and after, with the same envelope. The own-expansion refusal no longer tells the author to save the container under its object's name when a stored container already holds that name; it names that container to add the view to.
23+
24+
**What still saves.** A view item under any of these names: it is that name's sanctioned override. A container under its object's name, or under any other name of its own, whose expansion takes no name served elsewhere, and its own re-save. An overlay of a package's own container under that container's name. A container on another package's object, which expands under its own name. A container whose would-be sibling is in another organization: the caller's own selection decides, as it does for the read doors.
25+
26+
**Rows stored before this change.** They keep their bytes and are served as before, with one change: a package-less container row stored under the name of a view item a package ships now belongs to no package. On that package's object it expands under its own name (`showcase_task.showcase_task.in_progress` for a bare `list`), with no `_packageId` and no default, and the packaged views it used to replace are served again on both doors. The row itself still takes its own name's slot, as any stored row does. A new save of a row in a refused shape, a re-save included, is refused until its body stops colliding; `migrate meta --stored` and package duplication report such a row as failed with this refusal instead of re-saving it. Delete stays open.
27+
28+
**The fix.** Add the view as a member of the stored container that already serves the name (its `list`, `listViews`, `form` or `formViews`), or save a view item (`name`, `object`, `viewKind`, `config`) under that name to override it. For a name a package ships: save a view item under it to override the packaged view, or save the container under a name of its own that no package ships and no stored container expands.

‎content/docs/references/ui/component.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1084,7 +1084,7 @@ Sort field and direction pair
10841084
| **formFields** | `string[]` | optional | Child field names for the per-row expand form. When omitted they are derived from the child object's fields — except on an entry that names both `relationshipField` and at least one column, which is kept as authored: nothing is derived, and the per-row form is offered only when `inlineMode` is 'form', where it draws the child object's full field list |
10851085
| **inlineMode** | `Enum<'grid' \| 'form'>` | optional | Inline-edit form factor: 'grid' = editable cells; 'form' = read-only list + per-row full form. When omitted it is resolved from the relationship field's `inlineEdit`, else from the child object's shape — except on an entry that names both `relationshipField` and at least one column, which is kept as authored: nothing is resolved, the collection renders as a grid, and the per-row form is offered only when `formFields` lists more fields than `columns` |
10861086
| **amountField** | `string` | optional | Numeric child column summed for the running total and the `totalField` rollup. When omitted it is picked from the grid's number and currency columns: a computed one, else one named `amount`, `total`, `subtotal`, `line_total`, `line_amount` or `net_amount`, else the last currency column, else the last numeric one — except on an entry that names `relationshipField` and at least one column and gives every column a `type`. The renderer keeps that entry exactly as authored, so nothing is picked. With no `amountField` authored or picked, the sums read a child column named `amount`, and the grid shows a running total only when `totalField` is set |
1087-
| **sortField** | `string` | optional | Child field holding the line sort position, stamped on drag-reorder. When omitted it is the child object's first field named `position`, `sort_order`, `sequence`, `line_no`, `line_number` or `sort`, if it has one — except on an entry that names `relationshipField` and at least one column and gives every column a `type`. The renderer keeps that entry exactly as authored: nothing is derived, the grid stamps no line position, and a drag-reorder is not saved |
1087+
| **sortField** | `never` | optional | [REMOVED] `object-master-detail-form` property `details[].sortField` was removed in @objectstack/spec 17 (ADR-0087 D2) — the console reads no authored value: the field the line grid stamps with each line's position on drag-reorder is derived from the child object, so an authored `sortField` was accepted and dropped. Delete the key. For a drag-reorder to be saved, give the child object a field named `position` / `sort_order` / `sequence` / `line_no` / `line_number` / `sort`: the renderer stamps the child's first field with one of those names — except on an entry that names `relationshipField` and at least one column and gives every column a `type`, which the renderer keeps exactly as authored and stamps no line position on. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. |
10881088
| **totalField** | `string` | optional | Parent field to receive the rolled-up sum |
10891089
| **title** | `string` | optional | Section title |
10901090
| **minRows** | `number` | optional | Minimum number of rows |

‎packages/lint/src/validate-component-props.test.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,32 @@ describe('validateComponentProps — undeclared keys', () => {
9191
}
9292
});
9393

94+
// #21589 — an `object-master-detail-form` detail entry's `sortField` is a
95+
// retiredKey tombstone, one array level down. The same door, the same
96+
// warning: the finding names the entry's key, and the page is never refused.
97+
it('reports a retired detail-entry `sortField` as a warning carrying the prescription, at the entry\'s key', () => {
98+
const findings = validateComponentProps(
99+
stackWith([
100+
{
101+
type: 'object-master-detail-form',
102+
properties: {
103+
objectName: 'invoice',
104+
details: [
105+
{ title: 'Payments', childObject: 'invoice_payment' },
106+
{ title: 'Lines', childObject: 'invoice_line', sortField: 'line_no' },
107+
],
108+
},
109+
},
110+
]),
111+
);
112+
expect(findings).toHaveLength(1);
113+
const [f] = findings;
114+
expect(f.severity).toBe('warning');
115+
expect(f.rule).toBe(COMPONENT_PROPS_INVALID);
116+
expect(f.path).toBe('pages[0].regions[0].components[0].properties.details.1.sortField');
117+
expect(f.message).toContain('`object-master-detail-form` property `details[].sortField` was removed in @objectstack/spec 17');
118+
});
119+
94120
it('walks components nested inside `properties` (tabs items → children)', () => {
95121
const findings = validateComponentProps(
96122
stackWith([

0 commit comments

Comments
 (0)