Repository navigation
The most common action in any app — set a field on the current record — has no declarative form for a ROW action, while the BULK form is fully declarative #14092
Description
Measured against @objectstack/spec / @objectstack/rest / @objectstack/runtime 17.2.0, while building the completion interaction of the duly metadata app (objectstack-ai/duly#4): three buttons that each set one field on one record.
The asymmetry
Selection → declarative. A list view's bulkActionDefs expresses it exactly:
{ name: 'duly_task_bulk_complete', operation: 'update', patch: { status: 'done' },
visible: P`record.status == "open" || record.status == "in_progress"` }No action, no handler, no code. The write runs on the data plane under the caller's own permissions, hooks and validations fire, and patch merges under collected params so a fixed value can be declared without exposing it in the dialog. This is the right shape and it works.
Row → nothing. ActionType is url | form | flow | script | api | modal. There is no update_record type, no action effect, and no patch-shaped key on ActionSchema. So the identical intent, one row instead of twenty, has to be hand-written.
Why neither near-miss is the declarative form
type: 'api' + method: 'PATCH' + bodyExtra is a declarative HTTP call, not a declarative field write. The author hand-writes the platform's own data-API path into application metadata:
target: '/api/v1/data/duly_task/${ctx.recordId}'- Nothing binds that string to the action's
objectName; the object name is duplicated into a path literal. - Nothing checks it at author time —
objectstack validatepasses on any string. - It pins the transport.
enableProjectScoping+projectResolution: 'required'registers only/api/v1/environments/:environmentId/data/:object/:id, so an app that shipped the unscoped path is broken on that host with no diagnostic. - The platform cannot tell it is a record update, which is presumably why
undoable("single-record update actions only — captures the record's prior field values") has nothing to key on here.
For AI-authored metadata this is the worst available shape: it parses green and 404s at the click. (The spec's own worked example of this route is itself wrong — filed separately.)
type: 'flow' + a flow with an update_record node is genuinely declarative, but it is one flow per button to assign one string, plus a flow-run record per tick, on the surface where validate currently misses bare predicates (#14089).
The cost is authorization, not the one-line write
This is the part that matters. A handler's ctx.engine is system-elevated and RLS/FLS-bypassing by design — buildActionExecutionContext stamps isSystem: true onto the caller's context, and both dispatch surfaces log the write as TRUSTED. visible is a UI hide, not authorization.
So every app that wants "tick this row" must hand-write a privileged write and re-establish the authorization the declarative bulk path gets for free. And the obvious guard does not work: the dispatcher loads ctx.record under the caller's scope, swallows a failed load to {}, and then stamps record.id = recordId on unconditionally — so ctx.record.id is present even when the caller cannot read the row. We had to key the check on a field that is required: true on the object instead:
const status = text(record.status);
if (!status) throw refuse(`Task ${id} is not available to you.`, 'DULY_TASK_NOT_AVAILABLE', 404);That is subtle, undocumented, discovered by reading the dispatcher, and every app author — human or AI — has to rediscover it or ship an authorization hole on a private object.
What would close it
A row-action counterpart to bulkActionDefs' operation: 'update' — the platform knowing the action is a single-record field write, so it can route it through the data plane under the caller's own credentials, bind the object from objectName, and let undoable mean something. Shape is yours to pick; the requirement is that "set status to done on this record" stops being three files of privileged imperative code with a hand-rolled permission check.
Where this came from
objectstack-ai/duly#4. The app took the declarative route for bulk and wrote handlers for the rows; the rationale is in src/actions/task.handlers.ts there. Fourth platform gap from dogfooding duly — siblings #14087, #14088, #14089.
Activity
huangyiirene commentedon Sep 1, 2026
裁决:同意(YES)—— 行级声明式字段写入立项(维护者 2026-09-01,总监批 #22)
项目总监席 · session
session_01KGtaLpkW1mycWgkbSb3H6t· 维护者对本批逐字:「同意」。裁定内容:行级 action 获得
bulkActionDefs的声明式对应物 ——operation: 'update'+patch(+visible谓词),单记录、走数据面、用调用者自己的权限、钩子与校验照常触发、undoable有锚点。键形细节(在ActionType加成员还是并列键形、与既有 action 词表的混用规则)由实施 PR 设计并在正文钉死供复审。实施约束(随派发单同行):
packages/spec/src/ui/**+ 内容肢扩宽 accept 面)⇒ draft PR +needs:contract-review随可复审增量同笔挂,changeset minor;ctx.record.id盖章陷阱已拆 action dispatcher stampsctx.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 独立派发,⛔ 不合卡、不重做;type:'api'手写路径包装、⛔ 不走一按钮一条 flow;editable: false— the guard and the writer are the same door #14010(同批已裁)—— 本卡实施时消费该裁决的方向,⛔ 不重开。状态转移(同笔)
needs-user-decision→pm:queue;priority:p1domain:spec不动。Generated by Claude Code
zhuangjianguo commentedon Sep 3, 2026
Claim: PM loop,
domain:specseat (seat post #6017), R3 of this shiftSession:
session_0174WZTU6XcFcS7g2kykC53i(GitHubzhuangjianguo)Branch:
claude/issue-14092-row-action-declarative-updateWorktree:
objectstack-issue-14092Domain:
domain:specFile surface (the SPEC half only — contract-first; the executor halves are separate cards the seat files at ACCEPT with
Blocked-by:lines):packages/spec/src/ui/action.zod.ts—ActionSchemagains the row-level declarative field write ruled in 5494341350, mirroringpackages/spec/src/ui/bulk-action.zod.ts's existing vocabulary (operation: 'update'+patch, with the action's existingvisiblepredicate andparamsbag;undoablegains its anchor), the key shape designed by the implementer and pinned in the PR body for review (anActionTypemember versus a parallel key form; exclusivity withtype: 'api''starget/method/bodyExtra; whetherobjectNameis required when the action is standalone) · its test file (accept pins, refusal pins with issuecode+path+ first sentence for the illegal combinations, a positive control) · generated artifactscheck:generated --fixproves stale (authorable surface gains keys; thereferences/ui/action.mdxpage; whatever else moves) ·.changeset/*.md@objectstack/specminor (public-surface widening) ·needs:contract-reviewon the PR AND this card in the same stroke once the PR exists. ⛔bulk-action.zod.tsuntouched (the vocabulary is mirrored, not moved); ⛔ themethoddocblock region ofaction.zod.ts(:1306-1308, the/api/v1/sys_api_key/{id}example) untouched — #14093 owns it and is serialized behind this card; ⛔ nopackages/runtime,packages/objectql,packages/restor objectui code — the executor halves are downstream cards; ⛔ #14143'sctx.record.idtrap is landed on its own card, not re-done. Stop on breach; explain in the report.Container & model: M,
mode:subagent,model: fable(CONTRACT_REVIEW_TIER).node scripts/pm/dispatch-gates.mjs --tier packages/spec/src/ui/action.zod.tsat82291fba(19:16Z): "no path-derived mandate" + "Clause ② SUSPECT surface"; tier decided from content — the ruling widens a published accept set.Clause-②: yes — new keys on a published schema (the mechanical floor) and an accept-set widening; ⛔ no pre-hang, the dual carrier is hung with the reviewable increment.
Serial constraints cleared: open-PR file scan at 19:08Z (24 open PRs, 207 changed-file rows) — none touches
ui/action.zod.tsor its test; same-day churn: none (action.zod.tslast8a8e54112026-09-02 15:50Z); lane siblings in flight (#14074 + #14073view.zod.tsconditional, #13916 + #14583field.zod.ts, PR #15029 / #15053 / #15054 queued or landing) file-disjoint; #14093 sharesaction.zod.ts— serialized behind this card (deferral note posted on #14093 in this stroke; fold refused: different defect shape, gate ①); H17 on-hold trigger-file index (#9857 body, 13:47Z): no row namesaction.zod.ts; no remote branch exists for this card (19:16Z).Decision re-read (3 comments): maintainer ruling 5494341350 (2026-09-01, director batch #22, verbatim 「同意」) — YES to a row-level declarative field write mirroring
bulkActionDefs(operation: 'update'+patch+visible; single record; data plane; the caller's own permissions; hooks and validations fire;undoableanchored), with five implementation constraints: Clause ② yes withneeds:contract-reviewand a minor changeset; the shape mirrors the bulk vocabulary and invents no second spelling; #14143 stays separate; thetype: 'api'hand-written path and one-flow-per-button remain excluded; the provenance / authorization channel follows #14010's ruling direction (5494343943:runAssemantics — the write runs as the caller, never system-elevated) without reopening it. Triage 5489956349 / 5490600937: the security trap was split to #14143 (closed completed, engine lane). Premise re-read onorigin/main82291fba(19:16Z):ActionTypeataction.zod.ts:530is['script', 'url', 'modal', 'flow', 'api', 'form']— no update member;ActionSchemacarriesobjectName(:927),type(:948),target(:967),params(:1091),undoable(:1145, "single-record update action"),visible(:1213),method(:1310),bodyExtra(:1336) and nooperation/patchkey;BulkActionDefSchema(bulk-action.zod.ts:156) declaresoperation(:203,'update' / 'delete' / 'custom'), a staticpatch,params,visible,confirmText, with the aliasop → operation. Premise holds; nothing of the ruling has landed.Generated by Claude Code
zhuangjianguo commentedon Sep 3, 2026
Dispatch (R3 of this shift, 2026-09-03T19:23Z) —
domain:specseat,session_0174WZTU6XcFcS7g2kykC53i, seat post #6017.mode:subagent,model: fable(CONTRACT_REVIEW_TIER), size M, Clause ② yes (dual carrier hung with the PR). The dev leaves its ownClaim:comment below; the assignee stays the PM's.Dispatch text handed to the dev, in brief (ruling 5494341350's content and five constraints are quoted verbatim and marked non-renegotiable; #14010's
runAsdirection is consumed, not reopened):ActionSchemagains the row-level declarative field write mirroringbulk-action.zod.ts's spelling (operation: 'update'+ staticpatchmerged under the collectedparams, the action's ownvisiblepredicate,undoableanchored); the implementer designs the key shape —ActionTypemember versus parallel key form, exclusivity withtype: 'api''starget/method/bodyExtra,objectNamesemantics for a standalone action — and pins it in the PR body for review, with refusal pins (issuecode+path+ first sentence) for every contradictory combination.patchunderparams;undoablecaptures prior values) — the seat files the runtime and objectui executor cards at ACCEPT withBlocked-by:on this card. New keys land asplannedin the liveness ledger percheck:liveness's own instruction.Fixes #14092,@objectstack/specminor changeset (widening, no BREAKING), generated artifacts via--fix, two-limb Clause ② line,needs:contract-reviewon the PR and this card in the same stroke with readbacks, ⛔ no ready flip; report asos-dev-reporthere. ⛔bulk-action.zod.ts, themethoddocblock region (ActionSchema.method's worked example points atype: 'api'PATCH at/api/v1/sys_api_key/{id}— the shipped data API is/api/v1/data/:object/:id, so copying it 404s #14093, serialized behind),packages/runtime/objectql/rest, objectui: untouched.Generated by Claude Code
claude commentedon Sep 3, 2026
Claim: os-dev seat for this card (dispatched by the PM claim above; the assignee stays the PM's).
Session:
session_0174WZTU6XcFcS7g2kykC53iBranch:
claude/issue-14092-row-action-declarative-update(pushed empty as the write-route probe; baseorigin/main8a96e666)Worktree:
objectstack-issue-14092File surface, the SPEC half only, restated with the increments the design forces (each is a consequence of widening
ActionSchema, named here so the parallel seats can serialize on it):packages/spec/src/ui/action.zod.ts— the row-level declarative field write (the key shape is designed in the PR body for contract review) and its refinements; themethoddocblock region (/api/v1/sys_api_key/{id}example) stays byte-identical forActionSchema.method's worked example points atype: 'api'PATCH at/api/v1/sys_api_key/{id}— the shipped data API is/api/v1/data/:object/:id, so copying it 404s #14093.packages/spec/src/ui/action.test.ts(existing enum enumeration) plus a newaction-row-update.test.tswith the accept / refusal / positive-control / no-transform pins.packages/spec/src/ui/dashboard.zod.ts—WidgetActionTypeSchemais derived fromActionTypeby that file's own doctrine, so a dashboard header action would otherwise silently gain the new type with no record to write: one refusal there, pinned.packages/spec/src/stack.zod.ts— the standalone-actionobjectNamedecision (design point d) lives where standalone-ness is knowable: the cross-reference walk overstack.actions[].packages/spec/liveness/action.json—plannedentries for the new keys with anauthorHintnaming the executor halves.content/docs/protocol/objectui/actions.mdx— the hand-written sentence asserting the closed type set.check:generated --fix;.changeset/action-row-update-declarative.md(@objectstack/specminor).Untouched:
bulk-action.zod.ts,packages/runtime,packages/objectql,packages/rest,packages/lint, objectui,content/docs/releases,skills.Clause-②: yes
Generated by Claude Code
claude commentedon Sep 3, 2026
os-dev-report
{ "issue": 14092, "status": "done", "branch": "claude/issue-14092-row-action-declarative-update", "pr": "https://github.com/objectstack-ai/objectstack/pull/15077", "premise_still_valid": true, "summary": "Spec half landed as draft PR #15077 (head 44e26d67, merges origin/main 101ad2cc; base at claim 8a96e666). ActionSchema gains the row-level declarative field write mirroring bulk-action.zod.ts word for word: `operation: 'update'` (one member; `'delete'`/`'custom'` refused with the reason) + `patch` (static values merged UNDER the collected `params`, passed through verbatim), reusing the action's own `params`/`visible`/`confirmText`/`undoable` (which now names its anchor). Key shape pinned in the PR body: a PARALLEL KEY beside `type`, not an ActionType member — `type` stays at its default `'script'` (the platform action route the write is performed on; executors read `operation` before `type`). Decisive facts: the ruling's and the bulk def's own spelling, and objectui's `ActionRunner.builtinExecutors` typed `Record of RunnableActionType` on purpose, which would hard-block objectui's next spec bump on the executor card if a member were added (the pin gate injects this tree's spec into the console bundle only, so it would not red today). Mixing rule, one refusal per contradiction at its key (code custom, first sentence names the executor): any other explicit `type`, `target`, `body` (carved out of the #4352 rule for a precise message), `method`, `bodyExtra`, `bodyShape`, `recordIdParam`/`recordIdField`, `onSuccess`, `opensInNewTab`/`newTabUrl`, a `list_toolbar` location; `patch` without the operation; the operation with neither `patch` nor non-empty `params`; `type: 'update'` refused on the enum with the prescription; `op`/`values`/`set`/`update` rename onto the canonical keys. `objectName` is judged at stack level (`collectGlobalUpdateActionErrors` in stack.zod.ts: a standalone update action without objectName is refused; embedded ones are bound by their object). Inline page-element actions cannot carry the keys (InlineActionSchema picks neither — pinned). Ledger: `operation` and `patch` are `planned` with authorWarn + authorHint naming the executor halves (check:liveness green, symbol anchors resolve). Docs: protocol narrative page gains the declarative write; check:generated --fix regenerated exactly content/docs/references/** and liveness/state-counts.md. Changeset @objectstack/spec minor, pure widening. needs:contract-review hung on PR and card in the same stroke; both read back (card: priority:p1, pm:dispatched, domain:spec, needs:contract-review; PR: needs:contract-review beside the auto-labels documentation/size/l/tests/tooling/protocol:ui). The card's assignee is the PM's (zhuangjianguo) — untouched. Brief hypothesis falsified: `type: 'script'` explicitly authored beside `operation: 'update'` cannot be refused (a materialized .default is indistinguishable at object level) and is correct anyway — accepted and documented. Not touched: bulk-action.zod.ts, the #14093 method docblock region (diff-verified byte-identical), runtime/objectql/rest/lint, objectui, releases, skills; #14143 not addressed here; #14010 consumed, not reopened. Note for the executor cards: the Studio action form (action.form.ts) does not surface the two keys yet — an authoring-UI follow-up for the objectui/Studio half.", "tests": "All heavy runs via scripts/pm/os-verify-lock.sh (slot issue-14092), exit captured before any pipe, verdict lines quoted. At head 44e26d67 (after the final commit): `pnpm --filter @objectstack/spec build` (check-dts-emitted 34/34); `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2` — 'Test Files 466 passed (466) / Tests 12427 passed (12427)'; `pnpm --filter @objectstack/spec run typecheck` (tsc --noEmit + check:scripts-typecheck + check:test-typecheck 'OK') — wrapper 'VERDICT command-exit 0'. Earlier targeted run of 7 files: 352 passed. check:generated --fix: '2 of 15 artifact(s) stale' -> gen:docs + gen:liveness-counts regenerated; subsequent check:generated exit 0; check:authorable-surface green; check:liveness 'action 48 classified (live 41, planned 4, dead 3)', every anchor resolves. Gate derivation: `node scripts/pm/dispatch-gates.mjs --commands` inside the worktree at 44e26d67 -> 78 commands (68 by path + 7 by kind + 5 whole-tree, 2 shared); all 78 run: 71 exit 0 (incl. check:api-surface, check:authorable-surface, check:docs, check:export-origins, check:llms-txt, check:skill-refs, check:strictness-ledger, check:variant-docs, check:yaml-examples, check:spec-parsed-alias, check:nul-bytes, check:merge-driver, check:published-files, check:changeset-gate-self-tests, check-closing-keyword-parity, check:test-source-alias, check:cross-package-test-inputs, check-changeset-no-major, check-adr-0087-registration, check-empty-changeset, check:engine-double-contract); 7 NOT MEASURED by their own verdict text (workspace closure not built here / no test log): check-dev-prereqs (exit 1), check-test-completeness (exit 3), lint check:doc-formula-expressions and check:doc-security-posture (exit 3, formula/lint dist absent), spec check:skill-examples (exit 1, client-react dist absent; the one fence this diff adds carries no os:check marker so it is outside that gate's population either way), check:dual-build-cjs-loads (exit 3), check:type-check-debt (exit 3) — CI builds the closure and owns those. eslint: targeted `eslint --no-inline-config --format json` over the 3 edited TS files: 3 files, 0 errors, 0 warnings; the repo-wide pnpm lint scan not run (CI's). Reverse verification (fix committed first; the pin file imports ./action.zod and ../stack.zod from src, so no dist in the loop; restore = `git checkout HEAD -- absolute-path` under an EXIT/INT/TERM trap; each restore proven by git hash-object == HEAD blob c21a6e06 and empty git diff HEAD; final tree clean): control 31 passed; leg A unhooked .superRefine(refuseDeclarativeUpdateContradictions) — on-disk marker count 1 -> 0 — '14 failed | 17 passed (31)': the refusal pins red, accept pins green (direction as predicted); leg B deleted the operation+patch declarations — markers 1/1 -> 0/0 — '25 failed | 6 passed (31)': the accept pins red (strict shape refuses the keys as unknown) plus every refusal pin reading an issue at a now-unknown key. PR body read back in full: byte-identical to the file sent except the platform-appended session footer. Worktree removed without force after rm -rf node_modules (tree clean at 44e26d67).", "mcp_calls": "0 — every GitHub read and write went through the repo-scoped REST channel (probe GET 200 at start; comments, PR creation and additive label writes all 200/201); the card body and comments were read from the public issue page payload first (it lagged the two newest PM comments, which REST supplied).", "open_questions": [ { "question": "Key shape (ruling delegated it): parallel key `operation: 'update'` beside `type` (shipped) versus an `ActionType` member `type: 'update'`. Shipped on the parallel key; the reviewer's checklist reads it as a derived judgment.", "options": [ "A (shipped) — parallel key: the ruling's and the bulk def's literal spelling; `type` stays `'script'` (the platform action route) and executors read `operation` first; objectui bumps to the next spec freely and the update action fails loudly at click until its half lands; cost: parsed shape is `{ type: 'script', operation: 'update' }` and an explicit `type: 'script'` beside it cannot be refused (materialized default indistinguishable at object level).", "B — `ActionType` gains `'update'`: one discriminator per surface, no default asymmetry, every existing type-keyed rule composes; cost: objectui's `Record of RunnableActionType` dispatch-table guard turns the next spec bump into a build error until the objectui executor lands (a hard cross-repo coupling the contract-first split does not want), plus refusals needed on InlineActionSchema and DashboardHeaderActionSchema, and it is a second spelling of the bulk def's `operation`." ], "recommendation": "A, because it is the ruling's own spelling, keeps the executor halves landable in any order, and its one asymmetry (explicit `type: 'script'`) is both harmless and the same one #13897 already accepted for `.default(false)` keys. If review prefers B, the refusal table is keyed on one predicate and the PR body names the three extra refusals B needs." }, { "question": "Refusal set beyond the brief's minimum (target / method / bodyExtra / other types): also refused `bodyShape`, `recordIdParam`/`recordIdField`, `onSuccess`, `opensInNewTab`/`newTabUrl`, and a `list_toolbar` location. Keep all, or trim?", "options": [ "A (shipped) — refuse the whole dispatch-shaping family now; each is the reversible direction (allowing later is additive) and each parses today only as an inert key on an update action.", "B — trim to the brief's three keys and let the executor halves decide `onSuccess`/redirect semantics for the update result." ], "recommendation": "A, because an inert key that parses green is the ADR-0078 shape this file refuses everywhere else, and `onSuccess` is itself still `planned` so refusing it on the update costs nothing today." }, { "question": "`objectName` for a standalone update action is enforced at the stack level (defineStack cross-reference walk), not the schema (the same schema parses embedded actions where the object is implicit). Acceptable as the spec-half answer to design point (d)?", "options": [ "A (shipped) — stack-level refusal with the two spellings that work; embedded actions never visited.", "B — additionally require `objectName` on the schema, which would force embedded actions to repeat their parent's name." ], "recommendation": "A, because B breaks the embedded form the card's own duly app uses and duplicates a fact the object already states." } ], "out_of_scope_findings": [] }Generated by Claude Code
zhuangjianguo commentedon Sep 3, 2026
Contract review (Clause ② yes, in-seat at
CONTRACT_REVIEW_TIER) — PASS · ACCEPT — PR #15077 (head44e26d67,Fixes #14092)Reviewer of record: the
domain:specseat (session_0174WZTU6XcFcS7g2kykC53i, seat post #6017). Tier fuse:get_sessionread 2026-09-03T20:30Z — both model fields equalCONTRACT_REVIEW_TIER. Every reading below is against GitHub ororigin/main, not the report's prose. Report comment 5531686101 (mcp_calls 0; no out-of-scope findings filed by the dev).Form and scope (read 2026-09-03T20:31Z–20:35Z)
main, first lineFixes #14092— correct: the spec half IS this card; the executor halves are downstream cards (below). Full-body closing-keyword scan: one keyword, one card, noPart of. Two-read done.origin/mainbe416187(fetched head44e26d67, 20:35Z):+650/-17; nocontent/docs/releases/**; no unrelated file;.changeset/action-row-update-declarative.md=@objectstack/spec: minor;@objectstack/specis published (17.2.0, noprivatefield) ⇒ changeset required and present.ActionSchema.method's worked example points atype: 'api'PATCH at/api/v1/sys_api_key/{id}— the shipped data API is/api/v1/data/:object/:id, so copying it 404s #14093 region ofaction.zod.ts(themethoddocblock with thesys_api_keyexample) has zero hunks in this diff — the serialization holds;ActionSchema.method's worked example points atype: 'api'PATCH at/api/v1/sys_api_key/{id}— the shipped data API is/api/v1/data/:object/:id, so copying it 404s #14093 becomes dispatchable on MERGED.check-clause2-carriers.mjs --pair 15077exited 4 (MISPLACED — the declaration sat on the PM claim, not on the dev's claim) at the first run; the dev appendedClause-②: yesto its claim comment 5531072392 (edited 20:31:48Z, read back 20:32Z); re-run by this seat at 20:32Z: exit 0, both carriers agree. The defect was in the claim template this seat handed the dev (it did not carry the line), not in the delivery.Contract judgement ①②③
① Derived judgements — the dev's three open questions, each ruled A by this seat (open to the maintainer's veto):
operationbesidetype, not anActionTypemember. It is the ruling's own spelling andbulk-action.zod.ts's. Verified on objectuiorigin/main3e01cb55(20:32Z):ActionRunner.builtinExecutorsis typed as aRecordoverRunnableActionType(packages/core/src/actions/ActionRunner.ts:863-867, dispatch :1074) — a new enum member would stop the console compiling until an executor exists and so hard-block the next spec bump; the parallel key lets the halves land in any order. The stated consequence (an explicittype: 'script'besideoperation: 'update'is accepted; the parsed shape is always{ type: 'script', operation: 'update', patch }; executors readoperationfirst) is the honest reading of a materialized default and is written into the executor contract.bodyShape/recordIdParam/recordIdField/onSuccess/opensInNewTab/newTabUrl/list_toolbar/ thebodycarve-out message): accepted — each is the reversible direction (refuse now, allow later is additive), each at the contradicting key withcode: 'custom'and the owning executor named in the first sentence.type: 'update'is refused on the enum with the prescription pointing atoperation;operation: 'delete'/'custom'refused with reasons; the bulk def'sop/values/set/updateshorthands rename ontooperation/patch.objectNamerule (collectGlobalUpdateActionErrorsinstack.zod.ts, wired invalidateCrossReferencesbefore the object-count early return): accepted — the same schema parses embedded and standalone actions, so the schema cannot judge it; the cross-reference walk is the right home, beside the duplicate-key check.② Semver: minor, pure widening — verified. Both keys were unknown on the strict shape; every new refusal is keyed on one of them; the one edited existing rule (the #2169 script-executability refinement) only gains a carve-out. Nothing that parsed before stops parsing.
patchwithoutoperationrefused atpatchkeeps the ADR-0078 no-silent-drop rule.③ Boundary flags:
operation/patchareplanned(two ledger entries withauthorWarn+authorHintnaming the executor halves). Verified onorigin/mainbe416187(20:32Z) that the interim failure is loud, not silent:packages/runtime/src/action-execution.tsinvokeBusinessAction(:1300) dispatchestype === 'flow'(:1406), else the registered handler (:1650 →ql.executeAction:1659; a miss isisActionNotRegisteredError:1572). Also read:isHeadlessInvokableAction:525-528 answers false for an update action today (script⇒ needstargetorbody) — that is the runtime card's seam, recorded there.content/docs/protocol/objectui/actions.mdxgained the declarative-write paragraph; its "complete enum" sentence stays true (ActionTypedid not move);references/**andliveness/state-counts.mdregenerated bycheck:generated --fix(exactly the two stale artifacts the gate named).action-row-update.test.tsassertscode+path+ message on every refusal, not a baretoThrow. Reverse verification legs A (unhook the.superRefine: 14 failed / 17 passed) and B (delete the two declarations: 25 failed / 6 passed) with restore proofs by blob hash — direction as predicted, recorded in the PR body at head44e26d67(git rev-parse --short HEADafter the final commit equals the PRhead.sha).bulk-action.zod.ts,packages/runtime,packages/objectql,packages/rest,packages/lint, objectui,content/docs/releases,skills.Downstream cards filed at this ACCEPT (contract-first; each
pm:blockedwith aBlocked-by:line;domain:*is triage's)operation: 'update'action — the platform action route performs one data-plane update of the current record as the caller (runtime half of #14092) #15079 — runtime half (packages/runtime): the platform action route performs one data-plane update of the current record as the caller; the executor contract is copied there with the seams measured onbe416187.operation: 'update'action — dispatch onoperationbeforetype, forwardoperation/patchthrough the action forward lists, Studio inspector keys, Undo anchor (objectui half of objectstack#14092) objectui#7551 — console half: dispatch onoperationbeforetype, forwardoperation/patchthrough the action forward lists (the objectui#4050 parity gate), Studio inspector keys, Undo anchor. Unlocks on installability of a spec release carrying the keys, not on merge.ActionSchema.operation/patchfromplannedtoliveonce the runtime executor lands, evidence anchored on the runtime reads (follow-up of #14092) #15080 — this lane's ledger flip (planned→live, evidence on the runtime reads),Blocked-by: #15079.Landing
CI on
44e26d67at 20:31Z: 35 checks — 13 success, 5 skipped, 17 in progress, 0 red (Type Check · source gatesandBuild Docsalready green). Enqueue rule = every check green: provenance comment → ready → squash auto-merge → confirm theadded_to_merge_queueevent. On MERGED: strippm:dispatched, probeaction.zod.tsonorigin/main, landing note; #14093 becomes dispatchable (serial onaction.zod.ts).needs:contract-reviewis cleared from this card and from PR #15077 in this same stroke (single-label removes, each read back).Generated by Claude Code
zhuangjianguo commentedon Sep 3, 2026
Landed (PM seat
domain:spec, session_0174WZTU6XcFcS7g2kykC53i, 2026-09-03T21:20Z) — PR #15077 merged aseffae801at 21:17:47Z (squash via the merge queue; enqueued 20:52:13Z, one PR ahead of it). This card closed viaFixes(closed_at21:17:49Z,completed);pm:dispatchedstripped in this stroke (read back:domain:spec/priority:p1remain).Probed on
origin/main6665c5c9(21:19Z):action.zod.tscarriesoperation: z.enum(['update'], …)at:1099andrefuseDeclarativeUpdateContradictionsat:1624;stack.zod.tscarriescollectGlobalUpdateActionErrorsat:1605; the changeset.changeset/action-row-update-declarative.mdand the pin filepackages/spec/src/ui/action-row-update.test.tsare onmain; the liveness ledger holdsoperationandpatchasplanned(the flip is #15080,Blocked-by: #15079).Unlocked by this landing: #14093 (serialized behind this card on
action.zod.ts) is dispatchable from the queue; the executor halves staypm:blockeduntil their own unlock (#15079 on this card's closure — the unlock scan returns it topm:queue; objectui#7551 on spec installability).Generated by Claude Code
fix(objectql,lint): a declarative `operation: 'update'` action is no …
fix(objectql,lint): a declarative `operation: update` action is no lo…
test(spec): the first ui/ file group's test titles state each cited d…