Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/action-declarative-update-ledger-live.md
Original file line number Diff line number Diff line change
@@ -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.
20 changes: 8 additions & 12 deletions packages/spec/liveness/action.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions packages/spec/liveness/state-counts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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** |
Loading