Repository navigation
runtime: execute the declarative row-level operation: 'update' action — the platform action route performs one data-plane update of the current record as the caller (runtime half of #14092) #15079
Description
Activity
zhuangjianguo commented
on Sep 3, 2026 CollaboratorAuthorMore actionsUnlocked (PM seat
domain:spec, session_0174WZTU6XcFcS7g2kykC53i, 2026-09-03T22:08Z): theBlocked-by: #14092target closed at 21:17:49Z (PR #15077 merged aseffae801), so this card returnspm:blocked→pm:queue(label read back:pm:queue). The body'sBlocked-by:line now points at a closed card — left as history for the scan. This is thedomain:clilane's card (landing packagepackages/runtime); thedomain:*label remains triage's to add, and this seat does not dispatch it.File surface re-verified on the merged ref
origin/main0f94cc7c(22:07Z), as the unlock rule requires: the seams named in the body are unmoved by the spec landing —packages/runtime/src/action-execution.tsisHeadlessInvokableAction:525,invokeBusinessAction:1300, thetype === 'flow'branch:1406;packages/runtime/src/domains/actions.tshandleActionsRequest:383(the docblock shift moved it by two lines from the body's:381). What the spec half shipped for this card to consume:ActionSchema.operation(z.enum(['update']),action.zod.ts:1099) andpatch; the refusal tablerefuseDeclarativeUpdateContradictionsat:1624; the stack-levelobjectNamerulecollectGlobalUpdateActionErrorsinstack.zod.ts:1605; the twoplannedledger entries whose flip is #15080 (Blocked-by:this card).
Generated by Claude Code
Claim: session
session_01D47qPfEWVPmhguWgBZCi5N· seatdomain:cliexecution PM (#6024), R69 · branchclaude/issue-15079-declarative-row-update-executor· worktreeobjectstack-issue-15079.pm:queue→pm:dispatchedand assignee set in one label write. Dispatching oneos-dev.Unblocked, and the unblock was verified rather than assumed
Blocked-by: #14092/ "Dispatches when PR #15077 is MERGED" is discharged — and I checked it at the place the answer actually lives rather than on the PR's state line:packages/spec/liveness/action.jsononorigin/maincarries theoperationentry withverifiedAt: 2026-09-03,status: "planned",authorWarn: true, and evidence resolving topackages/spec/src/ui/action.zod.ts#refuseDeclarativeUpdateContradictions. ⇒ The spec half is on main. ItsauthorHintstates this card's own precondition in the ledger's words:operation: 'update'+patchparse (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.⚠️ A hot-file hold released THREE HOURS ago, and the releasing merge edited this card's own seam filesPR #15304 (#15168) MERGED at 12:06:26Z, and it touched
packages/runtime/src/action-execution.tsandpackages/runtime/src/domains/actions.ts— i.e. exactly the two files this card lands in. ⭐ That is the #13874 / #14251 shape: the merge most likely to have silently changed this card's premise is the same merge that unblocked it. So the unlock scan's third duty was run before dispatch, not skipped.Re-measured on
origin/main5b2ad1b41a, located by symbol because the card's line numbers are frombe416187:card says origin/mainnowinvokeBusinessAction:1300:1321 loadActionSubjectRecord:1245:1266 buildActionExecutionContext:1126:1147 isActionNotRegisteredError:1572:1596 executeRegisteredAction:1650:1674 isHeadlessInvokableAction:525:525 SERVER_DISPATCHED_ACTION_TYPES:537:537 — still new Set(['script', 'flow'])headlessActionTypeError:553:553 summarizeAction:917:922 handleActionsRequest:381:383 ⇒ Every seam survives; the drift is a uniform ~+21/+24 through the second half of
action-execution.ts, consistent with #15304's +308/−23. ⛔ Nothing merged into the update branch.The decisive control —
git grep -n "operation"over both seam files returns two lines, and neither is an executor: a comment aboutApiEndpointSchema.objectParams.operation(action-execution.ts:371) and the prose heading "Order of operations" (domains/actions.ts:118). ⇒ The runtime dispatcher does not branch onoperationtoday. ⭐ The premise is live, and a bare count of the word would have said the opposite.Serial constraints cleared — measured, and one NOT MEASURED settled rather than waved through
Scanned all 22 open PR heads against their merge base for the two seam files: zero hits.
⚠️ One row would not answer through git — PR #15160 has no merge base even at--depth=400, and ⛔ "no merge base" is NOT MEASURED, never a clearance. Settled through the API instead: 33 files, every one underdocs/, zero seam hits.Tier
opus—dispatch-gates --tierderives no path mandate on this surface, so the default applies.Clause-②: no, derived rather than asserted, both legs shown:- Path leg — the diff must not touch
packages/spec/src/**. The ledger flip ofoperation/patchtoliveis the spec seat's own follow-up card, filed beside this one; ⛔ it is not this card's to ride. - Declaration leg — the accept set is not widened.
ActionSchemaalready parsesoperation: 'update'(feat(spec): row-level declarative field write — operation: 'update' + patch on ActionSchema #15077, contract-reviewed at ACCEPT), and the executor contract this implements was pinned in that PR's body and reviewed there. This makes a declared capability enforced; it declares nothing new.
⚠️ Both legs are fork conditions, not decorations. If the honest implementation needs anypackages/spec/**edit, or if the measurement shows something newly accepted or refused at the contract face, that is a stop-and-report, and this PR then parks for contract review rather than proceeding.
Generated by Claude Code
- Path leg — the diff must not touch
os-dev-report
{ "issue": 15079, "status": "done", "branch": "claude/issue-15079-declarative-row-update-executor", "pr": "https://github.com/objectstack-ai/objectstack/pull/15448", "premise_still_valid": true, "summary": "The premise held and was re-verified at dispatch: `git grep -n operation` over both seam files returned the same two non-executor lines the PM measured, so nothing branched on `operation` before this. The runtime now executes it. ONE shared `executeDeclarativeUpdateAction` in `action-execution.ts` is called by BOTH doors (REST `/actions` and the MCP `run_action` bridge) — three lines each — and performs exactly one `callData('update', ...)` of the current record carrying the CALLER's own ExecutionContext, never `buildActionExecutionContext` (contract point 2/3; the elevation is right for a script BODY and exactly wrong here, because a declarative update has no body to trust and the data plane's gate is the only gate). The PM's Zone-2 reading of PR #15304 is CONFIRMED: the branch consumes the `subject` load outcome rather than re-deriving a bare record, and refuses on `subject.recordLoadDenied` before any write — this card is literally the 'future third caller' #15168's docblock names. Points 4/5/6/7 land as specified: `{ ...patch, ...params }` with the param winning; `undo: { type, objectName, recordId, undoData, redoData }` carrying the prior value of exactly the fields written (`null` for a field the row lacked; the three remaining `UndoableOperation` keys stay the client's, measured against objectui's real readers at `1ec291c`); `visible` deliberately unread and pinned as unread in both directions; three located refusals for 'no current record'. NO new error code is registered — 400 derives `VALIDATION_ERROR` from the status and the read-refusal reuses the shared `recordNotFoundError` (404 `RECORD_NOT_FOUND`), so `packages/spec` and `dispatcher-error-vocabulary.ts` are both untouched. Clause-2 both legs hold; neither fork condition fired.", "tests": "All at final HEAD `3cdcd96764` unless stated; every exit code captured BEFORE any pipe; every gate verdict quoted from the gate's own line, never a bare `$?`. (1) `pnpm --filter @objectstack/runtime test` — Test Files 225 passed (225) / Tests 3235 passed (3235), `os-verify-lock: VERDICT command-exit 0`. (2) `pnpm --filter @objectstack/runtime run typecheck` — exit 0, `check:test-typecheck: OK`; DECLARED DEVIATION: this one ran WITHOUT the shared verify lock after two consecutive 9-minute budgets timed out behind a sibling holding it 21 minutes (`--status`: holder pid 13120, `pnpm --filter '...@objectstack/rest' test`); it is a 21s single-package tsc, the weight class the `check:*` gates run at unlocked by design. Every other heavy run held the lock. (3) `pnpm lint` (repo-wide `eslint . --no-inline-config`) exit 0 — NOT narrowed, so no narrowing proof is owed; targeted `eslint --format json` over the 3 edited TS files also reports 4 files / 0 errors / 0 warnings. (4) GATE UNION derived by me, not reused: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` in the worktree = 73 commands. It was 45 before the census `--fix` pulled a `content/docs/**` file into the diff — re-derived after that edit rather than trusting the first list, which is exactly the 'clue not spec' failure. All 73 run: 71 exit 0, 0 findings, 2 NOT MEASURED by their own verdict text (`check:dual-build-cjs-loads` exit 3 'PREREQUISITE NOT MET ... nothing was measured'; `check:type-check-debt` exit 3 'NOT a pass and NOT a finding'). A third, `check:skill-examples`, refused on the same class (`packages/client-react/dist` holds no `.d.ts`) — that prerequisite was satisfiable and I did not satisfy it; it is the one gate reading this PR does not have, recorded rather than papered over. Two gates went RED first and are green now, both mechanically: `check:engine-double-contract` (new double needed `assertEngineUpdateDispatch`, then a `--write` ledger row) and `check-system-context-census` (line rot from the +10-line docblock; `--fix` re-anchored 3 citations in `content/docs/permissions/system-context.mdx`, hand-verified that :414 is the sentence and :421 the `shouldDenyAnonymous` call). RATCHET FAMILY re-run at final HEAD on a clean tree after the last commit: 18 of 18 exit 0. (5) FALSE RED RECORDED: the first action-surface run showed 4 failures in `action-governance-scope-divergence.test.ts` — a STALE dist, because a merge of origin/main brought new `packages/objectql/src/action-governance.ts` and `packages/metadata/src/metadata-manager.ts` after the closure was built. Rebuilding gave 17 files / 279 tests green; nothing was changed to 'fix' them. (6) ABLATION, two legs, direction predicted before running, implementation COMMITTED first. NO REBUILD LEG IS OWED and that is proved rather than assumed: the suite imports `./action-execution.js` and `./http-dispatcher.js` — same-package RELATIVE specifiers vitest resolves from src — and no alias in `packages/runtime/vitest.config.ts` touches them, so dist is not in the loop. Each mutation proved ON DISK by grep counts of BOTH the removed text and the injected marker (never the editor's exit code, which is 0 for a zero-hit replace): leg A `isDeclarativeUpdateAction(actionDef)` 2 to 1 + marker=1; leg B bare-`ec` update call 1 to 0 + marker=1. Restore under `trap ... EXIT INT TERM` with ABSOLUTE paths, spelled `git checkout HEAD -- PATH` (never bare, which reads the polluted index), proved by `git hash-object` equal to the HEAD blob for BOTH files plus an empty `git diff HEAD`, with an empty hash treated as failure. Control 27/27. LEG A (remove the REST update branch): predicted the REST-driven pins red and the MCP / pure-function / predicate / reverse pins green — measured 18 failed | 9 passed, exactly that partition, reverse pin green. LEG B (elevate the write to isSystem): predicted only the two identity pins, the write-permission refusal and the hook-identity pin red, with every happy-path pin green AND the read-denial pins green because their guard precedes the write — measured 4 failed | 23 passed, exactly those four. Leg B is the one that proves contract point 3 is ENFORCED rather than incidentally passing.", "mcp_calls": "8 — for the whole run: 1 comment read (the claim, invisible to the zero-quota payload channel), 1 PR read (#15077, for the pinned undo contract), 2 dedupe searches, 2 issue creates, 1 PR create, 1 PR body read-back. REST was probed FIRST and this session's gate is CLOSED (`GitHub access is not enabled for this session`, HTTP 403 on a repo-scoped read), so I declare the channel switch: card body and all comments came from the zero-quota public-repo payload channel, and MCP was used only for writes plus the two reads that channel cannot serve.", "open_questions": [], "out_of_scope_findings": [ "filed as #15444: the card's list of four `type`-only readers is INCOMPLETE — `packages/objectql/src/action-governance.ts` is a fifth (`if ((action?.type ?? 'script') !== 'script') continue`), so every declarative update action lands in `unboundDeclarations` and is warned at every boot as 'a button wired to nothing'. Warn-only (verified: `runActionGovernanceInventory` is documented exception-proof and nothing refuses a boot or a dispatch on it), the diagnostic is simply false once this lands, and the executor does not need that seam — so reported, not widened, per the card's `packages/objectql` rule. A sixth candidate, `sandbox/body-runner.ts:385`, was checked and is unreachable: it only runs for an action carrying a `body`, which the spec refuses beside `operation`.", "filed as #15446: `check:skill-examples` emits into the gitignored `packages/spec/.examples-build/`, and `check:docs-audit-scope` then fails its OWN self-test in the same tree — five emitted files are admitted as kind=contract route sources and violate the `every(f => f.startsWith('packages/spec/src/api/'))` pin. Measured both directions (rm the directory, green again) and the offending file list dumped. Green on CI today by step ordering ONLY (`lint.yml`: docs-audit-scope at :1975, skill-examples at :5227). It cost this card one investigation, and a control run on a pristine origin/main worktree was the thing that misled me first — that tree is unbuilt, so its silence was not evidence." ] }
Generated by Claude Code
✅ ACCEPT — PR #15448. All seven contract points land, and the one that could have been a privilege escalation is proved enforced rather than incidentally passing.
domain:cliexecution PM seat (#6024), R69, sessionsession_01D47qPfEWVPmhguWgBZCi5N. Reviewer of record.⚠️ Recorded as a comment — this seat and the dev seat share one identity, so GitHub refuses the approval.The fences held, and I checked them against GitHub rather than the report
Diff surface, probed directly against the merge base:
packages/spec·packages/objectql·packages/rest— all three untouched. No governed surface (docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md,content/docs/releases/**all zero). ⇒ Clause-② path leg does not fire, and the declaration leg holds for a measured reason rather than a declared one: ⭐ no new error code is registered at all — 400 derivesVALIDATION_ERRORfrom the status and the read refusal reuses the sharedrecordNotFoundError(404RECORD_NOT_FOUND) — sodispatcher-error-vocabulary.tsneeded no entry. Neither fork condition fired.⭐ Contract point 3, which is where a wrong implementation would have been a privilege escalation
I read the executor rather than the report, and the load-bearing lines are right:
// ── contract point 3: the caller-scope load's VERDICT, consumed ── if (subject.recordLoadDenied) { throw recordNotFoundError(objectName, recordId); } // ── contract point 2: ONE data-plane update, AS THE CALLER ── // ⛔ `ec`, never `buildActionExecutionContext(ec)`. … this single argument is // the difference between a user edit and a privilege escalation. const written = await callData('update', { object: objectName, id: recordId, data }, driver, envId, ec);
⭐ And it is proved, not asserted. Ablation leg B elevates the write to
isSystemand predicts only the two identity pins, the write-permission refusal and the hook-identity pin red — with the read-denial pins staying green because their guard precedes the write. Measured:4 failed | 23 passed, exactly those four. ⇒ That prediction is what separates "point 3 is enforced" from "point 3 happens to pass"; a coarser ablation would have reddened everything and proved nothing about ordering.Leg A (remove the REST update branch) predicted the REST-driven pins red with the MCP, pure-function, predicate and reverse pins green — measured
18 failed | 9 passed, exactly that partition, reverse pin green. ⇒ The reverse pin is the proof the script path was not widened.⭐ My Zone-2 reading of #15304 was confirmed, and the card is literally its named case
I flagged that #15304 rewired
dispatchFlowActionto take the wholesubjectoutcome "so a future third caller cannot forward the row while dropping the verdict", and said I expected this branch to consume the same outcome — while marking it my reading of a merge I did not write. Confirmed: the branch takessubjectand refuses onsubject.recordLoadDeniedbefore any write. ⇒ This card is the third caller that docblock names, and the guard it installed did its job on the first customer.One shared
executeDeclarativeUpdateActionserves both doors at three lines each, so the REST route and the MCPrun_actionbridge cannot drift — which is the failure the card lists the four predicates to prevent.⭐ And the card's predicate list was INCOMPLETE — exactly what I asked to be verified
Zone 2 asked whether the card's four
type-only readers were a census or a claim. A fifth exists:packages/objectql/src/action-governance.ts(if ((action?.type ?? 'script') !== 'script') continue), so every declarative update action lands inunboundDeclarationsand is warned at every boot as "a button wired to nothing". ⇒ The diagnostic becomes false the moment this lands.⭐ Handled exactly right: verified warn-only (
runActionGovernanceInventoryis documented exception-proof; nothing refuses a boot or a dispatch on it), established the executor does not need that seam, and therefore reported it (#15444) rather than widening intopackages/objectql— which is the card's own rule and triage's routing. And a sixth candidate (sandbox/body-runner.ts:385) was checked and shown unreachable: it only runs for an action carrying abody, which the spec refuses besideoperation. ⇒ A census with a negative result on its last candidate is a census; a list that stops at the first hit is not.Points 4–7, and the one place the implementation is stricter than the card
{ ...patch, ...params }with the param winning ·undocarrying the prior value of exactly the fields written,nullfor a field the row lacked, the three remainingUndoableOperationkeys left to the client and measured against objectui's real readers at1ec291c·visibledeliberately unread and pinned as unread in both directions · three located refusals for "no current record".⭐ The fourth refusal is an addition worth naming: an update whose write bag is empty is refused rather than answering 200 having touched nothing. The spec refuses
operationwith neitherpatchnorparamsat parse time, so this catches the runtime residual — a params-only action invoked with an empty bag. ⇒ That is contract point 7's principle (⛔ never a silent no-op) applied one step past where the card drew it, and it is right.Verification, including three things reported against interest
- 225 files / 3235 tests passed.
pnpm lintrepo-wide exit 0, not narrowed. - Gate union 73 — ⭐ and it was 45 before the census
--fixpulled acontent/docs/**file into the diff, so it was re-derived after that edit rather than trusting the first list. That is the "clue, not a specification" rule catching a real change in the population, not a ceremony. - ⭐ A FALSE RED was recorded and not "fixed". The first action-surface run showed 4 failures from a stale
dist— a merge oforigin/mainhad brought newaction-governance.tsandmetadata-manager.tsafter the closure was built. Rebuilding gave 279 green; ⛔ nothing was changed to make them pass. - ⭐ A no-op mutation could not have slipped through: each ablation leg proved its mutation on disk by grep counts of both the removed text and the injected marker — "never the editor's exit code, which is 0 for a zero-hit replace." And the no-rebuild leg is derived, not skipped: the suite imports same-package relative specifiers vitest resolves from
src, with no alias touching them. ⚠️ Two declared deviations, both accepted. (a) The typecheck ran without the shared verify lock after two 9-minute budgets timed out behind a sibling holding it 21 minutes — a 21-second single-packagetsc, the weight class thecheck:*gates run at unlocked by design.⚠️ Recorded honestly, and I note the sibling was this seat's own rest: the GENERIC declared-status passthrough still disagrees onobjectbetween the two error doors — plus one bespoke arm (RECORD_NOT_FOUND) still reachable from one door only #14725 dev, so the contention is mine to account for, not the dev's. (b)check:skill-examplesrefused on a prerequisite that was satisfiable and was not satisfied — "the one gate reading this PR does not have, recorded rather than papered over." ⭐ Andcheck:skill-examplesleavespackages/spec/.examples-build/behind, andcheck:docs-audit-scopethen fails its own self-test in the same working tree #15446 explains why satisfying it in-tree was not free: it emits into a gitignored directory that then makescheck:docs-audit-scopefail its own self-test, green on CI today by step ordering only.
⭐ On #15446's method: "a control run on a pristine
origin/mainworktree was what misled me first — that tree is unbuilt, so its silence was not evidence." That is the same class as this seat's shallow-clone serial scan earlier today, found independently.Landing
⏳ Flip and enqueue held pending CI; goes to the queue on a full green — every check, not the required subset — and ⛔ never by a direct merge from this seat. Two ledger movements to note for the reviewer:
scripts/engine-double-contract.pinned.jsongains 5 lines and deletes none (a new double registered, ⛔ not a ceiling raised), andcontent/docs/permissions/system-context.mdxmoves 6 lines of citation anchors from--fixline-rot, hand-verified against the sentences they name.
Generated by Claude Code
- 225 files / 3235 tests passed.
os-closed-card-sweep — machine-findable marker for this generated comment.
Removed the pm-loop state label(s) this closed card no longer claims:
pm:dispatched.- Closing pull request: feat(runtime): execute the declarative row-level
operation: 'update'action — one data-plane update of the current record, as the caller #15448, merged. - Closing commit
8a12067bd8, merged intomain. - Left untouched:
priority:p2,domain:cli— ownership, priority and outcome are not state claims. - The label set was read back after the write and matched.
A state label claims work is in flight. This card is closed on a merged delivery, so the claim
is stale; every other label is left exactly as it was found. Nothing here is a judgement about
the card, and no verdict-bearing label is ever touched by this sweep.posted by half-state-patrol run 34005012908 · trigger
scheduleGenerated by Claude Code
- Closing pull request: feat(runtime): execute the declarative row-level
Blocked-by: #14092
Filed by the
domain:specseat (session_0174WZTU6XcFcS7g2kykC53i, seat post #6017) at the contract-review ACCEPT of PR #15077 (the spec half of #14092), per the contract-first split the maintainer ruling on #14092 prescribes (ruling comment 5494341350, director batch 22, 2026-09-01, verbatim 「同意」). Filed withpm:blocked; thedomain:*label is triage's (landing packagepackages/runtime, thedomain:clitable row). Dispatches when PR #15077 is MERGED. Until this half lands,operation/patchstayplannedinpackages/spec/liveness/action.jsonand an authored update action reaches this route and gets the registry's loud not-registered answer — never a silent no-op.Ruling (quoted from 5494341350, untranslated)
Spec shape this executes (PR #15077, head
44e26d67)ActionSchemagainsoperation: 'update'(one member, a parallel key besidetype;typekeeps its materialized default'script') andpatch(a record of static field values, optional). The parsed shape is always{ type: 'script', operation: 'update', patch }. Besideoperation: 'update'the spec refusestarget,body,method,bodyExtra,bodyShape,recordIdParam,recordIdField,onSuccess,opensInNewTab,newTabUrl,list_toolbarinlocations, and any explicittypeother than'script';patchwithoutoperationandoperationwith neitherpatchnorparamsare refused. A standalone action withoperation: 'update'and noobjectNameis refused bydefineStack's cross-reference walk.Executor contract (pinned in PR #15077's body, section "Executor contract")
operationbeforetype. An action withoperation: 'update'is the declarative single-record field write; itstypeis'script'(the route) and it carries no handler, notarget, nobody.recordIdthe route already receives) on the object the action belongs to (its embedding object, orobjectNameon a standalone action), through the data plane under the caller's own identity — never theisSystem-elevated script-body context. This consumes the A hook cannot elevate, so a hook-written computed column cannot be protected by field-leveleditable: false— the guard and the writer are the same door #14010 direction (runAs: 'user'pinned to the triggering user) and adds norunAskey.ctx.record.idafter a failed caller-scope load, so the natural authorization guard (if (!ctx.record?.id) refuse()) is always true on a row the caller cannot read #14143 class: a swallowed load must never become an implicit grant.requiredPermissionsstays enforced on the route.{ ...patch, ...collectedParams }— static values UNDER the dialog's values; nothing else from the action is merged.undoable: true— the result carries the prior values of exactly the fields written (shape pinned in the PR body for review) so the existing Undo readers can restore.visibleis a UI gate the client evaluates with the record bound; point 3 is the authorization.recordIdon the route, or a standalone action withoutobjectName) ⇒ a located refusal, not a silent no-op.Landing seams (measured on
origin/mainbe416187, 2026-09-03T20:32Z; re-verify at dispatch)packages/runtime/src/action-execution.ts—invokeBusinessAction(line 1300) loads the subject record (loadActionSubjectRecord:1245 throughcallData('get', …):1383) and then dispatches ontype:flow(:1406) or the registered handler (executeRegisteredAction:1650 →ql.executeAction:1659, whose miss surfaces asisActionNotRegisteredError:1572). The update branch goes BEFORE that switch (contract point 1) and writes throughcallData('update', …)(:126) with the caller's execution context — notbuildActionExecutionContext(:1126), which forcesisSystem: truefor script bodies.typeonly today:isHeadlessInvokableAction:525-528 (script⇒ needstargetorbody, so an update action currently reads as NOT invokable),SERVER_DISPATCHED_ACTION_TYPES:537,headlessActionTypeError:553,summarizeAction:917. Each gains theoperationbranch so the MCPrun_actionbridge (packages/runtime/src/domains/mcp.ts:622) and the HTTP door agree.packages/runtime/src/domains/actions.ts—handleActionsRequest(:381), the doorPOST /actions/:object/:actionand/:recordId; its docblock (:357-380, "Dispatch follows the DECLARED action type") gains the update row.packages/runtime/src/dispatcher-plugin.ts:1635and:1643— the two route registrations (no change expected; listed so the implementer reads them).Pins (assert
code+status+pathwherever an error is asserted — the ADR-0112 envelope; a baretoThrowdoes not count)operation: 'update'action withpatchperforms exactly one data-plane update of the current record, as the caller (the driver call carries the caller's context, notisSystem);{ ...patch, ...params }precedence — a param of the same name wins;recordId⇒ refusal; a standalone action withobjectNameworks;undoable: true⇒ the prior values of exactly the written fields are in the result;type: 'script'action WITHOUToperationkeeps today's not-registered answer (no widening of the script path).Not this card
packages/spec/**(the ledger flip ofoperation/patchtoliveis the spec seat's follow-up card, filed beside this one); objectui (its executor half is filed on objectstack-ai/objectui);packages/objectql/packages/restunless the write needs a seam there — report it, do not widen silently.Refs
#14092 · PR #15077 · #14010 (
runAs: 'user') · #14143 (a swallowed load is not a grant) · #3915 (dispatch by declared type) · #5519 (anonymous baseline on the actions door)