Repository navigation
finding(core,app-shell,plugin-grid): Undo of an undoable update that writes a lookup field sends the EXPANDED related record back as the field's value, not the stored id, because the snapshot is taken from a $expanded row #11122
Description
Activity
objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsPath: records — Undo restores the value the record had | 缺项 (the Undo snapshot of a written lookup is taken from a
$expanded row, so Undo writes the related record object into the reference slot instead of its id) | P1Triage: first grade —
bug·priority:p1·domain:ui·area:records·pm:queue. Direction: normalise at the one capture rule, from the object's field map, so all three surfaces are fixed togetherTriage: lands in
@object-ui/core'scaptureUpdateUndoData(the one capture rule the runner, the console runtime and the record page share since PR objectui#11121) ⇒domain:ui.Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-29T17:54Z. ⛔ Not a claim, ⛔ not a dispatch.Why p1. NORTH-STAR rule 1, data integrity.
- Under the server's default lenient value-shape posture, the Undo write is admitted, and the lookup slot then stores an object. That is silent corruption, of exactly the kind
os migrate value-shapesexists to find. - Under the strict posture it is refused, and Undo fails loudly. That is the milder case.
- The reach is taught. objectstack's published skill example
ReassignLeadActionis an undoableapiaction that writes a lookup. On the grid, any written lookup that is also a visible or grouping column is captured the same way.
Direction.
captureUpdateUndoData, or one sibling that all three callers run first, captures a reference-bearing written field's stored id when the row holds an expanded value (the value's id, through the adapter's id key).- "Reference-bearing" is read from the object's field map (
isExpandableFieldType). ⛔ Never a shape heuristic: ajsonfield may legitimately hold an object with anid. - ⛔ No per-surface fix: the rule is shared precisely so that one change covers the runner, the console runtime and the record page.
- Pins:
- On the record page and on a grid row, Undo of a lookup update restores the id (
'a1'). - A
jsonfield holding{ id, … }restores the object unchanged. - The objectui#10404 / objectui#11082 pins stay green.
- On the record page and on a grid row, Undo of a lookup update restores the id (
- Coordinate: objectui#11081 (p1, the keyboard listener on the same undo path) is a different file. Whichever lands second rebases.
- Under the server's default lenient value-shape posture, the Undo write is admitted, and the lookup slot then stores an object. That is silent corruption, of exactly the kind
- addedarea:recordsBusiness objects, records, the views that show data, usable forms, searchBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seatand removed
on Sep 29, 2026 objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsClaim: PM loop round 4 —
domain:uiexecution seat 2
Session:session_011p7ikEivgXefNDaE5S5Uec
Account:os-support-ai(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-11122-undo-lookup-id
Worktree:objectui-issue-11122
Domain:domain:ui
Seat:domain:ui#2
File surface (triage5895671250: normalise at the one capture rule, from the object's field map):packages/core/src/actions/ActionRunner.ts:captureUpdateUndoData(:957), or one sibling that all three callers run first, captures a reference-bearing written field's stored id when the row holds an expanded value. The runner's own call is at:1674.- "Reference-bearing" is read from the object's field map through
isExpandableFieldType(packages/core/src/utils/expand-fields.ts:202). ⛔ Never a shape heuristic. - The two other callers, which pass the field map in:
packages/app-shell/src/hooks/useConsoleActionRuntime.tsx(:543,:556) andpackages/app-shell/src/views/RecordDetailView.tsx(:989,:1002). - Pins:
- Undo of a lookup update restores the id on the record page and on a grid row.
- A
jsonfield holding{ id, … }restores the object unchanged. - The objectui#10404 / objectui#11082 pins stay green.
- One changeset.
Stop on breach; explain in the report.
Container & model:M,mode:subagent,model: opus(TIER_DEFAULT;dispatch-gates --tier --repo objectstack-ai/objectuiover this surface: no path-derived mandate, at objectstack288611e3)
Clause-②: no
Why no: an Undo snapshot's value changes from an expanded object to the stored id.captureUpdateUndoDatamay gain a field-map parameter, which widens nothing a caller can author and moves no schema, prop or accepted set.
Thread-read: 5895671250
Serial constraints cleared: read 2026-09-29T18:05Z at objectuiorigin/main8a5ae3d63. PR objectui#11121 (objectui#11082, this rule's extraction) merged as76e9df06c. No open PR touchesActionRunner.ts,expand-fields.ts,useConsoleActionRuntime.tsxorRecordDetailView.tsx. PR objectui#11086 touchescore/src/actions/actionKeys.tsonly, which is disjoint. objectui#11080 (queued,AppContentundo toasts) is disjoint by file.
Generated by Claude Code
objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 11122, "status": "done", "branch": "claude/issue-11122-undo-lookup-id", "pr": "https://github.com/objectstack-ai/objectui/pull/11137", "session": "session_011p7ikEivgXefNDaE5S5Uec — subagent run; the id is the parent PM session's", "premise_still_valid": true, "summary": "captureUpdateUndoData(writtenFields, rowRecord, fields) now takes the written object's field definitions as a REQUIRED third argument. After the unchanged carry rule, it reads each value through the existing schema-driven toPredicateRecord, so a declared relation (EXPANDABLE_FIELD_TYPES) is captured as its stored id, and a multiple one as its array of ids. A json field holding { id, ... } is captured exactly as the row carries it. No shape heuristic is used; undefined fields collapse nothing. All three writers now pass a field map. The console runtime's api handler looks the written object up in objects, then in the metadata store after ensureType('object'). RecordDetailView passes objectDef.fields. ActionRunner's operation:'update' reads objectFields from the action context, which the console runtime and the record page now publish beside objectName; the runner uses it only when the captured object equals that objectName. Premise verified on main 8a5ae3d63: the card's probe is red at base. Measured: objectql expandRelatedRecords joins and injects on rec.id; multiple relations expand element-wise, with unloaded members left as ids; spec ReferenceIdValueSchema stores a non-empty string; before this change the runner had no field map at all. Dispatch zone 2 item 1 calls the runner's call 'the runner's own api path'; it is the operation:'update' path (executeUpdateOperation). The harness attribution reminder asked for a model-named Co-Authored-By trailer and a different PR footer. I followed the role file and AGENTS.md instead: the model-free trailer pair and the session-URL footer.", "tests": "All at head 728dd4ca3, the final commit, under os-verify-lock (slot issue-11122). (1) The 8 undo pin files (11122, 11082, 10404 in core, app-shell and plugin-grid): Test Files 8 passed (8), Tests 40 passed (40). (2) vitest run packages/core/: Test Files 188 passed (188), Tests 3700 passed, 27 skipped (3727). (3) The narrowed app-shell set: 134 files, enumerated by git grep as every app-shell test importing RecordDetailView, useConsoleActionRuntime, ConsoleActionRuntimeProvider, DeclaredActionsBar, ObjectView, PageView or ConsoleShell. Result: Test Files 134 passed (134), Tests 1152 passed (1152). (4) turbo run build --filter=@object-ui/app-shell^... --concurrency=2: 28 successful, 28 total. (5) pnpm --filter @object-ui/core type-check and pnpm --filter @object-ui/app-shell type-check, each tsc --noEmit plus tsconfig.test.json: exit 0 each. tsc --listFilesOnly shows both touched core tests and both new app-shell tests in the test programs. REVERSE VERIFICATION: the fix and the pins were committed first (bed1b655d, 728dd4ca3). The three implementation files were checked out at BASE 8a5ae3d63 with the pins at HEAD, under trap restore. Mutation proven on disk: readContextObjectFields 0 in ActionRunner.ts, objectFields 0 in each app-shell file, base capture body 1. Result: Tests 12 failed, 8 passed (20), with the predicted direction. Red: every relation case (lookup, all four types, multiple, mixed, either shape, runner) and both host-publication cases. Green: the json controls, unexpanded and null, absent, no field map, row not mutated, retargeted object. Restore proven: git diff HEAD empty, and each disk blob hash equals its HEAD blob (cdf07f25d, c04887c4a, b0f5a4577). No dist rebuild was needed: the root vitest alias maps @object-ui/core to src. The README snippet was compiled with strict tsc against the built core d.ts: exit 0. Control: a two-argument call is refused with TS2554.", "gates": [ "0 turbo build app-shell^... (28/28)", "0 pnpm --filter @object-ui/core type-check", "0 pnpm --filter @object-ui/app-shell type-check", "0 pnpm exec eslint on 7 changed source/test files (0 errors, 168 pre-existing warnings)", "0 node scripts/check-changeset-presence.mjs", "0 node scripts/check-control-bytes.mjs", "0 pnpm check:new-line-citations", "0 pnpm check:changeset-claims", "0 pnpm check:pending-changeset-literals", "0 pnpm check:vi-mock-specifiers", "0 pnpm check:vi-mock-inherit", "0 pnpm check:vi-mock-override-shape", "0 pnpm check:test-path-roots", "0 pnpm check:handler-key-reads", "0 pnpm check:doc-fences", "NOT MEASURED: check:doc-snippets. Reason: exit 2, PRECONDITION NOT MET; its build filter is about 34 packages and it cannot be scoped to one document. Declared to CI. Narrowed stand-in: the README snippet tsc above.", "NOT MEASURED: check:readme-exports. Reason: exit 1, the population collapsed on an unbuilt tree, so no verdict. Declared to CI. The diff adds no README import binding.", "NOT MEASURED: the full packages/app-shell/ suite. Reason: 884 files; I stopped my own run (exit 143) after 19m42s holding the shared lock, with two seats queued. Declared to CI. Narrowed set: 134 files above." ], "files_changed": [ ".changeset/11122-undo-lookup-id.md", "packages/core/src/actions/ActionRunner.ts", "packages/core/README.md", "packages/core/src/actions/__tests__/captureUpdateUndoData.export-11082.test.ts", "packages/core/src/actions/__tests__/captureUpdateUndoData.relations-11122.test.ts", "packages/app-shell/src/hooks/useConsoleActionRuntime.tsx", "packages/app-shell/src/views/RecordDetailView.tsx", "packages/app-shell/src/views/RecordDetailView.undoLookupId-11122.test.tsx", "packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime.undoLookupId-11122.test.tsx" ], "line_budget": "n/a: the diff touches no skills/** and no governed ledger (917 added, 21 deleted, 9 files)", "deviations": [ "PR assignee NOT set. The command node /home/user/objectstack/scripts/pm/label-write.mjs --repo objectstack-ai/objectui --issue 11137 --assign os-support-ai was refused by the auto-mode classifier ([External System Writes]) before sending. Not retried by another route; the seat to set it. The PR currently has no assignee; its four labels were applied by the repo labeler, not by me.", "File surface grew by packages/core/README.md: its captureUpdateUndoData example used the two-argument form, and it now documents the field map.", "The objectui#11082 export pin (captureUpdateUndoData.export-11082.test.ts) had its six direct calls given a third argument (a FIELDS map with no expanded value), because the parameter is required. Its assertions are unchanged and green.", "The narrowed app-shell run replaced the full suite: see gates.", "Clause-② line copied as 'no' from the claim. Beyond the claim's reasoning, the runner now reads a new context key, objectFields. It is undeclared on ActionContext and rides the existing index signature, as objectName does, so no .d.ts member moves. It is still new runtime input; the seat may re-judge. The changeset states the same." ], "mcp_calls": "0", "api_writes": "2 — (1) POST /repos/objectstack-ai/objectstack/dispatches (fleet-write relay request fw-20260929T195305Z-a47eab; its run 36622391413 executed POST /repos/objectstack-ai/objectui/pulls, creating draft PR #11137); (2) this os-dev-report comment on objectui#11122 via scripts/pm/post-stamped.mjs. The refused assignee write was never sent. git push is not a REST write.", "open_questions": [], "out_of_scope_findings": [ "carrier: 承接者:无 · noted, not filed. Same family as objectui#11122 (closed by this PR); the seat decides whether a follow-up is owed. The runner's operation:'update' capture has no field map in three places, so it keeps the old verbatim capture there: an action that retargets another object (related-list child rows through the record page's runner), a runner under PageView's runtime (no objectName bound), and an embedder's bare ActionProvider that publishes no objectFields. Each would need the target object's field definitions published by, or resolvable from, its host. Not measured with a relation column on screen. Dedupe words: undo expanded lookup related list; objectFields action context; retargeted undoable update", "carrier: 承接者:无 · noted, not filed. Read, not measured: the spec's FILE_REFERENCE_TYPES (file, image, avatar, video, audio) are resolved in place on read to { id, name, size, mimeType, url } (objectql, ADR-0104 D3 wave 2). An undoable update writing one would capture that object. These types are outside EXPANDABLE_FIELD_TYPES by design. Dedupe words: undo file field resolved; FileValueSchema undo snapshot", "carrier: 承接者:无 · noted, not filed. Read, not measured: other readers of _rowRecord on an expanded row read a relation as the record: the api handler's {field} target tokens (String(value)), resolveRecordIdParamSeed with a relation recordIdField, and defaultFromRow param seeds. Dedupe words: rowRecord expanded lookup token; defaultFromRow expanded relation; recordIdField lookup seed" ] }objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 11122, "status": "done", "branch": "claude/issue-11122-undo-lookup-id", "pr": "https://github.com/objectstack-ai/objectui/pull/11137", "session": "session_011p7ikEivgXefNDaE5S5Uec — subagent run; the id is the parent PM session's", "premise_still_valid": true, "summary": "PATCH ROUND (contract review 5897840314, finding 1), new head 37ab67eb0. The README example's `fields` literal put `type: 'json'` where check:doc-types reads a component type, which turned Doc Component Type Check and Test shard 5/8 red. The fix is one commit, 37ab67eb0 (packages/core/README.md only). The example now declares its field map as `declare const fields: FieldContainerLike`, the parameter's own exported type, and says in prose which field is text, lookup or json. So no field-type literal sits under a `type:` key, and no DOC_TYPE_EXEMPTIONS entry was added. Code, pins, changeset and PR body are unchanged from 728dd4ca3. ORIGINAL ROUND: captureUpdateUndoData(writtenFields, rowRecord, fields) now takes the written object's field definitions as a REQUIRED third argument. After the unchanged carry rule, it reads each value through the existing schema-driven toPredicateRecord, so a declared relation (EXPANDABLE_FIELD_TYPES) is captured as its stored id, and a multiple one as its array of ids. A json field holding { id, ... } is captured exactly as the row carries it. No shape heuristic is used; undefined fields collapse nothing. All three writers now pass a field map. The console runtime's api handler looks the written object up in objects, then in the metadata store after ensureType('object'). RecordDetailView passes objectDef.fields. ActionRunner's operation:'update' reads objectFields from the action context, which the console runtime and the record page now publish beside objectName; the runner uses it only when the captured object equals that objectName. Premise verified on main 8a5ae3d63: the card's probe is red at base. Measured: objectql expandRelatedRecords joins and injects on rec.id; multiple relations expand element-wise, with unloaded members left as ids; spec ReferenceIdValueSchema stores a non-empty string; before this change the runner had no field map at all. Dispatch zone 2 item 1 calls the runner's call 'the runner's own api path'; it is the operation:'update' path (executeUpdateOperation). The harness attribution reminder asked for a model-named Co-Authored-By trailer and a different PR footer. I followed the role file and AGENTS.md instead: the model-free trailer pair and the session-URL footer.", "tests": "PATCH ROUND at 37ab67eb0. (a) pnpm check:doc-types: exit 0 at the committed head ('Every documented component type is registered'). Before the fix it was exit 1, flagging `type 'json'` in packages/core/README.md. (b) Under os-verify-lock, vitest run on scripts/__tests__/check-doc-component-types.test.ts plus the three core undo pins (relations-11122, export-11082, ActionRunner 10404): Test Files 4 passed (4), Tests 147 passed (147). (c) turbo build of @object-ui/core... (2/2), then the reshaped README snippet compiled with strict tsc against the rebuilt core d.ts: exit 0. Negative control: a two-argument call is refused with TS2554. (d) check:doc-fences, check:control-bytes, check:new-line-citations, check-changeset-presence: exit 0 each. ORIGINAL ROUND at 728dd4ca3 (code unchanged since): All at head 728dd4ca3, the final commit, under os-verify-lock (slot issue-11122). (1) The 8 undo pin files (11122, 11082, 10404 in core, app-shell and plugin-grid): Test Files 8 passed (8), Tests 40 passed (40). (2) vitest run packages/core/: Test Files 188 passed (188), Tests 3700 passed, 27 skipped (3727). (3) The narrowed app-shell set: 134 files, enumerated by git grep as every app-shell test importing RecordDetailView, useConsoleActionRuntime, ConsoleActionRuntimeProvider, DeclaredActionsBar, ObjectView, PageView or ConsoleShell. Result: Test Files 134 passed (134), Tests 1152 passed (1152). (4) turbo run build --filter=@object-ui/app-shell^... --concurrency=2: 28 successful, 28 total. (5) pnpm --filter @object-ui/core type-check and pnpm --filter @object-ui/app-shell type-check, each tsc --noEmit plus tsconfig.test.json: exit 0 each. tsc --listFilesOnly shows both touched core tests and both new app-shell tests in the test programs. REVERSE VERIFICATION: the fix and the pins were committed first (bed1b655d, 728dd4ca3). The three implementation files were checked out at BASE 8a5ae3d63 with the pins at HEAD, under trap restore. Mutation proven on disk: readContextObjectFields 0 in ActionRunner.ts, objectFields 0 in each app-shell file, base capture body 1. Result: Tests 12 failed, 8 passed (20), with the predicted direction. Red: every relation case (lookup, all four types, multiple, mixed, either shape, runner) and both host-publication cases. Green: the json controls, unexpanded and null, absent, no field map, row not mutated, retargeted object. Restore proven: git diff HEAD empty, and each disk blob hash equals its HEAD blob (cdf07f25d, c04887c4a, b0f5a4577). No dist rebuild was needed: the root vitest alias maps @object-ui/core to src. The README snippet was compiled with strict tsc against the built core d.ts: exit 0. Control: a two-argument call is refused with TS2554.", "gates": [ "0 pnpm check:doc-types (at 37ab67eb0; was 1 at 728dd4ca3)", "0 vitest scripts/__tests__/check-doc-component-types.test.ts plus core undo pins (4 files, 147 passed)", "0 README snippet strict tsc against the rebuilt core d.ts (control: TS2554 on the two-argument call)", "0 pnpm check:doc-fences", "0 node scripts/check-control-bytes.mjs", "0 pnpm check:new-line-citations", "0 node scripts/check-changeset-presence.mjs", "(728dd4ca3, code unchanged) 0 turbo build app-shell^... (28/28)", "(728dd4ca3, code unchanged) 0 pnpm --filter @object-ui/core type-check", "(728dd4ca3, code unchanged) 0 pnpm --filter @object-ui/app-shell type-check", "(728dd4ca3, code unchanged) 0 pnpm exec eslint on 7 changed source/test files (0 errors, 168 pre-existing warnings)", "(728dd4ca3, code unchanged) 0 node scripts/check-changeset-presence.mjs", "(728dd4ca3, code unchanged) 0 node scripts/check-control-bytes.mjs", "(728dd4ca3, code unchanged) 0 pnpm check:new-line-citations", "(728dd4ca3, code unchanged) 0 pnpm check:changeset-claims", "(728dd4ca3, code unchanged) 0 pnpm check:pending-changeset-literals", "(728dd4ca3, code unchanged) 0 pnpm check:vi-mock-specifiers", "(728dd4ca3, code unchanged) 0 pnpm check:vi-mock-inherit", "(728dd4ca3, code unchanged) 0 pnpm check:vi-mock-override-shape", "(728dd4ca3, code unchanged) 0 pnpm check:test-path-roots", "(728dd4ca3, code unchanged) 0 pnpm check:handler-key-reads", "(728dd4ca3, code unchanged) 0 pnpm check:doc-fences", "(728dd4ca3, code unchanged) NOT MEASURED: check:doc-snippets. Reason: exit 2, PRECONDITION NOT MET; its build filter is about 34 packages and it cannot be scoped to one document. Declared to CI. Narrowed stand-in: the README snippet tsc above.", "(728dd4ca3, code unchanged) NOT MEASURED: check:readme-exports. Reason: exit 1, the population collapsed on an unbuilt tree, so no verdict. Declared to CI. The diff adds no README import binding.", "(728dd4ca3, code unchanged) NOT MEASURED: the full packages/app-shell/ suite. Reason: 884 files; I stopped my own run (exit 143) after 19m42s holding the shared lock, with two seats queued. Declared to CI. Narrowed set: 134 files above." ], "files_changed": [ ".changeset/11122-undo-lookup-id.md", "packages/core/src/actions/ActionRunner.ts", "packages/core/README.md", "packages/core/src/actions/__tests__/captureUpdateUndoData.export-11082.test.ts", "packages/core/src/actions/__tests__/captureUpdateUndoData.relations-11122.test.ts", "packages/app-shell/src/hooks/useConsoleActionRuntime.tsx", "packages/app-shell/src/views/RecordDetailView.tsx", "packages/app-shell/src/views/RecordDetailView.undoLookupId-11122.test.tsx", "packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime.undoLookupId-11122.test.tsx" ], "line_budget": "n/a: the diff touches no skills/** and no governed ledger (917 added, 21 deleted, 9 files)", "deviations": [ "PR assignee NOT set. In the original round the classifier refused the label-write --assign os-support-ai ([External System Writes]). Per the seat's patch order it was not retried; the seat has left it for the maintainer.", "No DOC_TYPE_EXEMPTIONS entry: reshaping was possible, so the gate was not widened.", "File surface grew by packages/core/README.md: its captureUpdateUndoData example used the two-argument form, and it now documents the field map.", "The objectui#11082 export pin (captureUpdateUndoData.export-11082.test.ts) had its six direct calls given a third argument (a FIELDS map with no expanded value), because the parameter is required. Its assertions are unchanged and green.", "The narrowed app-shell run replaced the full suite: see gates.", "Clause-② line copied as 'no' from the claim. Beyond the claim's reasoning, the runner now reads a new context key, objectFields. It is undeclared on ActionContext and rides the existing index signature, as objectName does, so no .d.ts member moves. It is still new runtime input; the seat may re-judge. The changeset states the same." ], "mcp_calls": "0", "api_writes": "Patch round: 1, this os-dev-report comment on objectui#11122 via scripts/pm/post-stamped.mjs. git push of 37ab67eb0 is not a REST write. The PR body was not PATCHed. Original round: 2 — (1) POST /repos/objectstack-ai/objectstack/dispatches (fleet-write relay request fw-20260929T195305Z-a47eab; its run 36622391413 executed POST /repos/objectstack-ai/objectui/pulls, creating draft PR #11137); (2) this os-dev-report comment on objectui#11122 via scripts/pm/post-stamped.mjs. The refused assignee write was never sent. git push is not a REST write.", "open_questions": [], "out_of_scope_findings": [ "carrier: 承接者:无 · noted, not filed. Same family as objectui#11122 (closed by this PR); the seat decides whether a follow-up is owed. The runner's operation:'update' capture has no field map in three places, so it keeps the old verbatim capture there: an action that retargets another object (related-list child rows through the record page's runner), a runner under PageView's runtime (no objectName bound), and an embedder's bare ActionProvider that publishes no objectFields. Each would need the target object's field definitions published by, or resolvable from, its host. Not measured with a relation column on screen. Dedupe words: undo expanded lookup related list; objectFields action context; retargeted undoable update", "carrier: 承接者:无 · noted, not filed. Read, not measured: the spec's FILE_REFERENCE_TYPES (file, image, avatar, video, audio) are resolved in place on read to { id, name, size, mimeType, url } (objectql, ADR-0104 D3 wave 2). An undoable update writing one would capture that object. These types are outside EXPANDABLE_FIELD_TYPES by design. Dedupe words: undo file field resolved; FileValueSchema undo snapshot", "carrier: 承接者:无 · noted, not filed. Read, not measured: other readers of _rowRecord on an expanded row read a relation as the record: the api handler's {field} target tokens (String(value)), resolveRecordIdParamSeed with a relation recordIdField, and defaultFromRow param seeds. Dedupe words: rowRecord expanded lookup token; defaultFromRow expanded relation; recordIdField lookup seed" ] }objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actions✅ ACCEPT: PR objectui#11137 at
37ab67eb0; ready and auto-merge follow this ACCEPTdomain:uiseat 2,session_011p7ikEivgXefNDaE5S5Uec. Checked against the diff and the head's check-runs, not against the report's own account.- Claim
5895849357, on triage5895671250. - Dev reports
5897588256(round 1) and5897941829(the patch round). - Contract review round 1
5897840314, FAIL: one finding, the README snippet'stype: 'json', which turned three runs red. - Contract review round 2
5898199841, atCONTRACT_REVIEW_TIER: PASS on this head. Round 1's other judgments stand.
Implemented-by: claude/issue-11122-undo-lookup-id Reviewed-by: session_011p7ikEivgXefNDaE5S5Uecitem reading the rule captureUpdateUndoData(writtenFields, rowRecord, fields)reads each carried value through the schema-driventoPredicateRecord. A declared relation (EXPANDABLE_FIELD_TYPES) is captured as its stored id, and a multiple one as its array of ids. Ajsonfield holding{ id, … }is captured as the row carries it. The field map decides, never a shape heuristic. The carry rule (objectui#10404 / objectui#11082) is unchangedthe three writers The console runtime's apihandler looks the object up inobjects, then in the metadata store.RecordDetailViewpassesobjectDef.fields. The runner readsobjectFieldsfrom the action context, which the console runtime and the record page now publish besideobjectName, and uses it only when the captured object equals thatobjectNamethe round-2 fix README-only. The snippet declares const fields: FieldContainerLike, the parameter's own exported type, and names the field kinds in prose. No field-type literal sits under atype:key, and ⛔ the doc-type gate was not widenedtests Pins on the record page, the console runtime and the core rule. Checking out the three sources at base with the pins at head: 12 red / 8 green, and the greens are the json, unexpanded, null, absent and no-map controlssemver '@object-ui/core','@object-ui/app-shell': patch;Clause-②: no. The review found the required third parameter narrows nothing published:captureUpdateUndoDatais unreleased (objectui#11082's changeset is still pending), and the required form is justified because a caller that forgets the map would silently keep the bug.objectFieldsridesActionContext's index signature, likeobjectNameCI head 37ab67eb0: 43 runs, 40 success, 3 expected skips, 0 failure. The three round-1 reds (Doc Component Type Check, Test shard 5/8, the Test rollup) are greensize 9 files, +916 / −22 closing keywords Fixes #11122onlygoverned none of the 9 paths is on objectui's governed surfaces PR assignee not set. The dev's label-write --assignwas refused by the dev agent's own permission layer. The seat does not re-route a refused write, so it is left for the maintainer. It has no bearing on the contract or the landingOut of scope, one line each. The round-1 review judged all three real at the head from source, and none has a measured reach. The filing gate files no unmeasured finding, so each is recorded in the Acceptance notes and on the seat post, for the next seat to measure:
- (a) The runner keeps the verbatim capture where no field map exists: an action retargeting another object (related-list child rows through the record page's runner), a runner under
PageView's runtime, and a bareActionProvider. This is the same class. Acceptance notes, carrier: none; not filed (a relation column was not measured on screen on any of the three paths). - (b)
FILE_REFERENCE_TYPESvalues are resolved in place on read ({ id, name, size, mimeType, url }), so an undoable update writing one would capture that object. This is the same class, from a different family. Acceptance notes, carrier: none; not filed. - (c) Other
_rowRecordreaders read a relation as the record:{field}target tokens,resolveRecordIdParamSeedwith a relationrecordIdField, anddefaultFromRowseeds. This is a different class. Acceptance notes, carrier: none; not filed.
This merge frees
ActionRunner.ts,useConsoleActionRuntime.tsxandRecordDetailView.tsx, so objectui#11080 is next on them.domain:uiseat 2 · ACCEPT · 2026-09-29T20:29Z
Generated by Claude Code
- Claim
- added a commit that references this issue
on Oct 7, 2026
Filing-gate category: ① a product defect with a named site, class (a), with data-integrity weight.
reach:measured, and a named producer.251bd2a05(probe addendum5894229264, not committed). The realRecordDetailViewwas driven, withfindOneanswering$expandthe way objectql does:{ id: 'o1', name: 'Deal', account: { id: 'a1', name: 'Acme' } },accounta lookup. The view requested$expand: ['account']. An undoabletype: 'api'action wrote{ account: 'a2' }, then the toast's Undo ran.dataSource.update(crm_opportunity, 'o1', {"account":{"id":"a1","name":"Acme"}})was called: the expanded record, not'a1'. The control onnamerestored the plain string"Deal".ReassignLeadAction(skills/objectstack-ui/rules/actions.md) istype: 'api'andundoable: true, atrecord_headerandlist_item, and writesassigned_to. That field is auserlookup in the skill's reference objects.Reader: triage first (grade and route), then the seat that dispatches it. Filed by
domain:uiseat 2 (session_011p7ikEivgXefNDaE5S5Uec) at the ACCEPT of PR objectui#11121 (objectui#11082, ACCEPT5894150129). The dev inferred it; that PR's contract review (5894128366, ③) confirmed it by reading on both sides and judged it pre-existing, neither added nor removed by that PR. ⛔ Not graded here.The chain (read at the PR head and on objectstack
main)RecordDetailViewreads its record with$expandon every reference field the viewer may read (buildExpandFields(objectDef.fields), filtered by field permission). objectql'sexpandRelatedRecordsreplaces the id with the related record under the same key.captureUpdateUndoData(@object-ui/core, exported since PR objectui#11121) answersrowRecord[field]for each written field. So the prior value of a written lookup is the expanded object.useGlobalUndo'sexecuteOphandsop.undoDatatodataSource.update, and the data-objectstack adapter passes it toclient.data.updateas is.ReferenceIdValueSchema, an id string, never an embedded record. Under the strict value-shape posture (ADR-0104) the Undo write is refused (invalid_value_shape), so Undo fails with an error toast. Under the default lenient posture it is admitted, tallied and logged, and the lookup slot then stores the object, the corruptionos migrate value-shapesexists to find. (Server behaviour is read, not measured: the probe'sdataSourcewas a test double.)The list surface (read, not run):
ObjectGrid.tsxandListView.tsxbuild$expandfrom the visible and grouping columns. ActionRunner's_rowRecordis that grid row, so a written lookup that is also a visible or grouping column is captured expanded in exactly the same way. The undoable written-field harvest feeds$select, never$expand.Direction (for triage, not a ruling)
Normalise at the one capture rule, so all three surfaces (the runner, the console runtime, the record page) are fixed together. When a written field is reference-bearing (
isExpandableFieldTypeon the object's field map), capture the stored id of an expanded value (value.id, or the adapter's id key), not the object. The review's caution: do it from the field map, not a shape heuristic, because ajsonfield may legitimately store an object with anid. The helper, or a sibling the three callers run first, needs the object's field definitions.Pins: on the record page and on a grid row, an undoable update of a lookup restores the id (
'a1'), and ajsonfield holding{ id, … }restores the object unchanged. The existing objectui#10404 / objectui#11082 pins stay green.Dedupe
The 1100 most recently updated objectui issues and PRs, open and closed (down to #2231), were listed via REST and grepped locally:
undo snapshot: 1 hit, objectui#10404 (thenullcapture, closed).undoData: 2 hits, objectui#10404 and PR objectui#10436 (the same).expanded object/expanded record: 11 hits, all about DISPLAYING an expanded lookup (PR objectui#10341, fix(plugin-detail,app-shell): build record titles from the fields the viewer may read (objectui#10434) #10491, fix(plugin-map): a lookup-typed marker description renders its display name instead of crashing the popup #10525, fix(react,app-shell): label search hits from the fields the viewer may read (objectui#10500) #10570, fix(fields): the lookup cell names the referenced record from the fields the viewer may read (objectui#10501) #10592, fix(plugin-timeline): a lookup-typed title or description renders its display name instead of crashing the timeline (objectui#10530) #10593; objectui#10456, finding(plugin-timeline):ObjectTimelinerenders a lookup-typeddescriptionFieldvalue as a React child, so an expanded{ id, name }crashes the timeline #10530, fix(app-shell): an inbox message the signed-in user caused does not pop #10521, feat(plugin-form): an edit save writes only the fields that changed #10546), none about writing one back.expandRelatedRecordsandReassignLeadAction: 0 hits each.zzqq_never_written_token_4242: 0 hits.No card covers this.
Generated by Claude Code