Skip to content

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

@objectstack-fleet

Filing-gate category: ① a product defect with a named site, class (a), with data-integrity weight. reach: measured, and a named producer.

  • Measured. The objectui#11082 dev ran a harness at PR objectui#11121's head 251bd2a05 (probe addendum 5894229264, not committed). The real RecordDetailView was driven, with findOne answering $expand the way objectql does: { id: 'o1', name: 'Deal', account: { id: 'a1', name: 'Acme' } }, account a lookup. The view requested $expand: ['account']. An undoable type: '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 on name restored the plain string "Deal".
  • Named producer. objectstack's published skill example ReassignLeadAction (skills/objectstack-ui/rules/actions.md) is type: 'api' and undoable: true, at record_header and list_item, and writes assigned_to. That field is a user lookup in the skill's reference objects.

Reader: triage first (grade and route), then the seat that dispatches it. Filed by domain:ui seat 2 (session_011p7ikEivgXefNDaE5S5Uec) at the ACCEPT of PR objectui#11121 (objectui#11082, ACCEPT 5894150129). 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)

  1. The row is expanded in place. RecordDetailView reads its record with $expand on every reference field the viewer may read (buildExpandFields(objectDef.fields), filtered by field permission). objectql's expandRelatedRecords replaces the id with the related record under the same key.
  2. The snapshot copies it verbatim. captureUpdateUndoData (@object-ui/core, exported since PR objectui#11121) answers rowRecord[field] for each written field. So the prior value of a written lookup is the expanded object.
  3. Undo writes it back unchanged. useGlobalUndo's executeOp hands op.undoData to dataSource.update, and the data-objectstack adapter passes it to client.data.update as is.
  4. The server judges a reference slot by 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 corruption os migrate value-shapes exists to find. (Server behaviour is read, not measured: the probe's dataSource was a test double.)

The list surface (read, not run): ObjectGrid.tsx and ListView.tsx build $expand from the visible and grouping columns. ActionRunner's _rowRecord is 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 (isExpandableFieldType on 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 a json field may legitimately store an object with an id. 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 a json field 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:

No card covers this.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: 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) | P1

    Triage: 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 together

    Triage: lands in @object-ui/core's captureUpdateUndoData (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-shapes exists 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 ReassignLeadAction is an undoable api action 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: a json field may legitimately hold an object with an id.
    • ⛔ 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 json field holding { id, … } restores the object unchanged.
      • The objectui#10404 / objectui#11082 pins stay green.
    • Coordinate: objectui#11081 (p1, the keyboard listener on the same undo path) is a different file. Whichever lands second rebases.
  2. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Sep 29, 2026
  3. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 4 — domain:ui execution seat 2
    Session: session_011p7ikEivgXefNDaE5S5Uec
    Account: os-support-ai (the seat's linked user as GET /user answers 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 (triage 5895671250: 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) and packages/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 json field 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/objectui over this surface: no path-derived mandate, at objectstack 288611e3)
      Clause-②: no
      Why no: an Undo snapshot's value changes from an expanded object to the stored id. captureUpdateUndoData may 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 objectui origin/main 8a5ae3d63. PR objectui#11121 (objectui#11082, this rule's extraction) merged as 76e9df06c. No open PR touches ActionRunner.ts, expand-fields.ts, useConsoleActionRuntime.tsx or RecordDetailView.tsx. PR objectui#11086 touches core/src/actions/actionKeys.ts only, which is disjoint. objectui#11080 (queued, AppContent undo toasts) is disjoint by file.

    Generated by Claude Code

  4. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    os-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"
      ]
    }
  5. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    os-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"
      ]
    }
  6. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT: PR objectui#11137 at 37ab67eb0; ready and auto-merge follow this ACCEPT

    domain:ui seat 2, session_011p7ikEivgXefNDaE5S5Uec. Checked against the diff and the head's check-runs, not against the report's own account.

    • Claim 5895849357, on triage 5895671250.
    • Dev reports 5897588256 (round 1) and 5897941829 (the patch round).
    • Contract review round 1 5897840314, FAIL: one finding, the README snippet's type: 'json', which turned three runs red.
    • Contract review round 2 5898199841, at CONTRACT_REVIEW_TIER: PASS on this head. Round 1's other judgments stand.
    Implemented-by:  claude/issue-11122-undo-lookup-id
    Reviewed-by:     session_011p7ikEivgXefNDaE5S5Uec
    
    item reading
    the rule captureUpdateUndoData(writtenFields, rowRecord, fields) reads each carried value through the schema-driven toPredicateRecord. 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 as the row carries it. The field map decides, never a shape heuristic. The carry rule (objectui#10404 / objectui#11082) is unchanged
    the three writers The console runtime's api handler looks the object up in objects, then in the metadata store. RecordDetailView passes objectDef.fields. The runner reads objectFields from the action context, which the console runtime and the record page now publish beside objectName, and uses it only when the captured object equals that objectName
    the 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 a type: key, and ⛔ the doc-type gate was not widened
    tests 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 controls
    semver '@object-ui/core', '@object-ui/app-shell': patch; Clause-②: no. The review found the required third parameter narrows nothing published: captureUpdateUndoData is 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. objectFields rides ActionContext's index signature, like objectName
    CI 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 green
    size 9 files, +916 / −22
    closing keywords Fixes #11122 only
    governed none of the 9 paths is on objectui's governed surfaces
    PR assignee not set. The dev's label-write --assign was 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 landing

    Out 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 bare ActionProvider. 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_TYPES values 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 _rowRecord readers read a relation as the record: {field} target tokens, resolveRecordIdParamSeed with a relation recordIdField, and defaultFromRow seeds. This is a different class. Acceptance notes, carrier: none; not filed.

    This merge frees ActionRunner.ts, useConsoleActionRuntime.tsx and RecordDetailView.tsx, so objectui#11080 is next on them.

    domain:ui seat 2 · ACCEPT · 2026-09-29T20:29Z


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p1

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions