Skip to content

Commit f846cc2

Browse files
docs(spec): step-18 entries, the exportOptions TSDoc and the stored-shapes row say a stored page's next save meets the component-props gate at the metadata save door (#23800)
Fixes #23789 Clause-②: no Text only. No accept set, export or generated artifact moves. ## Why `validateComponentProps` and `validateComponentTypes` run on `surfaces: CLI_AND_RUNTIME` with `runtimeTypes: ['page']` (`packages/lint/src/authoring-rules.ts`, read on `main` at `b485122`). The `runtime-publish` surface is the metadata write path's publish gate: an active `saveMetaItem` and the draft-to-active promotion. So the metadata save door judges a page component's `properties`. The read path does not: the entries' own comments say so ("the gate judges writes, never the read path, so only the next save of a page with a finding is refused"). The sentences below said the save path does not parse `properties`. That was false. Each rewrite keeps the load half and corrects the save half, in the wording PR #23418 used for its 24 entries: *a stored page still loads, because a page component's `properties` is not parsed on the metadata load path, and the next save of the page meets the component-props gate at the metadata save door.* ## Sites: old sentence → new sentence ### Shipped text: step-18 `reason` strings (printed by `os migrate meta` and the upgrade guide) | Entry | Old | New | | :--- | :--- | :--- | | `18.ui-page-action-row-operation-declared` | A page's `properties` is not parsed on the metadata save or load path; `os validate`'s props gate parses it against these same rows and reports the refusal at the row's `operation` or `onSuccess`. | The component-props gate parses a page's `properties` against these same rows and reports the refusal at the row's `operation` or `onSuccess` on `os validate`, `os build` and `os lint`. A stored page still loads, because a page component's `properties` is not parsed on the metadata load path, and the next save of the page meets the component-props gate at the metadata save door. | | `18.ui-object-block-grouping-config-typed` | `properties` on a page component is not parsed on the save path, so a stored page keeps loading and renders as it does today; the component-props gate reports such a value as a `component-props-invalid` error at the offending path on `os validate`, `os build` and `os lint`, … | `properties` on a page component is not parsed on the metadata load path, so a stored page keeps loading and renders as it does today, and the next save of the page meets the component-props gate at the metadata save door. The gate reports such a value as a `component-props-invalid` error at the offending path on `os validate`, `os build` and `os lint` and at that save door, … | | `18.element-text-variant-heading-subheading-retired` | a stored page replays the rewrite at rehydration; a page component's `properties` is not parsed on the save path, and the component-props gate reports an old spelling as a `component-props-invalid` error, carrying the prescription, on `os validate`, `os build` and `os lint`. | a stored page replays the rewrite at rehydration and still loads, because a page component's `properties` is not parsed on the metadata load path, and the next save of the page meets the component-props gate at the metadata save door. The gate reports an old spelling as a `component-props-invalid` error, carrying the prescription, on `os validate`, `os build` and `os lint` and at that save door. | ### Shipped text: `.d.ts` TSDoc | Site | Old | New | | :--- | :--- | :--- | | `packages/spec/src/ui/component.zod.ts`, `exportOptions` | Read by the component-props gate (refused as an error); a stored page still saves and loads, because page-component `properties` is not parsed on the metadata save or load path | Read by the component-props gate (refused as an error) on `os validate`, `os build` and `os lint` and at the metadata save door. A stored page still loads, because page-component `properties` is not parsed on the metadata load path, and its next save meets the component-props gate at the save door | ### Docs | Site | Old | New | | :--- | :--- | :--- | | `content/docs/deployment/cli.mdx`, stored-shapes table, refused-shape row | a page still loads and saves while `os validate`, `os build` and `os lint` refuse the binding in source | a page still loads, its next save is refused at the metadata save door (422), and `os validate`, `os build` and `os lint` refuse the binding in source | ### Code comments: 21 step-18 entry headers (not shipped text) The same sentence in every header, one transform: `… is not parsed on the metadata save or load path, so a stored page is never refused at load …` → `… is not parsed on the metadata load path, so a stored page is never refused at load (its next save meets the component-props gate at the metadata save door) …`. Two headers said "never refused" with no "at load", which is also false since the save door gates. They now read "never refused at load (its next save …)". Only the paragraph holding the sentence is re-wrapped. `18.ui-action-group-menu-member-params-array-only`, `18.ui-action-group-menu-members-typed`, `18.ui-object-form-custom-fields-typed`, `18.ui-object-form-fields-names-typed` (was "never refused"), `18.ui-object-form-members-typed`, `18.ui-object-form-sections-typed`, `18.ui-object-gantt-markers-typed`, `18.ui-object-grid-columns-typed`, `18.ui-object-grid-export-options-closed`, `18.ui-object-grid-kanban-calendar-list-members-typed`, `18.ui-object-grid-row-members-typed`, `18.ui-object-kanban-conditional-formatting-typed`, `18.ui-object-map-gantt-tree-navigation-typed`, `18.ui-object-master-detail-form-details-closed`, `18.ui-object-metric-aggregate-trend-typed`, `18.ui-object-metric-compare-to-typed`, `18.ui-object-metric-drill-down-report-typed`, `18.ui-object-metric-drill-down-typed`, `18.ui-object-timeline-items-typed`, `18.ui-object-timeline-mapping-typed` (was "never refused"), `18.ui-record-line-items-props-closed`. The card counted eleven. The criterion grep finds more, because the sentence often breaks across comment lines (`save or` / `load path`), so the sweep matched the joined text. ## Left alone, on purpose - `18.ui-page-action-row-declarative-update-rules.ts`: PR #23784 rewrites this exact line (its diff carries "at `os validate`, `os build`, `os lint` and the metadata save door"). Excluded by the claim. - `packages/spec/src/conversions/refused-shapes.test.ts:425`: it quotes the *old* fate as history ("Until the page parse judged this binding the fate read …"). It makes no claim. The file is also PR #23784's. - `content/docs/releases/v17/17-7.mdx:456-457` and the `packages/*/CHANGELOG.md` entries: release-owned. A correction there is a docs-only PR, not part of this one. ## Verification - Criterion grep `git grep -nE "not parsed on the (metadata )?save|still saves and loads|loads and saves" -- packages/spec/src content/docs` on this head: 3 hits. None is a live claim this PR owns: `content/docs/releases/v17/17-7.mdx:456` (release-owned), `refused-shapes.test.ts:425` (quoted history, PR #23784's file) and `18.ui-page-action-row-declarative-update-rules.ts:26` (PR #23784 rewrites it). On `main` (`b485122`) the same grep found 21 hits. The sweep also caught the sentences that break across comment lines, which a line grep cannot see. - `pnpm --filter @objectstack/spec build`: exit 0. - `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/migrations src/ui/component`: 31 files, 1676 tests passed. - `pnpm --filter @objectstack/spec typecheck`: exit 0. - Derived gate list: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, 104 commands once the changeset existed. Each runs with its exit code captured before any pipe. Their per-command results are in the `os-dev-report` comment on #23789, and CI runs the full farm. - Changeset: `@objectstack/spec` patch. The `reason` strings and the TSDoc ship in the package. --- _Generated by [Claude Code](https://claude.ai/code/session_01BYTVP3rWqkXqY8sfHuWGCD)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ae0ca66 commit f846cc2

