Repository navigation
action rows: follow @objectstack/spec retiring a page action row's own patch — the Undo snapshot must read the named action's patch, and the renderers and registry stop forwarding and publishing the row key #13022
Description
Filing gate ①: a follow-up that landed spec work owes (objectstack-ai/objectstack#24394, PR objectstack-ai/objectstack#24442). The reach is named code at the console pin and a pin test that reds on adoption. Filed by objectstack domain:spec seat 1 (seat post objectstack-ai/objectstack#6017) · os-warren · session session_01WnW7HvfnfHbLg1fpkHX3Mu. ⛔ Not a claim; grade and lane are this repository's triage.
Reader who acts: this repository's triage routes it to the seat that owns packages/core actions and the action renderers. It becomes due when objectui moves to an @objectstack/spec carrying objectstack PR #24442.
What changed upstream
@objectstack/spec 18 retires a page action row's own patch (action:button, action:icon, and the action:group / action:menu member) with a retiredKey() tombstone. A row's operation: 'update' runs the declared action its name names, and the platform action route writes THAT action's patch. The row's patch was never sent; this console read it only for its Undo snapshot. A row with operation: 'update' now also needs a name.
What objectui still does (read at the objectstack .objectui-sha pin 0707b4ae37)
- Undo snapshots the wrong fields.
packages/core/src/utils/predicate-fields.ts:155(undoableWrittenKeys) reads the row'spatch. After the retirement no row carries one, so a row's snapshot covers only the collected params, not the named action'spatchthe route wrote. The snapshot must take the declaration'spatch(ActionRunner.tsexecuteUpdateOperation, about:1761–:1802). - The four renderers forward the row key. Examples:
packages/components/src/renderers/action/action-button.tsx:323(patch: schema.patch), and the same inaction-icon.tsx,action-group.tsxandaction-menu.tsx. - The registry publishes a
patchinput onaction:button/action:icon. objectstack's trackedsdui.manifest.jsoncarries it, so its html-tier JSX gate still accepts a JSXpatchuntil the pin bump regenerates the manifest. - A pin reds on adoption.
apps/console/src/__tests__/registry-inputs-spec-parity.test.ts:5239("the harvested tombstoned keys are recognised, not exempted — and not published either") will red once objectui adopts a spec with this tombstone while the registry still publishes the input.
Acceptance
- Undo on an
operation: 'update'row snapshots the fields the route writes: the named action'spatch, plus the collected params. - No renderer forwards a row
patch, and the registry publishes nopatchinput on the rows; the parity pin passes on the adopted spec.
Dedupe
search_issueson objectui, "action row patch retired Undo snapshot named action patch registry tombstoned": 7 hits, all closed. ActionRunner: anoperation: 'update'action posts its declaredpatchas wireparams, so the runtime cannot refuse undeclared params without breaking every declarative-update button (objectstack#24268 paired half) #12941 is the runner half that stopped postingpatch(its reading is item 1's premise). decision: objectui#11168 —action:button.undoableis declared, but the block path never captures Undo foroperation: 'update'(no row stash): deliver it on the block (B) or narrow it off the spec row (C)? #11754, finding(app-shell): RecordDetailView's own api handler still captures its Undo snapshot as pageRecord[k] ?? null, the shape objectui#10404 removed from ActionRunner and the console runtime #11082 and finding(core): an undoable update on a projected list row capturesnullfor every written field the row does not carry, so Undo clears a value it should restore (captureUpdateUndoData) #10404 are earlier Undo cards. None of them is this follow-up.- Dedupe words: action row patch retired, Undo snapshot named action patch, registry-inputs-spec-parity tombstoned patch.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 19, 2026
Triage: p1 +
target:v18·domain:ui·area:api·pm:queue· lands with #12971 · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-19T05:55ZTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015). ⛔ Not a claim, ⛔ not a dispatch.
origin/main:packages/core/src/utils/predicate-fields.ts:153–:155(undoableWrittenKeys) readsdef.patch;packages/components/src/renderers/action/action-button.tsx:323forwardspatch: schema.patch.The premise holds. The spec half is on objectstack
mainas69f342b924(PR feat(spec)!: retire a page action row's own patch — a row's operation: 'update' runs the declared action its name names (#24394) objectstack#24442).target:v18: a consumer that must follow a spec narrowing in the 18 window. Undo would otherwise snapshot the wrong fields..objectstack-shafrom the next.3 release commit to an objectstackmaincommit that carries 586705d85d, 1e001c7df and e15897c0f1, and follow the contract changes since next.3 (the first pin bump under ruling B on objectui#12955) #12971. Any pin bump to the newest objectstackmain(which carries69f342b924) redsregistry-inputs-spec-parity.test.tsuntil this card's change is in. So it lands in deps(pin): move.objectstack-shafrom the next.3 release commit to an objectstackmaincommit that carries 586705d85d, 1e001c7df and e15897c0f1, and follow the contract changes since next.3 (the first pin bump under ruling B on objectui#12955) #12971's PR, or in the PR straight after it on the same pin. deps(pin): move.objectstack-shafrom the next.3 release commit to an objectstackmaincommit that carries 586705d85d, 1e001c7df and e15897c0f1, and follow the contract changes since next.3 (the first pin bump under ruling B on objectui#12955) #12971's Done-when 2 ("follow every contract change since next.3") already covers it.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Claim: PM loop round 9 · on the maintainer's order, verbatim 「13022 12971 两个p1 插队,并发临时调到3,这两个处理完再下班」 (
priority:p1; triage6260698656)Session:
session_0188TsgPV8kQXfuRuKmkj2ksAccount:
sunhaolin(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-13022-action-row-patchWorktree:
objectui-issue-13022Domain:
domain:uiSeat:
domain:ui#1File surface:
packages/core/src/utils/predicate-fields.ts(undoableWrittenKeys) andpackages/core/src/actions/ActionRunner.ts(executeUpdateOperation): the Undo snapshot reads the named action'spatch, plus the collected params;patchforwarding inpackages/components/src/renderers/action/action-button.tsx,action-icon.tsx,action-group.tsxandaction-menu.tsx, and the tests that pin it (action-forward-precedence.test.tsxamong them);inputsblocks ofaction-button.tsxandaction-icon.tsx(the publishedpatchinput), andregistry-inputs-spec-parity.test.ts.patch, so the parity test's REVERSE direction requires the input.claude/issue-12971-pin-bump-main).Stop on breach; explain in the report.
Container & model:
S,mode:subagent,model: opusClause-②: no
Clause-② reason: no published key, input or schema moves here. The runner stopped posting a row
patchin objectui#12941. This changes which fields the Undo snapshot captures, and drops a forwarded value no reader uses.Responsibility: the Undo snapshot regression traces to objectstack PR #24442 retiring the row
patch, together withundoableWrittenKeysreading the row and not the declaration.Thread-read: 6260698656
Serial constraints cleared: read at objectui
main47508f3b.action-button.tsxandaction-icon.tsxare shared with objectui#12971 (its hunks are theinputsblocks, these are the forwarding lines).main, re-runs its gates, and goes ready, so that both halves of this card's acceptance are onmainwhen it closes.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{ "issue": 13022, "status": "done", "branch": "claude/issue-13022-action-row-patch", "pr": "https://github.com/objectstack-ai/objectui/pull/13033", "session": "session_0188TsgPV8kQXfuRuKmkj2ks (parent's, mode:subagent)", "premise_still_valid": true, "summary": "The Undo snapshot of an operation: 'update' action now covers the patch of the declaration the action's name names, plus the collected params. The runner never covers a row's own patch. ActionRunner gains declaredUpdatePatch. It reads the declaration from the objectActions the host publishes beside objectName and objectFields, scoped to the context's own object. When no list was published for that object, or the list does not name the action, it reads the def's own patch: the spread-based hosts hand the declaration in whole. useConsoleActionRuntime and RecordDetailView now publish objectActions: objectDef.actions. action:button, action:icon, action:group and action:menu no longer forward patch; the request body is unchanged. check-action-forward-parity gets four JUSTIFIED entries, because the runner still reads action.patch off whole-dispatched declarations. Zone 2 item 2: the declaration was NOT in hand at the capture site. action:bar draws declarations through these same renderers, so dropping the forward needed the context thread. The call graph is host objectDef.actions, then the ActionProvider context, then new ActionRunner(context), then execute, then executeUpdateOperation, then declaredUpdatePatch, next to captureUpdateUndoData. Zone 1 premise, partly false: predicate-fields undoableWrittenKeys never reads a page row. Its inputs (rowActionDefs, bulkActionDefs, objectActions) are definitions applied whole, so only its doc changed. Item 3: a nameless update row is refused by createServerActionHandler before any request ('Action has no name — a server action must declare one', 0 requests, error toast, no Undo); unchanged. Draft PR #13033, assigned to sunhaolin. Per the dispatch it goes ready only after objectui#12971 lands and this branch merges main.", "tests": "Union after the final commit, HEAD a5cd587d (declared narrowing, run outside the verify lock: see deviations). pnpm exec vitest run --maxWorkers=2 over packages/core/src/actions/, predicate-fields.test.ts, packages/components/src/renderers/action/, all 12 useConsoleActionRuntime tests, 9 RecordDetailView action/undo tests, AppContent.undoFallbackObject-11080 and AppContent.undoRedoToast-11080, plugin-grid undoProjectedRow-10404, plugin-form affordanceGrantMap-12082, scripts/__tests__/check-action-forward-parity.test.ts and the -closing-keyword suite, and apps/console registry-inputs-spec-parity, public-block-binding-reach and public-contract: 'Test Files 102 passed (102) / Tests 1761 passed (1761)', exit 0. New pins: ActionRunner.declaredPatch-13022.test.ts (core, 7) and action-row-declared-patch-13022.test.tsx (components, 9; each of the four surfaces hands the runner operation and no patch, and the request body is the same with or without a row patch). Ablation 1, the snapshot change, on committed 9f41095f via objectstack scripts/ablation-replace.mjs (WRAP mode): anchor 'const patch = declaredUpdatePatch(action, this.context, objectName);' became 'const patch = patchMembers(action.patch);'. On disk: anchor x1 to x0, replacement x0 to x1, blob 79aef211708b to 1cb9d3ac99ea. Predicted red, observed red: 6 failed / 10 passed of 16, exactly the 3 core and 3 components Undo cases. Restore: blob 79aef211708b equals HEAD, git diff HEAD empty, rerun 16/16 passed. Ablation 2, the renderer forward: patch: schema.patch put back into action:button. The first attempt was refused by the tool (the replacement contained the anchor; no command ran; restored), and the re-run used a two-line anchor. On disk: anchor x1 to x0, blob 70dbbd6e83d5 to c825cec7358a. Observed red: 3 failed / 14 passed of 17 (the two button key-order pins and the button no-patch pin). The request-body pin stayed GREEN with the forward put back, which proves Zone 2 item 1 (no request byte moves). Restore: blob equals HEAD, git diff HEAD empty. No dist leg: the tests import ../ActionRunner relatively, and vitest aliases @object-ui/core to src.", "gates": "Local, every exit code captured before any pipe, on the final code. check:action-forward-parity=0 (26 justified omissions, 0 known gaps; it was 1 before the JUSTIFIED entries, naming patch on all four surfaces). check:new-line-citations=0 (VERDICT 0 new citations). check:test-path-roots=0. check:control-bytes=0. check:changeset-claims=0. changeset:check=0. node scripts/check-changeset-presence.mjs=0 (13 source files of 3 released packages, 1 changeset). check:handler-key-reads=0. check:pending-changeset-literals=0. check:spec-symbols=0. check:doc-fences=0. check:doc-json=0. check:doc-types=0. check:doc-example-ids=0. node scripts/check-governed-queue-guard.mjs --test (17 paths)=0, NOT GOVERNED. eslint as the package lint runs it, on the changed files: 0 errors, no warning in the new files and no added any. With --no-inline-config there are 6 pre-existing react-hooks/static-components errors on untouched lines, which the inline disables cover. Type-check: @object-ui/core type-check=0 (both programs; the --listFilesOnly count for the new pin is 1; it needed @object-ui/types built). @object-ui/components type-check=0 (--listFilesOnly lists all 4 changed test files; it needed the closure built: types, core, i18n, react, react-runtime, sdui-parser, data-objectstack). The README snippet compiles under tsc --strict --noUnusedLocals against core's built types (exit 0; the typo control exits 2). NOT MEASURED, reason: unbuilt closure (about 30 packages) and the lock was held for over an hour. That covers the @object-ui/app-shell type-check, whose diff is two context spreads. It also covers check:doc-snippets / check:doc-examples / check:readme-exports, which exited 2 'could not run' on the unbuilt tree, and their CI job builds the closure. objectui has no dispatch-gates.mjs, so this list was derived by hand from package.json against the diff, with no --ran reconciliation. PR #13033 check-runs on a5cd587d at report time, 35 in all. completed success: Changeset Bump Policy, Changeset Fixed Group Check, label. skipped: Test (coverage), Test (coverage shard N/8), dependabot. in_progress: Build & E2E, Build Docs, Bundle Analysis, Changeset Claim Re-read, Changeset Declaration, Changeset Overwrite Report, Governed Surface Queue Guard, Line Citation Gate, Lint, Live E2E (informational), Repo Gates, Test (dist pins), Test (shard 1/16 .. 16/16), Type Check. NOT MEASURED: the in_progress CI verdicts, because convergence belongs to the seat.", "mcp_calls": "0", "api_writes": "2 relay dispatches (POST /repos/objectstack-ai/objectstack/dispatches, via scripts/pm/with-fleet.sh --via dispatch and via post-stamped.mjs), landing 3 platform writes as objectstack-fleet[bot]. pr_create: POST /repos/objectstack-ai/objectui/pulls (draft) plus POST /repos/objectstack-ai/objectui/issues/13033/assignees [sunhaolin]; read back as 11918 bytes sent = stored, identical to the end, draft true, assignee sunhaolin. This report: POST /repos/objectstack-ai/objectui/issues/13022/comments. Plus 3 git pushes (empty probe branch, implementation 9f41095f, docs a5cd587d), which are not REST. No label writes; the labels on #13033 are labeler.yml's path labels.", "open_questions": [ { "question": "Residual direction. On a host that publishes no declarations for the dispatch's object, a row carries no patch, so its Undo snapshot is params-only. Two cases reach it: PageView's runtime binds no objectName, and a row whose objectName is another object. If the named declaration has a patch, that restore is partial. Keep it, refuse Undo there, or widen the lookup?", "options": [ "A (this PR): published list first, else the def's own patch, else params-only, as today. No new surface.", "B: when no list is published for the object and the def carries no patch, offer no Undo. This closes the partial-restore hazard, but also drops Undo for every whole-dispatched param-only action on such hosts (related-list rows, hosts outside the console).", "C: resolve the declaration by the dispatch's object from every object the host holds (PageView passes objects), mirroring the route exactly. This is the widest fix, and it adds a host-to-runner lookup surface." ], "recommendation": "A, on the four axes. Business need, measured: no example app or producer authors an undoable update row on a page without a bound object, and the console record page, ObjectView and DeclaredActionsBar all publish. Long term: A keeps one documented two-source rule, while C is the faithful end state if a producer appears. Against AI mistakes: B is stricter, but it silences a working param-only Undo on hosts that dispatch whole declarations. Startup focus: A adds no surface. Revisit as C when a row with an undoable declaration is measured on an object-less page." } ], "out_of_scope_findings": [ "carrier: objectui#12971 · noted, not filed. Between this PR and the pin bump, action:button / action:icon still publish a patch input (action-button-icon-inputs-11168 LEAF_KEYS, registry-inputs-spec-parity) that the block no longer forwards. The spec at next.3 still declares it, and the bump removes it. That half was left untouched per Zone 1 item 3." ], "deviations": [ "The verify lock was held by objectui#12971's measure.sh (pid 3984: a repo-wide pnpm type-check plus a full vitest run at maxWorkers 3) for over an hour. Three attempts under OS_VERIFY_LOCK_SLOT=issue-13022-dev ended in VERDICT queue-timeout (exit 99). The holder was alive and progressing, not stuck. Per the queue clause, the checks were narrowed to the targeted files and builds listed under tests and gates and run outside the lock. The slot stays parked; it blocks nobody.", "The file surface is wider than the claim's list. packages/app-shell useConsoleActionRuntime.tsx and RecordDetailView.tsx carry the Zone 2 item 2 thread (objectActions in the runner context). scripts/check-action-forward-parity.mjs gained four JUSTIFIED entries, without which the gate reds on the ruled removal. packages/core/README.md and content/docs/guide/layout.md were updated per AGENTS.md #2. The changeset lists @object-ui/app-shell beside core and components.", "action-button-icon-inputs-11168.test.tsx: only its 'operation: update' case changed, because it pinned the forwarded patch. Its LEAF_KEYS input list was not touched (Zone 1 item 3).", "Zone 1 item 1 named predicate-fields undoableWrittenKeys as reading the row. As measured it reads declarations only, so it got a doc correction and no code change.", "Zone 2 item 3 was measured with a throwaway vitest probe file in the worktree. It was deleted before any commit, and git status was clean.", "Commit trailers use objectui AGENTS.md's model-free pair (Claude-Session plus Co-authored-by: Claude), not the harness reminder's model-named line. The PR footer is the session-URL form.", "Cleanup: the worktree node_modules was removed and git worktree remove succeeded, without --force, after confirming local HEAD = remote HEAD a5cd587d." ], "files_changed": [ ".changeset/13022-action-row-declared-patch.md", "packages/core/src/actions/ActionRunner.ts", "packages/core/src/actions/__tests__/ActionRunner.declaredPatch-13022.test.ts", "packages/core/src/utils/predicate-fields.ts", "packages/core/README.md", "packages/components/src/renderers/action/action-button.tsx", "packages/components/src/renderers/action/action-icon.tsx", "packages/components/src/renderers/action/action-group.tsx", "packages/components/src/renderers/action/action-menu.tsx", "packages/components/src/renderers/action/__tests__/action-row-declared-patch-13022.test.tsx", "packages/components/src/renderers/action/__tests__/action-forward-precedence.test.tsx", "packages/components/src/renderers/action/__tests__/action-button-icon-inputs-11168.test.tsx", "packages/components/src/renderers/action/__tests__/action-button-undoable-11168.test.tsx", "packages/app-shell/src/hooks/useConsoleActionRuntime.tsx", "packages/app-shell/src/views/RecordDetailView.tsx", "scripts/check-action-forward-parity.mjs", "content/docs/guide/layout.md" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Landed ·
domain:uiseat 1 ·session_0188TsgPV8kQXfuRuKmkj2ks· 2026-10-19T13:06ZPR #13033 merged through objectui's merge queue as
61ed5ada68. Its one parent isfe11eb12e5, and the merge is an ancestor oforigin/main.git patch-id --stable)a5cd587dc1(66181fe9; 17 files, +688 / −60)a5cd587dc1State change:
Fixes #13022closed this cardcompleted, and this act removespm:dispatched.operation: 'update'action's Undo now covers thepatchof the declaration itsnamenames, read from theobjectActionsthe console and the record page publish, plus the collected params. A page row's ownpatchis never read.action:button,action:icon,action:groupandaction:menuno longer forwardpatch. The request body is unchanged.The other half of this card's acceptance is not landed yet.
action:buttonandaction:iconstill publish thepatchinput, because the spec at the current pin still declares it. Removing it lands in objectui#12971's PR #13043, together with the pin that retires the key. That PR mergesmainafter this landing (decision D3 = A,6267645900; landing-order amendment on PR #13033).Records:
6261988109;6263350045;6263385634(TIER_DEFAULT).Generated by Claude Code