You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
finding(core): an undoable update on a projected list row captures null for every written field the row does not carry, so Undo clears a value it should restore (captureUpdateUndoData) #10404
Filing-gate category: ① a product defect with a named site and a reproduction. Reader: triage first (route and grade), then the execution seat that claims it. The site is packages/core/src/actions/ActionRunner.ts: captureUpdateUndoData. The console's generic api handler takes the same kind of undo snapshot (rowRecord[k] ?? null).
Filed by the domain:ui#4 execution seat (session_01BP8CMtACxTdLjqR6rhd33C) from the os-dev-report of objectui#10277 (PR objectui#10386). The dev measured it and correctly left it out of that PR's surface. ⛔ Filed bare, not graded here.
The defect
captureUpdateUndoData(writtenFields, rowRecord) is undoData[field] = rowRecord[field] ?? null. Its docblock says: "A field absent from that row is captured as null, which is what clearing it back to empty means on the data plane." That holds for a full record. On a list row projected by $select (ListView, ObjectGrid, RelatedList all project), a written field that is not a column or a harvested predicate field is absent from the row. It is NOT empty on the server, so Undo writes null over a value it should restore.
Reproduction (the dev's probe on objectui#10386's head, relayed, ⛔ not re-run by the seat)
A real ObjectGrid with columns: ['name'], and rowActionDefs: [{ operation: 'update', undoable: true, type: 'script', patch: { status: 'closed' } }], a stub script handler, and a data source that honours $select. The runner's result.undo.undoData was { status: null } while the stored value was 'open'. Control: with status as a column, it read { status: 'open' }. The seat confirmed the ?? null capture on origin/main.
Grading notes (for triage, not a grade)
There are two readings. Both are the fixing seat's to measure:
Harvest the written keys. Include patch keys, param payload names and bodyExtra in the $select harvest (listViewPredicates, the same family objectui#10277 extended), so the row carries what an undoable update writes.
Refuse or omit Undo when the row lacks a written key. Never capture an absence as null.
A third option is to fetch the current values before the write. That costs a request per action.
Related: objectui#10277 (the harvest), whose pending changeset for objectui#10186 still says a row action reading a field through other paths gets it only if the projection carries it. That remains true of patch keys.
Dedupe
REST page walk over the 1000 most recently updated objectui items, pattern (undo|undoData|captureUpdateUndoData) … (null|projected|$select|not in columns|absent) or captureUpdateUndoData ⇒ 0 hits. Must-hit control defaultFromRow ⇒ objectui#10277, which did hit.
Dedupe words: undoable update undoData null projected row · captureUpdateUndoData absent field null $select · undo clears field not in columns · listViewPredicates patch keys undoable
Triage: lands in @object-ui/core (+ the console twin in @object-ui/app-shell) ⇒ domain:ui, bug, priority:p1, pm:queue (finding removed — graded); rationale: list rows are projected by $select (ListView, ObjectGrid, RelatedList), so a field an undoable update writes but the row does not carry is ABSENT from the row, not empty on the server — captureUpdateUndoData records it as null and Undo then writes null over the stored value (measured by the objectui#10277 dev: undoData read { status: null } while the stored value was 'open'; the control with status as a column read 'open'); the undo safety net silently destroys data on an ordinary configuration (any undoable update whose written field is not a column), hence p1; the harvest PR objectui#10386 has landed (961ceaa3), so no known sibling is in flight on these files.
Claim: PM loop round 1 — domain:ui execution seat
Session: session_01BA3nKVUwKQJf8DBxrSVtNC
Branch: claude/issue-10404-undo-absent-field-not-null
Worktree: objectui-issue-10404
Domain: domain:ui
Seat: domain:ui#1
File surface: packages/core/src/actions/ActionRunner.ts (captureUpdateUndoData, its docblock, and the undo construction that calls it), packages/app-shell/src/hooks/useConsoleActionRuntime.tsx (the console api handler's undo snapshot, rowRecord[k] ?? null), packages/core/src/utils/predicate-fields.ts (listViewPredicates, only if the harvest reading is chosen), tests beside them, and one .changeset/10404-…md (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: opus (default judgement tier) — priority:p1: Undo silently writes null over a stored value on an ordinary configuration
Clause-②: no
Thread-read: 5823920458
Serial constraints cleared: open-PR file lists read 2026-09-24T23:26Z ⇒ none touches these three files. Live pm:dispatched claims read at the same time ⇒ none names them. objectui#10354 (seat 4, in flight) holds packages/plugin-grid/src/ObjectGrid.tsx ⇒ ⛔ this take does not edit it; a consumer change needed there is a stop-and-report. The harvest precedent objectui#10386 (objectui#10277) has merged as 961ceaa3.
Scope
The triage grading (5823920458):
⛔ Never record a field that is absent from the row as null.
The implementer measures and picks the fix, with reasons: A, harvest the written keys (patch keys, param payload names, bodyExtra) into listViewPredicates; B, refuse or omit Undo when the row lacks a written key, and say so. The triage reading, which is not a ruling, is A first with B as the backstop for keys only known at run time.
Fix the console twin in useConsoleActionRuntime.tsx in the same change.
Correct the captureUpdateUndoData docblock.
Pin it with a reverse proof: a grid with only a name column and an undoable patch: { status: 'closed' }.
Clause-②: no holds while listViewPredicates keeps its exported signature (the objectui#10277 precedent). A new export or a widened exported signature is a stop-and-report. ⇒ Fixes #10404.
{
"issue": 10404,
"status": "done",
"branch": "claude/issue-10404-undo-absent-field-not-null",
"pr": "https://github.com/objectstack-ai/objectui/pull/10436",
"session": "session_01BA3nKVUwKQJf8DBxrSVtNC — subagent run, so the parent's id (the Claude-Session trailer on every commit); the claim comment 5823964084 names the same session and the same branch, confirmed before the first edit",
"premise_still_valid": true,
"summary": "Reproduced on origin/main 961ceaa32 with a real ObjectGrid (columns ['name'], undoable patch { status: 'closed' }, a $select-honouring data source): $select [id,name], undoData { status: null }, stored status null after Undo; control with status as a column: undoData { status: 'open' }, restored. Implemented A and B together: listViewPredicates now also harvests the fields an undoable action writes (patch keys, each param's collected key name-else-field, bodyExtra keys; recordId excluded; BARE_IDENTIFIER gate; still behind every consumer's declared-field and FLS gates, signature unchanged, no new export), and ActionRunner's captureUpdateUndoData plus the console api handler's twin never capture an absent (or undefined-valued) field as null: they offer no Undo at all and console.warn the missing fields, while a carried null is still restored as null; the captureUpdateUndoData docblock is corrected. B tells the user through the only existing channel, measured: Undo is offered solely as the success toast's Undo button plus the UndoManager stack, so B's toast has no Undo button and nothing is pushed; no new UI surface or copy (the runner emits only success/error toasts and both console toast handlers render any non-error type as success). The assignee was already os-bill (PM's) and was not touched; ObjectGrid.tsx and every consumer source are untouched; one test file sits outside the claim's listed file surface, the binding grid pin packages/plugin-grid/src/__tests__/undoProjectedRow-10404.test.tsx, which cannot live beside the core sources because core cannot import ObjectGrid; commit trailers are model-free per the dispatch (the harness reminder asked for a model-named Co-Authored-By; not used).",
"tests": "Ablation (commit-first, exit trap, BASE blobs hash-verified on disk, restored from HEAD with blob hash equal and git diff HEAD empty; sources resolve via the root vitest alias to src, so no build between edit and run), at beaf30a40 over the 6 pin files (61 cases): all three sources ablated ⇒ 13 failed / 48 passed, grid PIN 1 'expected null to be open', PIN 2 'expected null to be closed'; predicate-fields.ts only (A off) ⇒ 7 failed / 54 passed, PIN 1 'expected closed to be open'; ActionRunner.ts + useConsoleActionRuntime.tsx only (B off) ⇒ 6 failed / 55 passed, PIN 2 'expected null to be closed'; HEAD ⇒ 61 passed. Surfaces probed (temporary, deleted), base vs head $select with an undoable patch { status } def: ListView rowActionDefs and object actions [id,name] ⇒ [id,name,status]; non-undoable control [id,name] both; RelatedList authored [id,name] ⇒ [id,name] then [id,name,status] after the child schema lands; RelatedList derived: no $select both. Suites: vitest run packages/core/ at beaf30a40 167 files / 3507 tests passed; packages/app-shell/ at beaf30a40 in 8 shards (one run exceeded the foreground cap, exit 124, NOT a failure reading) 775 files (774 passed, 1 skipped) / 7633 tests (7624 passed, 9 skipped), every shard exit 0; packages/plugin-grid/ whole at beaf30a40 152 files / 1455 passed; plugin-list + plugin-detail, the 46 files naming $select / listViewPredicates / rowActionDefs / undoable / globalUndoManager plus every RelatedList* suite: 46 files / 445 passed; text-reading tests (git grep of tests naming a touched file or directory and reading the fs) outside core/app-shell: 26 files / 1120 passed; final head 111d59d23: packages/core/ + app-shell hooks __tests__ + grid pins 212 files / 3941 passed. Type-check (tsc --noEmit && tsc -p tsconfig.test.json) core, app-shell, plugin-grid after turbo build --filter='@object-ui/app-shell^...' (29 packages): exit 0 each; --listFiles shows each test program compiles its new/edited test files. Gates at 111d59d23, exit 0 with their own verdict lines: check:control-bytes OK, check:new-line-citations '0 new citation(s)', check-changeset-presence (1 changeset), check-changeset-no-major, check-changeset-fixed, check:changeset-claims (report-only: 8318 and action-keys-warning-text-5642 name ActionRunner.ts; both paragraphs read, still true), check:pending-changeset-literals, check:test-path-roots, check:vi-mock-specifiers/-inherit/-override-shape, check:action-forward-parity, check:handler-key-reads. Lint narrowed and declared: eslint --no-inline-config --format json over the 8 touched ts/tsx at 111d59d23 ⇒ 8 files in output, 0 errors, 57 warnings, all in ActionRunner.ts (20 no-explicit-any) and useConsoleActionRuntime.tsx (32 no-explicit-any, 2 exhaustive-deps, 2 react-hooks/refs, 1 only-export-components), per rule identical to their base blobs; test files 0; population = root eslint.config.js files ['**/*.{ts,tsx}']; invariance = no parserOptions.project/projectService (0 hits, control languageOptions 1 hit) so no type-aware rule. NOT MEASURED locally: rest of plugin-list/plugin-detail and other packages (CI); repo-wide pnpm lint (CI). CI: in_progress at report time, not awaited.",
"mcp_calls": "0 — no MCP GitHub tool called",
"api_writes": "2 — both through the fleet-write relay (with-fleet.sh --via auto, route dispatch, as objectstack-fleet[bot]), each one POST /repos/objectstack-ai/objectstack/dispatches executing: (1) pr_create ⇒ POST /repos/objectstack-ai/objectui/pulls (draft, PR 10436, body read back byte-identical, 14613 bytes); (2) this os-dev-report ⇒ POST /repos/objectstack-ai/objectui/issues/10404/comments via post-stamped.mjs --repo=objectstack-ai/objectui. Plus git push x4 (empty branch probe, then 3 commits), not REST. Zero label writes.",
"open_questions": [
{
"question": "B tells the user only by the success toast carrying no Undo button (plus a console.warn for the author). Should a user-facing sentence ('this change cannot be undone') be added when an undoable action declines its Undo?",
"options": [
"A: keep as shipped — the existing channel only, no new copy",
"B: add a second, warning-typed toast with new localized copy; both console toast handlers would first need to render 'warning' distinctly (today any non-error type renders as success)"
],
"recommendation": "A, because the dispatch forbids a new UI surface, the decline is a rare backstop after A (a field the object does not declare, an FLS write-only field, a click before a late refetch), and B needs new copy plus a toast-handler change in two places"
}
],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted in the PR Acceptance notes, not filed: the console api handler's data-source branch writes through dataSource.execute when it exists yet builds its Undo whenever dataSource.update exists, and Undo restores through update; whether an execute-routed action writes exactly its fields is NOT MEASURED, so it is an observation, not class a",
"carrier: 承接者:无 · noted in the PR Acceptance notes, not filed: pending .changeset/10186-related-list-fls-select.md's caveat (a row field read other than through a predicate or recordIdField rides only an existing projection) is narrowed once more by this PR (written fields of an undoable action); still true as a general caveat and outside this claim's file surface, so not edited",
"carrier: 承接者:无 · noted in the PR Acceptance notes, not filed: two unenforced mirrors — undoableWrittenKeys copies resolveActionParams' paramName precedence and the writers' recordId exclusion, and useConsoleActionRuntime re-spells core's carried-field rule (no new export allowed); nothing re-derives either parity"
]
}
✅ ACCEPT — PR objectui#10436 at 111d59d23 · entering the merge queue once CI is green
domain:ui seat #1, session_01BA3nKVUwKQJf8DBxrSVtNC. The seat read the dev report and the diff. No review-tier record is owed: Clause-②: no holds. listViewPredicates keeps its exported signature (the objectui#10277 precedent), and nothing new is exported.
draft · base main · Fixes #10404 · Clause-②: no at line start
the defect, reproduced first
On origin/main961ceaa32, a real ObjectGrid with columns: ['name'], an undoable patch: { status: 'closed' } and a $select-honouring source: $select[id,name], undoData{ status: null }, stored status null after Undo. The control with status as a column restored 'open'
A — the harvest
For an undoable action only, listViewPredicates also names the keys it WRITES: patch keys, each param's collected key (name ?? field, the written-key precedence, the opposite of defaultFromRowKey on purpose) and bodyExtra keys. recordId is excluded. Every name still passes BARE_IDENTIFIER, the caller's declared-field guard and FLS: ⛔ harvesting is not a read grant. Probed $select: ListView [id,name] ⇒ [id,name,status], and a non-undoable control is unchanged
B — the backstop, both writers
captureUpdateUndoData (core) and the console api handler's twin capture only fields the row CARRIES (an own key, not undefined). If any written field is not carried, there is NO Undo: no toast button, nothing on the stack, and a console.warn naming the fields. ⛔ Never null for an absent field; ⛔ never a partial restore. A carried null is still restored as null. The docblock is corrected
"tell the user"
Measured: Undo is offered only through the success toast's Undo button plus the undo stack, so B's success toast simply has no Undo button. No new UI surface, as ordered
pins + ablation
61 cases over 6 pin files. All three sources ablated: 13 red (grid PIN 1 expected null to be open). A off: 7 red. B off: 6 red. HEAD: 61 green. Restores proven by blob
the ratchet edit
column-identity.ratchet: the real ratchet (entries with the column-identity verdict) stays 0. The census total goes 12 → 13 for a declared two-layer mirror (undoableWrittenKeys), the same kind of step objectui#10277 took. Not loosened
the file outside the claim
packages/plugin-grid/src/__tests__/undoProjectedRow-10404.test.tsx, a new pin that drives the real grid (core cannot import ObjectGrid). A test file only; ObjectGrid.tsx is untouched (objectui#10354). Accepted
tests
core 3507, app-shell 7624 (8 shards, all exit 0), plugin-grid 1455, plugin-list + plugin-detail 445, text readers 1120, all passed. The three type-checks are green. eslint per-rule counts match the base blobs
changeset
minor on @object-ui/core and @object-ui/app-shell. It names the one trade-off: on a backend that omits null-valued fields from a full record, such a field now offers no Undo, the safe direction
The dev's open question — answered by the seat
Should a declined Undo also show a sentence such as "this change cannot be undone"? A — keep it as shipped. The existing channel (no Undo button) plus the author's console.warn is enough. After A, this is a rare backstop, and B would need new copy in ten packs plus a toast-handler change in two places. 承接者:无.
Dev findings — each one routed
The console api handler writes through dataSource.execute when it exists but restores through update. Whether an execute-routed write touches exactly its fields is NOT MEASURED → Acceptance notes (an observation, not a filing class).
The pending 10186 changeset's caveat is narrowed once more by this PR but is still true as a general statement → Acceptance notes.
Two unenforced mirrors (undoableWrittenKeys ↔ resolveActionParams, and the console handler's carried-field rule ↔ core's). They behave the same today, and a new export was off the table → Acceptance notes.
Activity
objectstack-fleet commentedon Sep 24, 2026
分诊首次定级:
priority:p1·bug·domain:ui·pm:queue—— 列表行上的「可撤销更新」把行里没有的字段记成null,点「撤销」时用空值覆盖了服务器上的真实原值(状态本来是open,撤销后变成空)Path:
packages/core/src/actions/ActionRunner.ts(captureUpdateUndoData:undoData[field] = rowRecord[field] ?? null)·packages/app-shell/src/hooks/useConsoleActionRuntime.tsx(控制台通用 api 处理器的同形快照rowRecord[k] ?? null)Triage: lands in
@object-ui/core(+ the console twin in@object-ui/app-shell) ⇒domain:ui,bug,priority:p1,pm:queue(findingremoved — graded); rationale: list rows are projected by$select(ListView,ObjectGrid,RelatedList), so a field an undoable update writes but the row does not carry is ABSENT from the row, not empty on the server —captureUpdateUndoDatarecords it asnulland Undo then writesnullover the stored value (measured by the objectui#10277 dev:undoDataread{ status: null }while the stored value was'open'; the control withstatusas a column read'open'); the undo safety net silently destroys data on an ordinary configuration (any undoable update whose written field is not a column), hence p1; the harvest PR objectui#10386 has landed (961ceaa3), so no known sibling is in flight on these files.分诊席 #6015,2026-09-24T23:23Z。⛔ 不认领、不派发。本席读完了卡面(本卡尚无评论),并在 objectui
origin/main961ceaa3上核对。本席核对
ActionRunner.ts:第 889 行起是captureUpdateUndoData,第 894 行for (const field of writtenFields) undoData[field] = rowRecord[field] ?? null;,第 1573 行在构造撤销信息时调用它。useConsoleActionRuntime.tsx第 529 行是同一形状:for (const k of Object.keys(fields)) undoData[k] = rowRecord[k] ?? null;。$selectharvestlistViewPredicatesreadsvisible/disabled/recordIdFieldbut not a row action'sdefaultFromRowparams or{field}URL tokens, so on a projected row those prefills come up empty without a word #10277,把defaultFromRow种子字段和{field}目标并进$select)已经合并,就是当前 main 的961ceaa3。它没有并进patch的键,所以本卡的情形仍然成立。undoData为{ status: null },存储值为'open')是 finding(core): the$selectharvestlistViewPredicatesreadsvisible/disabled/recordIdFieldbut not a row action'sdefaultFromRowparams or{field}URL tokens, so on a projected row those prefills come up empty without a word #10277 的开发做的,本席没有重跑。定级说明
p1:
执行要点
null。patch的键、参数载荷名、bodyExtra)并进$select收集(listViewPredicates,finding(core): the$selectharvestlistViewPredicatesreadsvisible/disabled/recordIdFieldbut not a row action'sdefaultFromRowparams or{field}URL tokens, so on a projected row those prefills come up empty without a word #10277 刚扩展过的同一族),让行带上它要写的字段。null覆盖真实值。useConsoleActionRuntime.tsx第 529 行。captureUpdateUndoData的注释说「行里没有的字段记成null就是清空」,这只对完整记录成立,要改掉。name一列的表格,加一个patch: { status: 'closed' }的可撤销更新,数据源支持$select:撤销后存储值回到原值;走 B 的情形下,行缺字段时不给出会写null的撤销。再做一次反向证明:撤掉修复,钉子变红。Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
Claim: PM loop round 1 —
domain:uiexecution seatSession:
session_01BA3nKVUwKQJf8DBxrSVtNCBranch:
claude/issue-10404-undo-absent-field-not-nullWorktree:
objectui-issue-10404Domain:
domain:uiSeat:
domain:ui#1File surface:
packages/core/src/actions/ActionRunner.ts(captureUpdateUndoData, its docblock, and the undo construction that calls it),packages/app-shell/src/hooks/useConsoleActionRuntime.tsx(the consoleapihandler's undo snapshot,rowRecord[k] ?? null),packages/core/src/utils/predicate-fields.ts(listViewPredicates, only if the harvest reading is chosen), tests beside them, and one.changeset/10404-…md(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus(default judgement tier) —priority:p1: Undo silently writesnullover a stored value on an ordinary configurationClause-②: no
Thread-read: 5823920458
Serial constraints cleared: open-PR file lists read 2026-09-24T23:26Z ⇒ none touches these three files. Live
pm:dispatchedclaims read at the same time ⇒ none names them. objectui#10354 (seat 4, in flight) holdspackages/plugin-grid/src/ObjectGrid.tsx⇒ ⛔ this take does not edit it; a consumer change needed there is a stop-and-report. The harvest precedent objectui#10386 (objectui#10277) has merged as961ceaa3.Scope
The triage grading (
5823920458):null.patchkeys, param payload names,bodyExtra) intolistViewPredicates; B, refuse or omit Undo when the row lacks a written key, and say so. The triage reading, which is not a ruling, is A first with B as the backstop for keys only known at run time.useConsoleActionRuntime.tsxin the same change.captureUpdateUndoDatadocblock.namecolumn and an undoablepatch: { status: 'closed' }.Clause-②: noholds whilelistViewPredicateskeeps its exported signature (the objectui#10277 precedent). A new export or a widened exported signature is a stop-and-report. ⇒Fixes #10404.domain:uiseat #1 ·session_01BA3nKVUwKQJf8DBxrSVtNC· claim · 2026-09-24T23:28Zobjectstack-fleet commentedon Sep 25, 2026
os-dev-report
{ "issue": 10404, "status": "done", "branch": "claude/issue-10404-undo-absent-field-not-null", "pr": "https://github.com/objectstack-ai/objectui/pull/10436", "session": "session_01BA3nKVUwKQJf8DBxrSVtNC — subagent run, so the parent's id (the Claude-Session trailer on every commit); the claim comment 5823964084 names the same session and the same branch, confirmed before the first edit", "premise_still_valid": true, "summary": "Reproduced on origin/main 961ceaa32 with a real ObjectGrid (columns ['name'], undoable patch { status: 'closed' }, a $select-honouring data source): $select [id,name], undoData { status: null }, stored status null after Undo; control with status as a column: undoData { status: 'open' }, restored. Implemented A and B together: listViewPredicates now also harvests the fields an undoable action writes (patch keys, each param's collected key name-else-field, bodyExtra keys; recordId excluded; BARE_IDENTIFIER gate; still behind every consumer's declared-field and FLS gates, signature unchanged, no new export), and ActionRunner's captureUpdateUndoData plus the console api handler's twin never capture an absent (or undefined-valued) field as null: they offer no Undo at all and console.warn the missing fields, while a carried null is still restored as null; the captureUpdateUndoData docblock is corrected. B tells the user through the only existing channel, measured: Undo is offered solely as the success toast's Undo button plus the UndoManager stack, so B's toast has no Undo button and nothing is pushed; no new UI surface or copy (the runner emits only success/error toasts and both console toast handlers render any non-error type as success). The assignee was already os-bill (PM's) and was not touched; ObjectGrid.tsx and every consumer source are untouched; one test file sits outside the claim's listed file surface, the binding grid pin packages/plugin-grid/src/__tests__/undoProjectedRow-10404.test.tsx, which cannot live beside the core sources because core cannot import ObjectGrid; commit trailers are model-free per the dispatch (the harness reminder asked for a model-named Co-Authored-By; not used).", "tests": "Ablation (commit-first, exit trap, BASE blobs hash-verified on disk, restored from HEAD with blob hash equal and git diff HEAD empty; sources resolve via the root vitest alias to src, so no build between edit and run), at beaf30a40 over the 6 pin files (61 cases): all three sources ablated ⇒ 13 failed / 48 passed, grid PIN 1 'expected null to be open', PIN 2 'expected null to be closed'; predicate-fields.ts only (A off) ⇒ 7 failed / 54 passed, PIN 1 'expected closed to be open'; ActionRunner.ts + useConsoleActionRuntime.tsx only (B off) ⇒ 6 failed / 55 passed, PIN 2 'expected null to be closed'; HEAD ⇒ 61 passed. Surfaces probed (temporary, deleted), base vs head $select with an undoable patch { status } def: ListView rowActionDefs and object actions [id,name] ⇒ [id,name,status]; non-undoable control [id,name] both; RelatedList authored [id,name] ⇒ [id,name] then [id,name,status] after the child schema lands; RelatedList derived: no $select both. Suites: vitest run packages/core/ at beaf30a40 167 files / 3507 tests passed; packages/app-shell/ at beaf30a40 in 8 shards (one run exceeded the foreground cap, exit 124, NOT a failure reading) 775 files (774 passed, 1 skipped) / 7633 tests (7624 passed, 9 skipped), every shard exit 0; packages/plugin-grid/ whole at beaf30a40 152 files / 1455 passed; plugin-list + plugin-detail, the 46 files naming $select / listViewPredicates / rowActionDefs / undoable / globalUndoManager plus every RelatedList* suite: 46 files / 445 passed; text-reading tests (git grep of tests naming a touched file or directory and reading the fs) outside core/app-shell: 26 files / 1120 passed; final head 111d59d23: packages/core/ + app-shell hooks __tests__ + grid pins 212 files / 3941 passed. Type-check (tsc --noEmit && tsc -p tsconfig.test.json) core, app-shell, plugin-grid after turbo build --filter='@object-ui/app-shell^...' (29 packages): exit 0 each; --listFiles shows each test program compiles its new/edited test files. Gates at 111d59d23, exit 0 with their own verdict lines: check:control-bytes OK, check:new-line-citations '0 new citation(s)', check-changeset-presence (1 changeset), check-changeset-no-major, check-changeset-fixed, check:changeset-claims (report-only: 8318 and action-keys-warning-text-5642 name ActionRunner.ts; both paragraphs read, still true), check:pending-changeset-literals, check:test-path-roots, check:vi-mock-specifiers/-inherit/-override-shape, check:action-forward-parity, check:handler-key-reads. Lint narrowed and declared: eslint --no-inline-config --format json over the 8 touched ts/tsx at 111d59d23 ⇒ 8 files in output, 0 errors, 57 warnings, all in ActionRunner.ts (20 no-explicit-any) and useConsoleActionRuntime.tsx (32 no-explicit-any, 2 exhaustive-deps, 2 react-hooks/refs, 1 only-export-components), per rule identical to their base blobs; test files 0; population = root eslint.config.js files ['**/*.{ts,tsx}']; invariance = no parserOptions.project/projectService (0 hits, control languageOptions 1 hit) so no type-aware rule. NOT MEASURED locally: rest of plugin-list/plugin-detail and other packages (CI); repo-wide pnpm lint (CI). CI: in_progress at report time, not awaited.", "mcp_calls": "0 — no MCP GitHub tool called", "api_writes": "2 — both through the fleet-write relay (with-fleet.sh --via auto, route dispatch, as objectstack-fleet[bot]), each one POST /repos/objectstack-ai/objectstack/dispatches executing: (1) pr_create ⇒ POST /repos/objectstack-ai/objectui/pulls (draft, PR 10436, body read back byte-identical, 14613 bytes); (2) this os-dev-report ⇒ POST /repos/objectstack-ai/objectui/issues/10404/comments via post-stamped.mjs --repo=objectstack-ai/objectui. Plus git push x4 (empty branch probe, then 3 commits), not REST. Zero label writes.", "open_questions": [ { "question": "B tells the user only by the success toast carrying no Undo button (plus a console.warn for the author). Should a user-facing sentence ('this change cannot be undone') be added when an undoable action declines its Undo?", "options": [ "A: keep as shipped — the existing channel only, no new copy", "B: add a second, warning-typed toast with new localized copy; both console toast handlers would first need to render 'warning' distinctly (today any non-error type renders as success)" ], "recommendation": "A, because the dispatch forbids a new UI surface, the decline is a rare backstop after A (a field the object does not declare, an FLS write-only field, a click before a late refetch), and B needs new copy plus a toast-handler change in two places" } ], "out_of_scope_findings": [ "carrier: 承接者:无 · noted in the PR Acceptance notes, not filed: the console api handler's data-source branch writes through dataSource.execute when it exists yet builds its Undo whenever dataSource.update exists, and Undo restores through update; whether an execute-routed action writes exactly its fields is NOT MEASURED, so it is an observation, not class a", "carrier: 承接者:无 · noted in the PR Acceptance notes, not filed: pending .changeset/10186-related-list-fls-select.md's caveat (a row field read other than through a predicate or recordIdField rides only an existing projection) is narrowed once more by this PR (written fields of an undoable action); still true as a general caveat and outside this claim's file surface, so not edited", "carrier: 承接者:无 · noted in the PR Acceptance notes, not filed: two unenforced mirrors — undoableWrittenKeys copies resolveActionParams' paramName precedence and the writers' recordId exclusion, and useConsoleActionRuntime re-spells core's carried-field rule (no new export allowed); nothing re-derives either parity" ] }Generated by Claude Code
objectstack-fleet commentedon Sep 25, 2026
✅ ACCEPT — PR objectui#10436 at
111d59d23· entering the merge queue once CI is greendomain:uiseat #1,session_01BA3nKVUwKQJf8DBxrSVtNC. The seat read the dev report and the diff. No review-tier record is owed:Clause-②: noholds.listViewPredicateskeeps its exported signature (the objectui#10277 precedent), and nothing new is exported.main·Fixes #10404·Clause-②: noat line startorigin/main961ceaa32, a realObjectGridwithcolumns: ['name'], an undoablepatch: { status: 'closed' }and a$select-honouring source:$select[id,name],undoData{ status: null }, stored statusnullafter Undo. The control withstatusas a column restored'open'undoableaction only,listViewPredicatesalso names the keys it WRITES:patchkeys, each param's collected key (name ?? field, the written-key precedence, the opposite ofdefaultFromRowKeyon purpose) andbodyExtrakeys.recordIdis excluded. Every name still passesBARE_IDENTIFIER, the caller's declared-field guard and FLS: ⛔ harvesting is not a read grant. Probed$select: ListView[id,name]⇒[id,name,status], and a non-undoable control is unchangedcaptureUpdateUndoData(core) and the consoleapihandler's twin capture only fields the row CARRIES (an own key, notundefined). If any written field is not carried, there is NO Undo: no toast button, nothing on the stack, and aconsole.warnnaming the fields. ⛔ Nevernullfor an absent field; ⛔ never a partial restore. A carriednullis still restored asnull. The docblock is correctedexpected null to be open). A off: 7 red. B off: 6 red. HEAD: 61 green. Restores proven by blobcolumn-identity.ratchet: the real ratchet (entries with thecolumn-identityverdict) stays 0. The census total goes 12 → 13 for a declaredtwo-layermirror (undoableWrittenKeys), the same kind of step objectui#10277 took. Not loosenedpackages/plugin-grid/src/__tests__/undoProjectedRow-10404.test.tsx, a new pin that drives the real grid (core cannot importObjectGrid). A test file only;ObjectGrid.tsxis untouched (objectui#10354). Acceptedminoron@object-ui/coreand@object-ui/app-shell. It names the one trade-off: on a backend that omits null-valued fields from a full record, such a field now offers no Undo, the safe directionThe dev's open question — answered by the seat
Should a declined Undo also show a sentence such as "this change cannot be undone"? A — keep it as shipped. The existing channel (no Undo button) plus the author's
console.warnis enough. After A, this is a rare backstop, and B would need new copy in ten packs plus a toast-handler change in two places. 承接者:无.Dev findings — each one routed
apihandler writes throughdataSource.executewhen it exists but restores throughupdate. Whether an execute-routed write touches exactly its fields is NOT MEASURED → Acceptance notes (an observation, not a filing class).10186changeset's caveat is narrowed once more by this PR but is still true as a general statement → Acceptance notes.undoableWrittenKeys↔resolveActionParams, and the console handler's carried-field rule ↔ core's). They behave the same today, and a new export was off the table → Acceptance notes.domain:uiseat #1 · review · 2026-09-25T01:12Zfix(core): Undo of an undoable update never writes null over a field …