27 files changed

Lines changed: 168 additions & 132 deletions
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): the step-18 upgrade text and the `exportOptions` TSDoc say a stored page's next save meets the component-props gate at the metadata save door
6+
7+
Clause-②: no
8+
9+
Text only; no accept set moves. Since the component-props gate (`validateComponentProps` / `validateComponentTypes`) runs on a `page` write at the metadata save door, three step-18 semantic entries that `os migrate meta` and the protocol upgrade guide print — `ui-page-action-row-operation-declared`, `ui-object-block-grouping-config-typed` and `element-text-variant-heading-subheading-retired` — and the `object-grid` `exportOptions` TSDoc in the `.d.ts` still said a page component's `properties` is not parsed on the save path. They now say what runs: a stored page still loads, because `properties` is not parsed on the metadata load path, and the next save of the page meets the component-props gate at the metadata save door (422 `INVALID_METADATA` on a finding), beside `os validate`, `os build` and `os lint`.

‎content/docs/deployment/cli.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1491,7 +1491,7 @@ can produce, because both supply a live one.
14911491
| Rows that still fail the current schema after conversion | That is a genuine contract violation, not chain-owned history. The write path's rejection is correct; fix the row in Studio |
14921492
| A flow whose rename the conflict guard refused | The old node-type token is a live name something else owns here. Rewriting would clobber that owner, so the row fails loudly naming the token — never a silent skip |
14931493
| A site the conversion chain leaves as stored because no lossless rewrite exists — above all a page filter carrying `$and` / `$or` / `$not` | Flattening a combinator changes which rows the page selects, so it is never done. Each site is printed as a `TODO` line under its row — path, block, and what blocks the rewrite — whatever the row's outcome; a row with nothing but TODOs is reported `skipped`. It does not fail the run, since no run of this pass can clear it: rewrite each site by hand |
1494-
| A stored shape protocol 18 refuses and no conversion rewrites — a `controlled_by_parent` object without exactly one required `master_detail` (two masters, none, a required `lookup` only, an explicit `required: false`), a view bound to the retired view-level API data source or a page component whose `data` prop binds it (`object-grid`, `object-map`, `object-gantt`, `object-tree`), a flow value slot carrying a `{…}` token (a `get_record` / `update_record` / `delete_record` filter operand among them) | Each would need a choice only its owner can make — which master, which page, what an absent value should do — so it is never rewritten. Each site is printed as a `TODO` line under its row, naming the step-18 entry that refuses it, the object, view, page component or flow node, and the owner's choices; a binding's request headers are withheld. Until it is rewritten by hand, the row is refused: an object registers but derives no master, a view registers under `[metadata_spec_invalid]`, a page still loads and saves while `os validate`, `os build` and `os lint` refuse the binding in source, and a flow is not registered |
1494+
| A stored shape protocol 18 refuses and no conversion rewrites — a `controlled_by_parent` object without exactly one required `master_detail` (two masters, none, a required `lookup` only, an explicit `required: false`), a view bound to the retired view-level API data source or a page component whose `data` prop binds it (`object-grid`, `object-map`, `object-gantt`, `object-tree`), a flow value slot carrying a `{…}` token (a `get_record` / `update_record` / `delete_record` filter operand among them) | Each would need a choice only its owner can make — which master, which page, what an absent value should do — so it is never rewritten. Each site is printed as a `TODO` line under its row, naming the step-18 entry that refuses it, the object, view, page component or flow node, and the owner's choices; a binding's request headers are withheld. Until it is rewritten by hand, the row is refused: an object registers but derives no master, a view registers under `[metadata_spec_invalid]`, a page still loads, its next save is refused at the metadata save door (422), and `os validate`, `os build` and `os lint` refuse the binding in source, and a flow is not registered |
14951495
| Any other site `registerFlow` refuses in a stored flow the parse accepts — a text slot's template hole, a decision's `mode`, a malformed predicate, a plugin node's undeclared config key, an `api` flow with no secret | The flow's own definition is wrong, and no conversion rewrites it. Each site is printed as a `TODO registerFlow` line under its row, in the words `registerFlow` refuses it with; a site already listed under a step-18 entry is not repeated. Until it is fixed, the flow is not registered at boot and never runs. A flow the parse itself refuses is reported among the rows that could not be rewritten, in the parse's words |
14961496
14971497
**One thing it lists and never writes: decision nodes that changed meaning at

