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 d0c09e4
Browse filesBrowse the repository at this point in the historyBrowse files
feat(lint)!: the two page component rules run on the runtime publish door (ruling B) (#23418)
Fixes#23388
Clause-②: no (narrowing)
Ruling of record `6161173492` (letter B), quoting the maintainer: 「23388
B 是不是最简单?」「23388 如果页面有问题,不是应该修改了再保存吗?」
## What changes
- **`packages/lint/src/authoring-rules.ts`:** `validateComponentProps`
and `validateComponentTypes` go from `surfaces: CLI_ONLY` to
`CLI_AND_RUNTIME`, with `runtimeTypes: ['page']` (ruling items 1 and 2).
Both entry comments drop the #4716 stored-row false-refusal discipline
and the two pending `surfaceReason`s, and cite this ruling as the ruling
of record (item 3). The `validatePrintPageBlocks` entry comment said
"the false-refusal budget `validateComponentTypes` is held on"; it now
says that budget was replaced by this ruling. The rules' own code and
`ComponentPropsMap` are not touched.
- **Header lines (item 3):** the header lines the dispatch pointed at
(~494-511, `RUNTIME_OBJECT_ADVISORY_VOLUME`) describe the #4716 split of
the OBJECT rules, not these two page rules. They are still true and are
not rewritten. No other header line in the file names these two rules.
- **The crossing needed no mechanism edit.** `page` already maps to
`pages` in `TYPE_TO_STACK_KEY`. The runtime gate judges a finding
located on the written item against the baseline without the item, so
the next save of a stored page with a finding is that save's own
finding. The read and boot path runs no authoring rule.
- **Pins (item 4)** are in
`packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts`
and run through the real `saveMetaItem` / `publishMetaItem` /
`loadMetaFromDb` / `getMetaItem`:
- (a) An active save of a page whose `page:header` carries `titel` is
refused with `422 INVALID_METADATA`, rule `component-props-unknown-key`
at `pages[0].regions[0].components[0].properties.titel`. The message
carries `` `titel` → `title` `` and the hint carries `` Remove `titel`
``. Nothing is stored. The draft-to-active promotion of the same page is
refused the same way.
- (c) A component typed `page:headr` is refused with `422`, rule
`component-type-unknown`, hint exactly ``Rename `page:headr` →
`page:header`.``
- Control: the declared spelling saves, and so does
`acme:pipeline_board`, a type outside the reserved namespaces.
- (b) A page row carrying both findings is seeded into the store. Then:
`loadMetaFromDb` returns `loaded: 1, errors: 0, invalid: 0` and
registers the page; `getMetaItem` serves the stored body; the next save
of the same body is refused with both rule ids and the stored row is
unchanged; the fixed page then saves. This mirrors the precedent
sentence at `18.page-default-record-page-one-per-object-refused.ts:42`.
- **Changeset (item 5):**
`.changeset/23388-page-component-rules-runtime-door.md`.
`@objectstack/lint` gets `minor` with the BREAKING banner, and
`@objectstack/spec` gets `patch` (text only). It carries the ruling's
sentence verbatim and the ADR-0087 disposition `not-required
(already-registered …)`, listing the same D3 ids the 23147 changeset
names. The card's step ① (a stored-row listing) is not built.
## Overlap with #23357 (ruling item 6, for the seat)
Once this lands, #23357's narrow judge (the retired page data binding,
in `packages/lint/src/validate-component-props.ts` /
`packages/metadata-protocol/src/protocol.ts`) overlaps the general rule
at the save door. One operation has two implementations, and the
governed side wins. I did not touch it, because seat 1 holds those
files. The same seat should fix one sentence while it is in those files:
`validate-component-props.ts:111-116` ("The runtime publish door … does
not run this rule") becomes false with this PR.
## File surface beyond the claim, and why
Ruling item 3 limits the rewrite to `authoring-rules.ts`. My change also
made the texts below false, and os-dev requires a published text that a
change makes false to be fixed in that same change, so I corrected them
here:
- **`packages/lint/src/validate-print-page-blocks.ts`** and
**`packages/lint/src/validate-component-types.ts`** headers:
comment-only. Each named the props/types rules' pending stored-row
budget or `surfaceReason`.
- **24 step-18 D3 entries** under
`packages/spec/src/migrations/entries/semantic/18.ui-*`,
`18.element-data-source-and-object-block-filter-rule-array.ts`, and
**`packages/spec/src/migrations/registry.ts.template`**: these are
shipped upgrade-guide and `os migrate meta` text. Each said "a stored
page still saves and loads, because a page component's `properties` is
not parsed on the metadata save or load path", or for one entry "a
re-save through the metadata API is not refused there". After this PR,
the next save of such a page is refused by the component-props gate at
the save door. Each now says the page still loads and that its next save
meets the component-props gate at the metadata save door. Code comments
that said "a stored page is never refused" now say "never refused at
load". Text only: no key, schema or conversion changes. No committed
generated artifact moves (`registry.ts` is gitignored, and
`check:spec-changes` / `check:upgrade-guide` are green).
- One more now-false sentence is NOT fixed, because #23332 holds that
file: the doc comment on the `object-grid` export-options row at
`packages/spec/src/ui/component.zod.ts:5991-5994` ("a stored page still
saves and loads"). It is a comment, not runtime text.
## Deliberate correction of pending changesets
*(Seat update at head `3301d5f2f`: `18.0.0-next.1` was cut on `main`
(`0c52e4307`), which moved both notes into `.changeset/pre/`; they are
retained for the GA `18.0.0` note that `changeset pre exit` assembles,
so the correction still applies there. The shipped `18.0.0-next.1`
CHANGELOG is history and is not edited. Paths below are the current
ones.)*
`check:empty-changeset` is **red by design**: this PR changes two
pending changesets it did not add, and those sentences became false
because of this PR's diff. I added no label. The seat decides at review,
against the head sha.
1. **`.changeset/pre/23147-component-props-every-finding-refused.md`**
- Before: "**Not judged here.** The runtime publish door (the Studio,
REST `/meta`, MCP) does not run this rule, so a page saved there is not
refused for any of these. `defineStack` and the stack schema keep the
open `properties` bag as written. …"
- After: "**Not judged here.** `defineStack` and the stack schema keep
the open `properties` bag as written. …"
- What makes the before false: `packages/lint/src/authoring-rules.ts`,
the `validateComponentProps` entry, `- surfaces: CLI_ONLY,` becomes `+
surfaces: CLI_AND_RUNTIME,` / `+ runtimeTypes: ['page'],`.
2. **`.changeset/pre/23103-page-action-row-on-success-and-executor.md`**
- Before: "… as a `component-props-invalid` finding (an error, like
every finding of that gate). A stored page still saves and loads."
- After: "… as a `component-props-invalid` finding (an error, like every
finding of that gate). A stored page still loads; the metadata save door
runs the same gate, so the next save of a page carrying a refused row is
refused with the same text."
- What makes the before false: the same `validateComponentProps` line in
`authoring-rules.ts`.
The gate's exact failing output (`node scripts/check-empty-changeset.mjs
--base origin/main`, exit 1, at `ba20e780e`):
```text
This PR changes a changeset it did not add:
.changeset/pre/23103-page-action-row-on-success-and-executor.md
present on the merge base and CHANGED by this PR -- this is somebody else's release note
.changeset/pre/23147-component-props-every-finding-refused.md
present on the merge base and CHANGED by this PR -- this is somebody else's release note
```
It ends with one `::error file=…` annotation for each of the two files:
"… exists on the merge base and was not added by this PR … DELIBERATE
CORRECTION -- your change may have made this PENDING release note false,
and you rewrote it in the same stroke. Remedy: do NOT restore it -- say
so on the PR and get it confirmed …"
## Acceptance notes
- *(Seat update at head `3301d5f2f`: closed. PR #23485 (#23421,
`8693110c4`) retired both conversions from the load path at 18, so both
spellings are refused with the rename at every door and the save door
and `os validate` agree. The measurement below describes the state
before that landing.)*
- **Door asymmetry, measured.** These two rules run at the CLI at the
`normalized` tier, after ADR-0087 D2 conversions. The save door judges
the body as sent. I ran both rules over every conversion fixture with a
`pages` stack (31 of 136 conversions), raw against normalized:
- 29 give identical verdicts.
- Two live-window aliases differ: `page-header-subtitle-alias`
(`page:header` `description`, 5 raw-only findings) and
`inline-action-api-params-to-body-extra` (an inline api action's object
`params`, 3 raw-only).
- Both declare `toMajor: 17`, so their window runs out at 18 (`retiresIn
= toMajor + 1`), but neither is `retiredFromLoadPath` yet. Until that
happens, a save spelling them is refused with the canonical key (for
example, "Did you mean `description` → `subtitle`?"), while `os
validate` converts them.
- The changeset says so. Stored rows are rehydrated through the
conversions on read, so a Studio round-trip sends back the canonical
spelling.
- The objectui page designer writes no component props, per the
director's reading in the ruling.
## Verification (head `ba20e780e`)
- `pnpm --filter @objectstack/lint test`: 138 files, 6699 tests passed.
`typecheck` passed.
- `pnpm --filter @objectstack/metadata-protocol test`: 235 files passed,
3 skipped; 28368 tests passed, 19 skipped. `typecheck` passed.
- Consumers that save pages through the door: `@objectstack/objectql`
(`default-record-page-save-door`, `protocol-meta`) 113 passed;
`@objectstack/rest` (`meta-alternate-door-read-gates`,
`meta-history-diff-authoring-door`, `meta-plural-i18n`, `rest`) 667
passed; `@objectstack/runtime` (`domains/meta-list-projection-parity`)
739 passed.
- `@objectstack/spec`: `vitest --project local` over `src/migrations`,
`src/conversions` and `src/stack-conversions-record.test.ts`, 763
passed; `test:repo`, 59 files, 1011 tests passed.
- **Ablation:** done with `scripts/ablation-replace.mjs` (anchor 1 → 0,
blob changed), then a lint rebuild and `ablation-dist-preflight` (marker
present in 4 built files).
- `validateComponentProps` back on `CLI_ONLY`: (a), the draft promotion
and (b) go red; (c) and the control stay green.
- `validateComponentTypes` back on `CLI_ONLY`: (c) and (b) go red, and
(a) goes red too (its `rulesRun` assertion names both rules).
- Restore leg: `git checkout HEAD`, hash equal to the HEAD blob,
rebuild, both markers `--absent`, and the 5 pins pass.
- **Derived gates** (`dispatch-gates --ran`): 89 derived; 85 run, 4 NOT
MEASURED, 0 unrun.
- Red: `check-empty-changeset` (by design, above).
- NOT MEASURED:
- `check-plugin-teardown-shape --self-test`: shallow clone, so the
pinned control commit is unreachable.
- `check:dual-build-cjs-loads`: needs a whole-repo build.
- `check:type-check-debt`: its re-measure build was OOM-killed (137) on
the shared box.
- `check:query-options-erasure`: the whole-repo eslint ratchet was
killed by a 240 s timeout.
- All other gates exit 0.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
The metadata save door runs the two page component rules that `objectstack validate`, `objectstack build` and `objectstack lint` run. A page saved through Studio, REST `/meta` or an MCP/AI author whose components carry an undeclared, wrong-typed or retired property (`component-props-unknown-key`, `component-props-invalid`), or a component `type` inside a spec-reserved namespace that the platform does not declare (`component-type-unknown`), is refused with `422 INVALID_METADATA`, and each issue carries the fix the CLI already prints. Before, the save answered `200` and the renderer ignored the property or drew a placeholder.
7
+
8
+
Clause-②: no (narrowing)
9
+
10
+
<!-- adr-0087: not-required (already-registered element-data-source-and-object-block-filter-rule-array, element-flat-data-binding-retired, element-text-variant-heading-subheading-retired, ui-action-group-menu-member-params-array-only, ui-action-group-menu-members-typed, ui-object-block-grouping-config-typed, ui-object-form-custom-fields-typed, ui-object-form-fields-names-typed, ui-object-form-members-typed, ui-object-form-sections-typed, ui-object-gantt-markers-typed, ui-object-grid-columns-typed, ui-object-grid-export-options-closed, ui-object-grid-kanban-calendar-list-members-typed, ui-object-grid-page-size-positive-integer-refused, ui-object-grid-row-members-typed, ui-object-kanban-conditional-formatting-typed, ui-object-map-gantt-tree-navigation-typed, ui-object-master-detail-form-details-closed, ui-object-metric-aggregate-trend-typed, ui-object-metric-compare-to-typed, ui-object-metric-drill-down-report-typed, ui-object-metric-drill-down-typed, ui-object-timeline-items-typed, ui-object-timeline-mapping-typed, ui-record-line-items-props-closed) Every props shape the save door now refuses was declared, and each narrowing of one recorded in its own D3 entry, before this change; the entries named here are the step's narrowings whose refusal the component-props gate carries, and this diff amends each one whose text said a stored page still saves, to say its next save meets that gate at the metadata save door. The change moves an existing rule onto one more door; it removes no key and rewrites no shape, so it adds no entry and no conversion: nothing on the load path refuses a page. -->
11
+
12
+
**BREAKING**: an accept-set narrowing on the metadata save door (`PUT /api/v1/meta/page/:name`, the draft promotion `POST /api/v1/meta/page/:name/publish`, the package publish, and every in-process `saveMetaItem` caller on an environment's authoring channel), shipped as `minor` under the launch-window convention for accept-set narrowings (`.changeset/pre.json` is open in `next` pre mode).
13
+
14
+
**The next save of a stored page whose components carry an undeclared, wrong-typed or retired property, or a reserved-namespace type the platform does not declare, is refused with the fix; stored pages keep serving until then.** The door judges writes only: a stored page row is not re-judged at boot, loads and is served as before, and a draft save is not judged until it is published.
15
+
16
+
## FROM → TO
17
+
18
+
| you saved | the door now answers | write instead |
19
+
|:--|:--|:--|
20
+
|`page:header` with `properties: { titel: 'Deals' }`|`422`, `component-props-unknown-key` at `…properties.titel`, offering `title`|`properties: { title: 'Deals' }`|
21
+
|`record:related_list` with `properties: { limit: 'five' }`|`422`, `component-props-invalid`, naming the expected type |`properties: { limit: 5 }`|
| a component typed `acme:pipeline_board` (outside the reserved namespaces) | unchanged: saved | nothing |
24
+
25
+
**The one-line fix: apply what the issue names, then save again.** If a component really does read the key, the props row in `@objectstack/spec` is what is wrong, not the page.
26
+
27
+
**Who is affected.** A deployment whose stored pages carry such a finding: those pages keep loading and serving, and only their next save is refused until the finding is fixed. `objectstack migrate meta --stored` does not list these findings. The authored pages measured when the props findings became errors (37 pages in this repository and 8 in `objectstack-ai/hotcrm`) carried none, and the in-repo page corpus carries no `component-type-unknown` finding; deployed rows were not measured. The two former ADR-0087 D2 aliases on page component properties, `page:header``description` (write `subtitle`) and an `element:button` inline `type: 'api'` action's object `params` (write `bodyExtra`), are retired from the load path at 18 and refused at every door with the rename (`objectstack validate`, load, and this save door), so the save door and `objectstack validate` agree on them.
28
+
29
+
The `@objectstack/spec` change is text only: the reason of each D3 migration entry named above (and the matching step-18 summaries) said a stored page still saves and loads, and now says it still loads and that its next save meets the component-props gate at the metadata save door.
Copy file name to clipboardExpand all lines: .changeset/pre/23103-page-action-row-on-success-and-executor.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -15,7 +15,7 @@ The page action rows are `action:button` and `action:icon` (`properties`), and e
15
15
-**`onSuccess`** was any value (`z.unknown()`). The runner reads only `ActionSchema`'s post-success block, `{ navigate, openIn }`, and classifies `navigate` with the url scheme set. So `onSuccess: { navigate: 'javascript:alert(1)' }` parsed clean, and so did a misspelled key or a callback shape, which the runner gives no reading at all. The rows now hold `ActionSchema.onSuccess`'s own block, the same instance by reference, so a row's verdict on any `onSuccess` value is the action's: the scheme set on `navigate`, the rename prescriptions for `url` / `redirectUrl` / `to` / `route` / `path` / `target` / `navigateTo` / `href`, and the closed `openIn` with its `'self'` default.
16
16
-**The executor key** (`actionType` on a node, `type` on a member) is a free string, and the rows judged a url `target` only when it read `'url'`. The runner also dispatches `'navigation'`, its local alias of `'url'`, to the same navigator, so `actionType: 'navigation'` with `target: 'javascript:…'` parsed clean. The url executor now has one spelling on the rows: `'navigation'` is refused at the executor key, with the rename onto `'url'`. The key otherwise stays open: a handler a host registers under another name (`navigate_create`, `navigate_edit`, …) is still dispatched by name.
17
17
18
-
A page's `properties` bag is outside the stack parse. `os validate` judges it through its props gate, which parses the bag against these same rows, so it reports each refusal at the row's own path with the same text, as a `component-props-invalid` finding (an error, like every finding of that gate). A stored page still saves and loads.
18
+
A page's `properties` bag is outside the stack parse. `os validate` judges it through its props gate, which parses the bag against these same rows, so it reports each refusal at the row's own path with the same text, as a `component-props-invalid` finding (an error, like every finding of that gate). A stored page still loads; the metadata save door runs the same gate, so the next save of a page carrying a refused row is refused with the same text.
Copy file name to clipboardExpand all lines: .changeset/pre/23147-component-props-every-finding-refused.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -16,7 +16,7 @@ Clause-②: no (narrowing)
16
16
-`component-props-unknown-key`: a key the component's props row does not declare, at the bag's own level, on a strict object or union arm below it, or on a strip-mode object below it.
17
17
-`component-props-invalid`: a value the props row rejects (a wrong type, a missing required prop, a closed-set refusal such as a url action's `target` scheme or a `record:details` section's `count`, a retired enum value), and a missing `dataSource.object` on `element:record_picker`, `element:number` and `element:repeater`.
18
18
19
-
**Not judged here.**The runtime publish door (the Studio, REST `/meta`, MCP) does not run this rule, so a page saved there is not refused for any of these. `defineStack` and the stack schema keep the open `properties` bag as written. A component `type` that `ComponentPropsMap` carries no row for (a `custom.*` or plugin type) is not judged.
19
+
**Not judged here.**`defineStack` and the stack schema keep the open `properties` bag as written. A component `type` that `ComponentPropsMap` carries no row for (a `custom.*` or plugin type) is not judged.
20
20
21
21
`ComponentPropsSeverity`, the type `@objectstack/lint` exports for these findings, narrows from `'error' | 'warning'` to `'error'`.
| SDUI component props — every `ComponentPropsMap` verdict: an undeclared prop, a value its props schema rejects, a missing `dataSource.object` binding, and a retired prop (`retiredKey()`) refused with its prescription (#23026, #23147) | ✓ | ✓ | ✓ |—|
473
+
| SDUI component props — every `ComponentPropsMap` verdict: an undeclared prop, a value its props schema rejects, a missing `dataSource.object` binding, and a retired prop (`retiredKey()`) refused with its prescription (#23026, #23147; at the save door #23388) | ✓ | ✓ | ✓ |✓ᵖ|
0 commit comments