Skip to content

Commit 69f342b

Browse files
feat(spec)!: retire a page action row's own patch — a row's operation: 'update' runs the declared action its name names (#24394) (#24442)
Fixes #24394 Clause-②: yes (narrowing) ## What A page action row's `operation: 'update'` runs the declared action its `name` names. The platform action route resolves that declaration by name (ADR-0110 D2, "resolve, then address: declaration first") and writes the declaration's own `patch`, merged under the request's params, which it judges against the declaration (ADR-0104 D2, landed as `d79dc4883c`). The row's own `patch` reached no request. This PR takes route B from the seat's claim `6257461893`: the row's `patch` retires, and the rows' describes and their half of the declarative-update judge are re-trued to what the route writes. | surface | change | | --- | --- | | `packages/spec/src/ui/component.zod.ts` | `patch` becomes a `retiredKey()` tombstone on `action:button`, `action:icon` and the shared `action:group` / `action:menu` member (`rowPatchRetired`, which replaces the by-reference `actionPatch`). The three `operation` describes, the node `params` describes and the three `name` describes now say the row runs its named action, needs a `name` beside `operation: 'update'`, and that its static `params` are written only where that action declares them. `rowDeclarativeUpdate` passes `writesNamedAction: true`. | | `packages/spec/src/ui/action-declarative-update.ts` | `staticParams` retires; the surface flag is `writesNamedAction`. On a row, rule 1 ("`patch` without `operation`") stands down, because the tombstone refuses every row `patch`. Rule 4 ("nothing to write") asks for a non-empty `name`, refused at `name`. On `ActionSchema` both rules and their texts are unchanged. | | `packages/spec/src/ui/action.zod.ts` | one line: the surface descriptor's `staticParams: false` becomes `writesNamedAction: false` (behaviour-neutral) | | `packages/spec/src/conversions/registry.ts` | D2 conversion `page-action-row-patch-removed` (toMajor 18, `retiredFromLoadPath`, `retiredAfter: '17.7.0'`, order 70), stripping from every node and member, scoped by component type | | `migrations/entries/retired-keys/18.ui__ActionButtonProps__patch.ts`, `18.ui__ActionIconProps__patch.ts` | `RETIRED_KEYS_BY_MAJOR[18]` entries. The member's keys have no authorable-surface row of their own, so build gate (b) named exactly these two. | | `migrations/entries/semantic/18.ui-page-action-row-patch-retired.ts` + a `STEP18_RATIONALE` fragment (order 104, after the merge of `538f923cb9`, beside #24417's order-103 fragment) | ADR-0087 D3 entry | | `migrations/entries/semantic/18.ui-page-action-row-{operation-declared,declarative-update-rules}.ts`, `18.ui-action-group-menu-members-typed.ts` | same-major D3 texts that still prescribed a row `patch`, re-trued so the 18 upgrade guide does not contradict itself | | `packages/spec/liveness/page.json` | the `regions` row's evidence segment [23]: the row `patch` clause now records the measured read (Undo only, never posted) and the tombstone. Status and `verifiedAt` are unmoved. | | generated | `authorable-surface/ui.json` (two `[RETIRED]` lines), `content/docs/references/ui/component.mdx` | | `.changeset/24394-page-action-row-patch-retired.md` | `@objectstack/spec` minor, BREAKING, FROM → TO, one-line fix, ADR-0087 `registered` marker | ## Premises P1–P3 (tested before building; all hold) - **P1, the row's `patch` reaches no write request.** objectui at the pin `0707b4ae37` and at `main` `4b7ce47` has identical code. `ActionRunner.executeUpdateOperation` (pin `packages/core/src/actions/ActionRunner.ts:1782`, the `patch` read at `:1799`–`:1802`; main `:1801`, `:1818`–`:1821`) reads `action.patch` only into `written`, which feeds the Undo snapshot. The request is `wire = { ...collected }`. `createServerActionHandler` (`packages/core/src/actions/serverActionHandler.ts`, pin `:208`–`:243`) posts `{ recordId, params }` to `/api/v1/actions/OBJECT/NAME`, and refuses a nameless action before any request ("Action has no name — a server action must declare one"). A second Undo-only read: `packages/core/src/utils/predicate-fields.ts:155` (`undoableWrittenKeys`). - **P2, no named producer.** No page action row appears in `examples/**` or `packages/apps/**`. Control: the same grep finds `element:button` / `page:header` / `record:*` rows in 10 example files. In objectui, the page block inspector has no curated editor for any action row (`previews/block-config.ts`, pin and main), and its Advanced editor edits present keys only. The registry's `patch` input on `action:button` / `action:icon` (`action-button.tsx:591`, `action-icon.tsx:422`) is an advertisement, not an authored instance. - **P3, no typed read of the spec row type's `patch` at the pin.** objectui non-test source imports `ActionButtonProps` only for `size` (`packages/types/src/ui-action.ts:900`), and the four row schemas whole into `packages/types/src/zod/public-blocks.zod.ts` (`propsBag`). That helper already composes tombstoned rows (`page:header`, `element:number`). `UIActionSchema.patch` is `SpecAction['patch']`, which is `ActionSchema`'s and untouched. The first P3 grep was truncated by `head` and missed the `ActionButtonProps` import; the untruncated re-read above is the reading. ## Clause-② measured on the built rows Both directions. Narrowing: every row `patch` is refused, and a row with `operation: 'update'` and no `name` (an input list or static `params` included) is refused at `name`. Widening: a row with `operation: 'update'` and a `name` but no `patch` and no `params` now parses; before, it was refused at `patch` as having nothing to write. So the claim's provisional `yes (narrowing)` stands. ## The row half of rule 4, read as "names its action" The ruling says the row half re-trues to "the row's write is its named action's `patch`". Read literally, a row with no `name` has no named action and nothing to write: the console refuses it before any request, and the route has no declaration to resolve. So the row's rule now refuses a nameless `operation: 'update'` row at `name`, and accepts a named one with no `patch` and no `params`. The alternative reading is dropping the row half entirely. It would have newly admitted a bare nameless `{ operation: 'update' }` that the console refuses at click. That is raised for the seat in the report. ## Pins - **New:** `packages/spec/src/ui/component-action-row-patch-retirement.test.ts`, in the `repo` project (listed in `vitest.repo-tests.json`). It covers: - every `patch` value on all four rows is ONE `invalid_type` issue at the key, with the prescription, and no `unrecognized_keys` (strict-arm control); - a named update row with no `patch` parses; - the type-level `never` (`@ts-expect-error`) and the walked shape keeping the key; - `ActionSchema.patch` untouched; - the page parse still accepts a stored row; - the D2 strip for stored and built rows, with its type scope, idempotence and load-path retirement; - the ledger rows; - a tree-scoped absence pin over the radius `@objectstack/spec` already declares, with a structural matcher (a `patch` key inside an object whose `type` is a page action row) and anti-vacuity cases. - **Reworked:** `component-action-row-declarative-update.test.ts` keeps the three members a row shares with the action (2, 3, 5) under ONE TEXT. Members 1 and 4 move to the row's own surface: "nothing to write is no `name`", "`params` are no write of the row's own", and "the action's half does not move". - **Re-spelled fixtures** (they used a row `patch` only as a way to write a valid update): `component-action-row-on-success-executor.test.ts` and `packages/lint/src/validate-component-props-row-on-success.test.ts`. - **Ledger:** `component-props-unknown-members.pin.test.ts` drops the four row `patch{}` lines (the member is `never` now). `retired-key-removal-version.pin.test.ts` gains the JOIN row. - **Lint:** `validate-component-props.test.ts` pins `component-props-invalid` + `error` + path + prescription on each node and member, and the nameless-row refusal at `properties.name`. ## Tests and gates (union run after the final commit, head `d6976f1c92`) - `@objectstack/spec`: `pnpm test` gave 696 files, 21258 passed. `typecheck` exit 0, test layer included: the `@ts-expect-error` compiles against an identity-pinned debt ledger that held. - `@objectstack/lint` (downstream of spec): `test` 145 files, 6866 passed; `typecheck` exit 0. - `@objectstack/metadata-protocol` (downstream, the page save door): `test` 246 files, 20655 passed. - `test:repo` for spec, narrowed and declared: the full repo project exceeded the 10-minute foreground limit (SIGTERM). The new pin and its sibling ran under `--project repo`: 2 files, 47 passed. The full set is `Test Core`'s. - `node scripts/pm/dispatch-gates.mjs --ran` reported 113 derived, 113 run, 0 NOT-MEASURED, 0 UNRUN. Every exit code was captured before any pipe; one family recorded exit 1 (below). `node scripts/check-platform-checklist.mjs --changed-from origin/main`: OK, 4 anchors judged, 0 lost. Local-only `check:dev-prereqs`: exit 0. - Ablation, one shot per behaviour (both legs through `scripts/ablation-replace.mjs`, anchor count x1 → x0, restored by blob hash and empty `git diff HEAD`). A1 reverted the rows' rule 4 to the action's: 78 tests went red. A2 replaced the tombstone with a record: the 2 tombstone pins went red. - Reverse verification of the rebuilt `.d.ts`: a throwaway file in `packages/lint/src` typed an `ActionButtonProps` literal with `patch`. `tsc` went red on that literal only (TS2353 against the tombstone's `never` brand); the control literal compiled. The file was deleted and the tree verified clean. ## Acceptance notes - **Pre-existing main red, not this diff.** `pnpm check:platform-checklist` exits 1 on `origin/main` (`e22defb13c`) and at this branch's base. `docs/qa/platform-checklist/areas/access-security.json` anchors `packages/core/src/security/resolve-authz-context.ts#legacyGrantAnchorRetired`, which `b2e78701f7` (PR #24373) deleted. Neither file is in this diff. The gate is not in per-PR CI (`lint.yml`, the maintainer decision recorded at its checklist step); its watchdog carries the red (#24438). - **objectui still reads the row `patch`**, outside this PR's surface: the four renderers forward it (`action-button.tsx:323`, `action-icon.tsx:191`, `action-group.tsx:371`, `action-menu.tsx:304`). The runner's Undo snapshot (`ActionRunner.ts:1799`) and `undoableWrittenKeys` (`predicate-fields.ts:155`) read it, and the registry lists a `patch` input. So the tracked `sdui.manifest.json` advertises `patch` on `action:button` / `action:icon` to the html-tier JSX gate until the pin moves. After this lands, the Undo snapshot of a row with no `patch` covers only collected params, not the named action's `patch` the route wrote. That is the follow-up the seat files. - `main` was not merged before opening: the dispatch limits merges to a reported conflict, a main-caused red or a queue removal. The two incoming commits touch 11 files, none under `packages/spec` and none in this diff. --- _Generated by [Claude Code](https://claude.ai/code/session_01WnW7HvfnfHbLg1fpkHX3Mu)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 26501ae commit 69f342b

23 files changed

Lines changed: 1126 additions & 159 deletions
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: retire a page action row's own `patch` — a row's `operation: 'update'` runs the declared action its `name` names, and the platform action route writes that action's `patch` (#24394)
6+
7+
**BREAKING** — `patch` on a page action row (`action:button`, `action:icon`, and each `action:group` / `action:menu` member of `properties.actions`) is now refused. A row's `operation: 'update'` posts to the platform action route under the row's `name`. The route resolves that declared action by name and writes the action's own `patch`, merged under the request's params. The row's `patch` was never sent: the reference console read it only for its Undo snapshot. Beside `operation: 'update'` a row now needs a `name`, since without one it has no declared action to run and the console refused it before any request. A row's static `params` are written only where that action declares them, and the route refuses any other with a 400 (ADR-0104 D2). `ActionSchema.patch`, the action's own declaration, is unchanged.
8+
9+
Clause-②: yes (narrowing)
10+
11+
Both directions, measured on the built rows. Narrowing: every row `patch` is refused, and a row with `operation: 'update'` and no `name` (an input list or static `params` included) is refused at `name`. Widening: a row with `operation: 'update'` and a `name` but no `patch` and no `params` now parses; before, the rows refused it at `patch` as having nothing to write, though its write is the named action's `patch`.
12+
13+
Measured before removal, at `71fa166e96`: no producer. No page action row in this repository's examples or apps authors `patch` (or any `operation`), and neither does any objectui page row at the pinned `0707b4ae37` or at objectui `main` `4b7ce47`.
14+
15+
## FROM → TO
16+
17+
| you wrote (17.7 and earlier) | write instead |
18+
| --- | --- |
19+
| `operation: 'update'` with a `patch` on a page action row | the values in the `patch` of the declared action the row's `name` names (`defineAction({ name, operation: 'update', patch })`), the row keeping only `operation: 'update'` and that `name` |
20+
| `patch` on a page action row with no `operation` | nothing — delete the key; it was already refused |
21+
| `operation: 'update'` on a page action row with no `name` | a `name` naming the declared `operation: 'update'` action |
22+
23+
**The one-line fix:** move each row's `patch` values onto the declared action its `name` names, then delete the row's `patch`.
24+
25+
**What an author who still writes it sees.** `tsc` fails at a typed authoring site (the rows type the key as retired), and `os validate`, `os build` and `os lint` refuse it as an `error` (`component-props-invalid`) at its own path (`properties.patch`, `properties.actions.N.patch`) with the prescription:
26+
27+
> An `action:button`'s `patch` was removed in @objectstack/spec 18 (ADR-0110 D2) — a page action row's `operation: 'update'` runs the declared action its `name` names, and the platform action route resolves that action by name and writes ITS `patch`, so the row's own `patch` was never sent and never written. Move the values into the `patch` of the action the row's `name` names, then delete the key. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.
28+
29+
A page's own parse keeps its component `properties` as an open bag, so an existing page carrying the key is never refused at load.
30+
31+
## The retirement kit
32+
33+
- **A `retiredKey()` tombstone** at `patch` on `ActionButtonProps`, `ActionIconProps` and the `action:group` / `action:menu` member. `RETIRED_KEYS_BY_MAJOR[18]`: `ui/ActionButtonProps:patch`, `ui/ActionIconProps:patch` (the member's keys have no surface row of their own). The authorable-surface rows read `[RETIRED]`.
34+
- **The rows' half of the declarative-update judge** reads the row's own surface: a row has no "`patch` without `operation`" rule of its own (its tombstone refuses every `patch`), and its "nothing to write" rule asks for the `name`. `ActionSchema`'s half of every rule, and every text it prints, is unchanged.
35+
- **The D2 conversion `page-action-row-patch-removed`** (protocol 18, retired from the load path) deletes the key from every node and member — region-level, nested in a container's `children`, and in a slotted page's named slot — one notice per row. It is scoped by component `type`: the same key name on another component is left alone.
36+
- **The D3 entry `ui-page-action-row-patch-retired`** records the judgement the strip cannot make: when the named action's own `patch` lacks a value the row carried, that value was never written, and adding it to the action changes what the action writes.
37+
- **No deprecation window**, per the project's startup-stage posture.
38+
39+
**Not part of it:** objectui's renderers still forward the row's `patch`, its runner still reads it for the Undo snapshot, and its component registry still lists a `patch` input on `action:button` / `action:icon` (so the tracked `sdui.manifest.json` advertises it to the html-tier page gate until the pin moves). Those reads leave on the objectui side.
40+
41+
⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is published, so this is breaking for consumers no telemetry was consulted for.
42+
43+
<!-- adr-0087: registered page-action-row-patch-removed, ui-page-action-row-patch-retired -->

0 commit comments

Comments
 (0)