‎packages/spec/src/migrations/entries/semantic/18.element-text-variant-heading-subheading-retired.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,11 @@ export const entry: SemanticMigration = {
3232
+ 'and `subheading` in a medium-weight small heading style, and `h2` / `h3` draw their own, larger '
3333
+ 'styles. Whether the page wanted that level is the author\'s call — a heading placed for its '
3434
+ 'size rather than its place in the outline may want a deeper level. Nothing is dropped at rest: '
35-
+ 'a stored page replays the rewrite at rehydration; a page component\'s `properties` is not '
36-
+ 'parsed on the save path, and the component-props gate reports an old spelling as a '
35+
+ 'a stored page replays the rewrite at rehydration and still loads, because a page component\'s '
36+
+ '`properties` is not parsed on the metadata load path, and the next save of the page meets the '
37+
+ 'component-props gate at the metadata save door. The gate reports an old spelling as a '
3738
+ '`component-props-invalid` error, carrying the prescription, on `os validate`, `os build` and '
38-
+ '`os lint`. ADR-0087',
39+
+ '`os lint` and at that save door. ADR-0087',
3940
acceptanceCriteria:
4041
'No `element:text` page component carries `variant` `heading` or `subheading`; `os validate` '
4142
+ 'reports no `component-props-invalid` finding under `properties.variant` for these blocks. For '

‎packages/spec/src/migrations/entries/semantic/18.ui-action-group-menu-member-params-array-only.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import type { SemanticMigration } from '../../types.js';
1010
// key, and #21704's fork 5 A refused the member's `properties.params`, so a
1111
// member has no static-values spelling and the container drops a non-array
1212
// `params` on any other type at run time. D3 only: page-component `properties`
13-
// is not parsed on the metadata save or load path, so a stored page is never
14-
// refused at load; there is no D2 conversion, because the only home for static values
15-
// is a different node (an `action:button`), which no rewrite can build in the
13+
// is not parsed on the metadata load path, so a stored page is never refused at
14+
// load (its next save meets the component-props gate at the metadata save
15+
// door); there is no D2 conversion, because the only home for static values is
16+
// a different node (an `action:button`), which no rewrite can build in the
1617
// author's place; and the census found no writer to respell.
1718
export const entry: SemanticMigration = {
1819
id: 'ui-action-group-menu-member-params-array-only',

‎packages/spec/src/migrations/entries/semantic/18.ui-action-group-menu-members-typed.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ import type { SemanticMigration } from '../../types.js';
99
// with the rows' prescriptions; `outcomeMessages`, a member `className` and
1010
// `properties.params` refused; `outcomeMessages` undeclared on all four action
1111
// blocks. D3 only: page-component `properties` is not parsed on the metadata
12-
// save or load path, so a stored page is never refused at load; and the authored census
12+
// load path, so a stored page is never refused at load (its next save meets the
13+
// component-props gate at the metadata save door); and the authored census
1314
// found no working member to respell — the refused values are objectui's probes
14-
// of the very reads the ruling refuses (a member `className`, `outcomeMessages`,
15-
// `properties.params`) and of the host's `autoTrigger` flag.
15+
// of the very reads the ruling refuses (a member `className`,
16+
// `outcomeMessages`, `properties.params`) and of the host's `autoTrigger` flag.
1617
export const entry: SemanticMigration = {
1718
id: 'ui-action-group-menu-members-typed',
1819
surface: 'page `action:group` and `action:menu` components — each member of `properties.actions` (whose keys '

‎packages/spec/src/migrations/entries/semantic/18.ui-object-block-grouping-config-typed.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,10 @@ export const entry: SemanticMigration = {
4242
+ 'unpadded name silently equivalent, which is the consumer tolerance the contract refuses, '
4343
+ 'and a bare string, a number or an empty list has no mapping that says what grouping was '
4444
+ 'meant. Metadata AT REST is left exactly as stored — `properties` on a page component is '
45-
+ 'not parsed on the save path, so a stored page keeps loading and renders as it does today; '
46-
+ 'the component-props gate reports such a value as a `component-props-invalid` '
47-
+ 'error at the offending path on `os validate`, `os build` and `os lint`, a padded name '
45+
+ 'not parsed on the metadata load path, so a stored page keeps loading and renders as it does '
46+
+ 'today, and the next save of the page meets the component-props gate at the metadata save '
47+
+ 'door. The gate reports such a value as a `component-props-invalid` error at the offending '
48+
+ 'path on `os validate`, `os build` and `os lint` and at that save door, a padded name '
4849
+ 'with the received value and the unpadded name to write. ADR-0049 / ADR-0087.',
4950
acceptanceCriteria:
5051
'Every `object-grid` and `object-kanban` node in your pages either omits `grouping` or '

‎packages/spec/src/migrations/entries/semantic/18.ui-object-form-custom-fields-typed.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import type { SemanticMigration } from '../../types.js';
88
// snake_case. The maintainer ruled on #21704 (fork 2, letter B) that the spec
99
// declares a closed runtime form field of the members the form draws, in
1010
// camelCase, keyed by `name`. D3 only: page-component `properties` is not
11-
// parsed on the metadata save or load path, so a stored page is never refused at load;
12-
// and the authored census, with every inline option list evaluated, found no
11+
// parsed on the metadata load path, so a stored page is never refused at load
12+
// (its next save meets the component-props gate at the metadata save door); and
13+
// the authored census, with every inline option list evaluated, found no
1314
// working member to respell — the refused values are a type-level test's
1415
// `visibleOn` and a fixture pinning that an inline `defaultValue` seeds
1516
// nothing. The shipped object-manager dialog's options (`{ label: 'Box', value:

‎packages/spec/src/migrations/entries/semantic/18.ui-object-form-fields-names-typed.ts‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ import type { SemanticMigration } from '../../types.js';
77
// while the form drew a `{ name }` field entry its own page-builder guide
88
// taught. objectstack-ai/objectui#11550 retired that entry from every authoring
99
// face (the form still draws a STORED one, by its name, as tolerance), so both
10-
// rows now take field names — objectui's own declaration of the member — and
11-
// an object entry is refused with what to write instead. D3 only: page-
12-
// component `properties` is not parsed on the metadata save or load path, so a
13-
// stored page is never refused; and the authored census found no authored
14-
// value to respell — the refused values are fixtures probing the stored read,
15-
// the console warning and objectui's own refusal.
10+
// rows now take field names — objectui's own declaration of the member — and an
11+
// object entry is refused with what to write instead. D3 only: page-component
12+
// `properties` is not parsed on the metadata load path, so a stored page is
13+
// never refused at load (its next save meets the component-props gate at the
14+
// metadata save door); and the authored census found no authored value to
15+
// respell — the refused values are fixtures probing the stored read, the
16+
// console warning and objectui's own refusal.
1617
export const entry: SemanticMigration = {
1718
id: 'ui-object-form-fields-names-typed',
1819
surface: 'page `object-form` and `object-master-detail-form` components — `properties.fields` (whose '

‎packages/spec/src/migrations/entries/semantic/18.ui-object-form-members-typed.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ import type { SemanticMigration } from '../../types.js';
1111
// `z.unknown()` (the form draws a `{ name }` field entry and an inline runtime
1212
// field inside a section, which the typed shapes would refuse), and
1313
// `customFields` waits for the spec to declare objectui's runtime form field.
14-
// D3 only: page-component `properties` is not parsed on the metadata save or
15-
// load path, so a stored page is never refused at load; an off-shape value has no
14+
// D3 only: page-component `properties` is not parsed on the metadata load path,
15+
// so a stored page is never refused at load (its next save meets the
16+
// component-props gate at the metadata save door); an off-shape value has no
1617
// rewrite that says what the author meant; and the authored census found no
1718
// authored value to respell — the refused values are fixtures probing that the
1819
// form refuses them.

‎packages/spec/src/migrations/entries/semantic/18.ui-object-form-sections-typed.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import type { SemanticMigration } from '../../types.js';
99
// 3, letter B) a page-block section shape of its own — the form view's section
1010
// keys plus the three entry arms the form reads, canonical spellings only — and
1111
// the stored form view is unchanged. D3 only: page-component `properties` is
12-
// not parsed on the metadata save or load path, so a stored page is never
13-
// refused at load; and the authored census, with every inline option list evaluated,
12+
// not parsed on the metadata load path, so a stored page is never refused at
13+
// load (its next save meets the component-props gate at the metadata save
14+
// door); and the authored census, with every inline option list evaluated,
1415
// found no working section to respell — the refused values are the two probes
1516
// in objectui's `formSectionGroupReference-7051.test.tsx` (a section declaring
1617
// neither `fields` nor `group`, and a group-owned `label` / `collapsible`

0 commit comments

Comments
 (0)