Skip to content

Commit ecd2158

Browse files
claude[bot]claude
andauthored
chore(spec): re-grade ActionSchema operation / patch to live in the liveness ledger (#16784)
* wip: flip action operation/patch to live * chore(spec): regenerate liveness state-counts * chore(changeset): declare the published liveness-ledger move for the action declarative update --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b72226f commit ecd2158

3 files changed

Lines changed: 24 additions & 14 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
Liveness ledger: `ActionSchema.operation` and `ActionSchema.patch` re-graded `planned``live`, and their author warning dropped.
6+
7+
Both keys were seeded `planned` with an `authorWarn` whose hint said, in as many words, that "nothing performs the write yet". That premise is gone: the runtime half of the declarative row-level field write is merged, so the ledger now says what the tree does.
8+
9+
- **`operation``live`.** It is the executor's own discriminator (`isDeclarativeUpdateAction`, a bare read of the declared key) and it is consulted *before* `type` at every reader: the REST `/actions` door, the MCP `run_action` door, the headless-invokability predicate, the type-error prescription, and the MCP listing summary.
10+
- **`patch``live`.** `declarativeUpdateWrite` reads it as the base of the write bag `{ ...patch, ...params }` — the static values sit *under* the ones the dialog collected — and `executeDeclarativeUpdateAction` hands that bag to a single data-plane update of the routed row, under the caller's own execution context.
11+
12+
Judged separately and both measured, not inferred: deleting the `operation` read fails 24 of the executor's 27 pins, deleting the `patch` read fails 16 of them, and the unmutated tree passes all 27.
13+
14+
**What moves for consumers.** `@objectstack/spec` ships `liveness/` in its published files, and `@objectstack/lint` resolves that directory off the installed package to build its author-warning map. Dropping `authorWarn` on these two rows therefore removes a real `os lint` finding: authoring `operation: 'update'` + `patch` no longer draws `liveness-planned-property`. Nothing else moves — no schema, no `.describe()`, no export, no accept-set change.

packages/spec/liveness/action.json

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,22 +42,18 @@
4242
"note": "api/script/flow wired; url thinner; modal PARTIAL (maps to serverActionHandler, not a real modal); form LIVE via objectui ActionRunner.executeForm (routes a type:'form' action to the FormView at /forms/:target, forwarding the current record id) — the 2026-06 audit mis-classified as dead (objectui renderer not re-verified; fixed the 'Log Time does nothing' report). Build-time lint-view-refs.ts validates the form target resolves to a form view. RE-VERIFIED 2026-07 (#3714 follow-up): `api` -> executeAPI (:974, string-or-ApiConfig endpoint, method/headers/queryParams/responseType) and `form` -> executeForm (:920) both resolve; content/docs/ui/actions.mdx had been telling authors the opposite (\"no runtime executor / renderer today\") and was corrected in the same pass. 2026-08-26: REPOINTED (framework half only) — the evidence led with packages/runtime/src/http-dispatcher.ts, which reads this key nowhere. That file's 9 word-`type` occurrences are ALL something else: four are the TypeScript `import type` keyword (:9, :13, :14, :21) and the rest are other domains' prose or data (`error.type` :862, a field-type→JSON-Schema mapper :926, `details.type` :1156, a metadata type list :1634, an inbox query param :1765). The action consumer was extracted into domains/actions.ts + action-execution.ts (+ the sandbox body-runner, which is where the type gate has always lived) and http-dispatcher.ts now only delegates (handleActions → handleActionsRequest at :1969-1970). WHY THE GATE COULD NOT SEE THE ROT — and this is the part that differs from the `target`/`requiredPermissions` siblings repointed the day before: those were caught because their cited file had 0 occurrences of the key, whereas `type` is a common English word AND a TypeScript keyword, so the word-bounded key-mention check anchors on the coincidence and PASSES. This entry was repaired by a hand call-graph read, not by tooling; the check's designed, honest limit is what left it standing. The objectui half is unchanged and stays as measured @732b1bf (2026-07-28) — it was not re-graded here. 2026-08-28: RE-ANCHORED (#13003, adoption of the #12516 grammar) — re-closed by hand against c459da6bc. All six framework consumers stand and none had left its file, so this is a grammar migration rather than a repair; what it DOES repair is a second, quieter gap: `:524`/`:552`/`:906` were written as bare line suffixes with no path in front of them, which the evidence scanner cannot parse as citations at all (`PATH_RE` needs a repo-rooted token), so three of this entry's six consumers were unfalsifiable prose that no check has ever resolved. They are now anchors of their own."
4343
},
4444
"operation": {
45-
"status": "planned",
46-
"verifiedAt": "2026-09-03",
45+
"status": "live",
46+
"verifiedAt": "2026-09-08",
4747
"evidenceScope": "in-repo",
48-
"evidence": "packages/spec/src/ui/action.zod.ts#refuseDeclarativeUpdateContradictions (the mixing rule: every executor-binding key beside `operation: 'update'` is refused at its own path, and `patch` without it); packages/spec/src/stack.zod.ts#collectGlobalUpdateActionErrors (a standalone `operation: 'update'` action without `objectName` is refused by defineStack)",
49-
"authorWarn": true,
50-
"authorHint": "`operation: 'update'` + `patch` parse (the row-level declarative field write ruled on #14092) but nothing performs the write yet: the runtime action dispatcher and the objectui row-action executor are the downstream cards the spec seat files at ACCEPT (Blocked-by #14092). Until they land the button reaches the platform action route with no handler and fails loudly at click; it never writes silently.",
51-
"note": "The discriminator of the declarative single-record field write — one member, `'update'`; `'delete'`/`'custom'` are refused with the reason. PLANNED, deliberately not `live` — the spec-first contract-split convention (the `onSuccess` precedent). Declared by the #14092 maintainer ruling (2026-09-01): the row-level counterpart of a list view's `bulkActionDefs` `operation: 'update'`, spelled with the same words. EXECUTOR CONTRACT the downstream halves must honour: a single-record data-plane update of the CURRENT record, executed AS THE CALLER and never system-elevated (the #14010 `runAs: 'user'` direction, consumed not reopened) — the caller's object/row/field permissions, the object's hooks and validations fire as for a user edit; a caller who cannot read or write the row is refused (the #14143 class); `patch` is merged UNDER the collected `params`; `undoable` captures the prior values of exactly the fields written. The console's action-button forward list does not carry these keys yet and `ActionRunner` dispatches the action on its default `type: 'script'` route, which is where the runtime half will branch on `operation` before looking for a handler. Amend to `live` citing the runtime dispatch branch and the objectui forward/undo reads when those halves land."
48+
"evidence": "packages/runtime/src/action-execution.ts#isDeclarativeUpdateAction (the executor's own discriminator — a bare `action?.operation === DECLARATIVE_UPDATE_OPERATION`, deliberately with no `type` clause so data at rest whose `type` contradicts its `operation` is still routed by `operation`); packages/runtime/src/domains/actions.ts#handleActionsRequest (the REST `/actions` door: the branch runs ahead of the `type` switch and ahead of the trusted-mode plumbing, and hands off to `executeDeclarativeUpdateAction`); packages/runtime/src/action-execution.ts#invokeBusinessAction (the MCP `run_action` door takes the same branch into the same executor — one implementation, two doors); packages/runtime/src/action-execution.ts#isHeadlessInvokableAction (asked FIRST, so a declarative update is listed as invokable although it carries neither `target` nor `body`); packages/runtime/src/action-execution.ts#headlessActionTypeError (asked FIRST, so the key suppresses the `type` prescription that would otherwise be wrong for it); packages/runtime/src/action-execution.ts#summarizeAction (the MCP listing face projects the declared `operation` and forces `requiresRecord`); packages/spec/src/ui/action.zod.ts#refuseDeclarativeUpdateContradictions (authoring-time: every executor-binding key beside `operation: 'update'` is refused at its own path, and `patch` without it); packages/spec/src/stack.zod.ts#collectGlobalUpdateActionErrors (defineStack refuses a standalone `operation: 'update'` action with no `objectName`)",
49+
"note": "The discriminator of the declarative single-record field write — one member, `'update'`; `'delete'`/`'custom'` are refused with the reason. FLIPPED `planned` -> `live` 2026-09-08 (#15080) on the runtime half #15079 (PR #15448, merged 2026-09-04): the key now decides dispatch at both server doors, and it is read BEFORE `type` at every one of them — contract point 1 of the executor contract PR #15077 pinned here, and a rule about ORDER, not merely a new branch. Declared by the #14092 maintainer ruling (2026-09-01): the row-level counterpart of a list view's `bulkActionDefs` `operation: 'update'`, spelled with the same words. The EXECUTOR CONTRACT is now honoured in tree rather than promised: the write is a single data-plane update of the CURRENT record executed AS THE CALLER under `wiring.ec` and never `buildActionExecutionContext` (the #14010 `runAs: 'user'` direction), so object/row/field permissions, hooks and validations fire as for a user edit and a caller who cannot read the row is refused (the #14143 class); `patch` merges UNDER the collected `params`; `undoable` captures the prior values of exactly the fields written; an action with no current record is a located 400, never a silent no-op. Pinned end-to-end in packages/runtime/src/action-declarative-update.test.ts, one describe per contract point plus a reverse pin that a handler-less `type: 'script'` action WITHOUT `operation` is unchanged. `evidenceScope: in-repo` is exact and deliberate: this verdict rests on the framework-side readers alone. The console half (objectui#7551) is a SECOND reader and the flip did not wait for it — objectui's `ActionRunner` still dispatches on `type` — so a console-side forward/undo pointer is a later widening of this row's scope, not a precondition of its verdict."
5250
},
5351
"patch": {
54-
"status": "planned",
55-
"verifiedAt": "2026-09-03",
52+
"status": "live",
53+
"verifiedAt": "2026-09-08",
5654
"evidenceScope": "in-repo",
57-
"evidence": "packages/spec/src/ui/action.zod.ts#refuseDeclarativeUpdateContradictions (refused on an action without `operation: 'update'` — it would be silently dropped; `operation: 'update'` with neither `patch` nor `params` is refused as nothing-to-write)",
58-
"authorWarn": true,
59-
"authorHint": "`operation: 'update'` + `patch` parse (the row-level declarative field write ruled on #14092) but nothing performs the write yet: the runtime action dispatcher and the objectui row-action executor are the downstream cards the spec seat files at ACCEPT (Blocked-by #14092). Until they land the button reaches the platform action route with no handler and fails loudly at click; it never writes silently.",
60-
"note": "The static field values of the declarative write, merged UNDER the collected `params` (a param of the same name wins). Passed through verbatim — no transform, no field-existence check here (a lint reference diagnostic). PLANNED, deliberately not `live` — the spec-first contract-split convention (the `onSuccess` precedent). Declared by the #14092 maintainer ruling (2026-09-01): the row-level counterpart of a list view's `bulkActionDefs` `operation: 'update'`, spelled with the same words. EXECUTOR CONTRACT the downstream halves must honour: a single-record data-plane update of the CURRENT record, executed AS THE CALLER and never system-elevated (the #14010 `runAs: 'user'` direction, consumed not reopened) — the caller's object/row/field permissions, the object's hooks and validations fire as for a user edit; a caller who cannot read or write the row is refused (the #14143 class); `patch` is merged UNDER the collected `params`; `undoable` captures the prior values of exactly the fields written. The console's action-button forward list does not carry these keys yet and `ActionRunner` dispatches the action on its default `type: 'script'` route, which is where the runtime half will branch on `operation` before looking for a handler. Amend to `live` citing the runtime dispatch branch and the objectui forward/undo reads when those halves land."
55+
"evidence": "packages/runtime/src/action-execution.ts#declarativeUpdateWrite (`const patch = action?.patch` — the ONE producer of the write bag `{ ...patch, ...params }`; contract point 4, the static patch sits UNDER the values the dialog collected so a param of the same name wins, and nothing else from the action is merged); packages/runtime/src/action-execution.ts#executeDeclarativeUpdateAction (calls it and hands the bag to a single data-plane `update` of the routed row under the CALLER's own execution context; an empty bag is refused with a located 400 rather than answered 200 for a no-op, and `undoable` reads back the prior value of exactly the keys the bag names) — reached from both server doors, `handleActionsRequest` (REST) and `invokeBusinessAction` (MCP `run_action`); packages/spec/src/ui/action.zod.ts#refuseDeclarativeUpdateContradictions (authoring-time: `patch` is refused on an action without `operation: 'update'` — it would be silently dropped; `operation: 'update'` with neither `patch` nor `params` is refused as nothing-to-write)",
56+
"note": "The static field values of the declarative write, merged UNDER the collected `params` (a param of the same name wins). Passed through verbatim — no transform, no field-existence check here (a lint reference diagnostic). FLIPPED `planned` -> `live` 2026-09-08 (#15080) on the runtime half #15079 (PR #15448, merged 2026-09-04): the declared object is now the base of the bag one data-plane `update` writes, so authoring a key here changes what lands on the row. Declared by the #14092 maintainer ruling (2026-09-01): the row-level counterpart of a list view's `bulkActionDefs` `operation: 'update'`, spelled with the same words. The `patch`-UNDER-`params` precedence is the bulk def's own rule mirrored word for word, and it is pinned both ways — through the door and as the pure function — in packages/runtime/src/action-declarative-update.test.ts, beside the pin that the wire cannot widen the bag (an undeclared param is refused by ADR-0104 D2) and the pin that `undoable` restores exactly the keys this bag names. `evidenceScope: in-repo` is exact and deliberate: the verdict rests on the framework-side executor alone; the console half (objectui#7551) is a SECOND reader the flip did not wait for."
6157
},
6258
"target": {
6359
"status": "live",

packages/spec/liveness/state-counts.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ for both corollaries.
3030
| `object` | 51 | 0 | 0 | 0 | 1 | 52 |
3131
| `field` | 90 | 0 | 0 | 1 | 2 | 93 |
3232
| `flow` | 34 | 0 | 0 | 6 | 0 | 40 |
33-
| `action` | 41 | 0 | 0 | 3 | 4 | 48 |
33+
| `action` | 43 | 0 | 0 | 3 | 2 | 48 |
3434
| `hook` | 19 | 0 | 0 | 3 | 0 | 22 |
3535
| `permission` | 36 | 0 | 0 | 6 | 0 | 42 |
3636
| `position` | 12 | 0 | 0 | 0 | 0 | 12 |
@@ -63,4 +63,4 @@ for both corollaries.
6363
| `batch_endpoints` | 5 | 0 | 0 | 2 | 0 | 7 |
6464
| `route_generation` | 0 | 0 | 0 | 4 | 0 | 4 |
6565
| `realtime_subscription` | 0 | 0 | 0 | 6 | 0 | 6 |
66-
| **total** | **848** | **5** | **1** | **92** | **12** | **958** |
66+
| **total** | **850** | **5** | **1** | **92** | **10** | **958** |

0 commit comments

Comments
 (0)