From d88b35f103327423a06081d61df778dd201a7170 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 04:33:07 +0000 Subject: [PATCH 1/3] wip: flip action operation/patch to live --- packages/spec/liveness/action.json | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/spec/liveness/action.json b/packages/spec/liveness/action.json index 7c33a2755c..d4f70f9b3a 100644 --- a/packages/spec/liveness/action.json +++ b/packages/spec/liveness/action.json @@ -42,22 +42,18 @@ "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." }, "operation": { - "status": "planned", - "verifiedAt": "2026-09-03", + "status": "live", + "verifiedAt": "2026-09-08", "evidenceScope": "in-repo", - "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)", - "authorWarn": true, - "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.", - "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." + "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`)", + "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." }, "patch": { - "status": "planned", - "verifiedAt": "2026-09-03", + "status": "live", + "verifiedAt": "2026-09-08", "evidenceScope": "in-repo", - "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)", - "authorWarn": true, - "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.", - "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." + "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)", + "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." }, "target": { "status": "live", From f157b626a120a4bdd525b5e99b1f15b50ef463d8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 04:46:58 +0000 Subject: [PATCH 2/3] chore(spec): regenerate liveness state-counts --- packages/spec/liveness/state-counts.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/spec/liveness/state-counts.md b/packages/spec/liveness/state-counts.md index 85b774d338..da72b8c131 100644 --- a/packages/spec/liveness/state-counts.md +++ b/packages/spec/liveness/state-counts.md @@ -30,7 +30,7 @@ for both corollaries. | `object` | 51 | 0 | 0 | 0 | 1 | 52 | | `field` | 90 | 0 | 0 | 1 | 2 | 93 | | `flow` | 34 | 0 | 0 | 6 | 0 | 40 | -| `action` | 41 | 0 | 0 | 3 | 4 | 48 | +| `action` | 43 | 0 | 0 | 3 | 2 | 48 | | `hook` | 19 | 0 | 0 | 3 | 0 | 22 | | `permission` | 36 | 0 | 0 | 6 | 0 | 42 | | `position` | 12 | 0 | 0 | 0 | 0 | 12 | @@ -63,4 +63,4 @@ for both corollaries. | `batch_endpoints` | 5 | 0 | 0 | 2 | 0 | 7 | | `route_generation` | 0 | 0 | 0 | 4 | 0 | 4 | | `realtime_subscription` | 0 | 0 | 0 | 6 | 0 | 6 | -| **total** | **848** | **5** | **1** | **92** | **12** | **958** | +| **total** | **850** | **5** | **1** | **92** | **10** | **958** | From 12f96c0127f7088127c6256b3aec605663f71741 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 05:15:50 +0000 Subject: [PATCH 3/3] chore(changeset): declare the published liveness-ledger move for the action declarative update --- .../action-declarative-update-ledger-live.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .changeset/action-declarative-update-ledger-live.md diff --git a/.changeset/action-declarative-update-ledger-live.md b/.changeset/action-declarative-update-ledger-live.md new file mode 100644 index 0000000000..66af161731 --- /dev/null +++ b/.changeset/action-declarative-update-ledger-live.md @@ -0,0 +1,14 @@ +--- +"@objectstack/spec": patch +--- + +Liveness ledger: `ActionSchema.operation` and `ActionSchema.patch` re-graded `planned` → `live`, and their author warning dropped. + +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. + +- **`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. +- **`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. + +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. + +**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.