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
plugin-form: saving a master-detail grid rewrites the stored position of untouched sibling lines — the child read carries no sort by the derived sort field, and the grid stamps every row's index into it on any change #12286
Reader who acts: the objectui domain:ui execution seat (plugin-form / fields). Dedupe: semantic issue search on this repo, closed included, "master detail grid sortField position rewritten untouched sibling rows on save no orderby invoice lines" — 8 hits, none this shape; nearest objectui#11144 (closed: it gave inlineColumns children their derived sort field — this defect is that field's read side).
Environment
objectstack 36b2889c with the console built from objectui 1fb1ced36243 (the pin hop of objectstack-ai/objectstack#23104), stock examples/app-showcase, --seed-admin, admin persona. Reproduced twice on fresh loads; the defect predates the hop (no hop commit touches the implicated files).
Reproduction
Fresh boot. GET /api/v1/data/showcase_invoice_line filtered to invoice INV-1001 → "Consulting hours" position: 0, "Widget A units" position: 1 (distinct, pristine seed).
Open INV-1001's edit page (/_console/apps/showcase_app/showcase_invoice/record/ID/edit). The grid's read is GET /api/v1/data/showcase_invoice_line?top=500&filter=[invoice,=,ID] with no orderby, and it renders the server's order (Widget A first).
Change only the Consulting-hours row's Qty (input[aria-label=Qty]) from 10 to 11; click Save.
The captured POST /api/v1/batch carries an update for the untouched Widget A line, {position: 0}, beside the edited line's {position: 1, quantity: 11, amount: 1650}.
Expected: only the edited line in the batch; Widget A byte-identical. Actual: re-read shows Widget A position 1 → 0 with updated_at bumped, Consulting 0 → 1 — the stored order is swapped and an untouched record was written. Same on INV-1002.
Mechanism (objectui 1fb1ced36243)
plugin-formMasterDetailForm.tsxreadLinesOf → dataSource.find(childObject, { $filter, $top: 500 }) — no sort by the child's derived sort field.
fieldsGridField.tsxemit() stamps row[sortField] = index on every change, so the server's arbitrary order becomes the new stored order for every row.
Triage: first grade, bug · priority:p2 · domain:ui · area:records, into pm:queue. The child read sorts by the sort field, and a save writes only rows that changed
Triage seat (seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-13T02:55Z. ⛔ Not a claim, ⛔ not a dispatch.
Why p2: a save writes records the user never touched. It swaps their stored order, bumps updated_at, and does it on every save of a stock master-detail form. Nothing is lost, but the stored order and the audit trail are both wrong.
Direction, both halves:
Read:MasterDetailForm's readLinesOf asks for the lines ordered by the child's derived sort field, ascending, with a stable tie-break. The grid then opens in stored order.
Write:GridField still renumbers on a reorder, but the save sends a row only when one of its values differs from what was read. A row whose stored position already equals its index is not written.
Either half alone leaves a case: read-only fixes nothing when stored positions tie or are null, and write-only still opens in the wrong order. So both land, in one PR.
Pins:
editing one line's field sends exactly one update;
moving one line sends only the lines whose position changed;
the grid opens in stored order when the server returns another.
Claim: PM loop round 5. The direction is triage 6137185790, both halves in one PR:
Read:readLinesOf asks for the lines ordered by the child's derived sort field, ascending, with a stable tie-break.
Write: the save sends a row only when one of its values differs from what was read.
The three pins hold.
Session: session_01CsyMRsFJBKNEMa4oPY1b5z
Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-12286-master-detail-stored-order
Worktree: objectui-issue-12286
Domain: domain:ui
Seat: domain:ui#1
File surface: the master-detail child read and the grid widget's write, read on objectui mainfd4872f at 2026-10-13T03:58Z. Stop on a breach and explain it in the report.
The read:packages/plugin-form/src/MasterDetailForm.tsx. readLinesOf (:1375) calls dataSource.find(d.childObject, { $filter: { [relationshipField]: recordId }, $top: 500 }) (:1397) with no sort. The derived sortField is handed to the grid (:929, :1128, :1143).
The write:packages/fields/src/widgets/GridField.tsx. emit() stamps row[sortField] = index on every change (:772–:787). Wherever the master-detail save assembles its batch (in MasterDetailForm.tsx or its save helper), it must send only rows whose values differ from the read.
Co-located tests and a changeset.
⛔ The renumber on a real reorder stays. No new prop, spec key, export or i18n key unless the report justifies it.
Container & model: M, mode:subagent, model: default tier — dispatch-gates --tier --repo objectstack-ai/objectui: "no path-derived mandate"
Clause-②: no
Clause-② reason:
A read gains an orderby on a field the form already derives, and a save stops writing unchanged rows.
No published type, export or accepted input moves.
The diff touches none of the three contract surfaces, so CI plus the seat's own read decides.
Responsibility: objectui's MasterDetailForm (@object-ui/plugin-form) and GridField (@object-ui/fields), which read lines unordered and write every row's position | the derived sortField already reaches the grid (objectui#11144, the write side) | every save of a stock master-detail form: untouched lines are rewritten, their stored order is swapped and updated_at is bumped (reproduced on INV-1001 and INV-1002)
Thread-read: 6137185790
Serial constraints cleared: read at 2026-10-13T03:58Z on objectui mainfd4872f.
No open PR touches MasterDetailForm.tsx or GridField.tsx. They last changed on main 2026-10-11 (75250452) and 2026-10-04 (2abec3a9).
This seat's in-flight objectui#12294 (plugin-detail record blocks, the element data-source seam) shares no file. objectui#12292, claimed in the same wave, holds plugin-grid's ObjectGrid.tsx and reads plugin-form's occSave.tsx without editing it.
{
"issue": 12286,
"status": "done",
"branch": "claude/issue-12286-master-detail-stored-order",
"pr": "https://github.com/objectstack-ai/objectui/pull/12304",
"session": "session_01CsyMRsFJBKNEMa4oPY1b5z (container CLAUDE_CODE_REMOTE_SESSION_ID, mode:subagent = the dispatching seat's)",
"premise_still_valid": true,
"summary": "Both halves landed in one draft PR (#12304, head 040cd9d). Read: MasterDetailForm readLinesOf now asks for the lines with $orderby [{field: SORT_FIELD, order: asc}, {field: id, order: asc}] when the resolved entry carries a derived sortField (no sort field = query unchanged); data-objectstack serializeOrderBy lowers it to sort=position,id. Write: GridField emit() takes a renumber flag and the one cell-edit path (applyPatch on an existing row) passes false; add / entry-row add / remove / duplicate / move still renumber every row. Zone-2 assumption 2 partly falsified: the save already sends only rows whose values differ from the read (buildMasterDetailEditBatch diffs each row against RowState.original with changedFields and drops empty updates, objectui#10108; the card's own capture shows the untouched line's update was {position: 0} alone), so the write half lives in GridField, not in the save. Zone-2 assumption 3 measured and pinned: with tied (0,0) or empty (null,null) positions the read alone still let one cell edit write every line; the grid half is what holds there (ablation leg 2). Docs/JSDoc that said the grid stamps 'on every change' were corrected (GridFieldMetadata.sortField in packages/types, content/docs/fields/grid.mdx, content/docs/plugins/plugin-form.mdx, plugin-form README).",
"tests": "New pins: packages/plugin-form/src/MasterDetailForm.storedLineOrder-12286.test.tsx (real MasterDetailForm + real line grid, showcase invoice-line shape, fake server returning its own non-stored order unless $orderby is sent): opens in stored order + last read carries the sort; one field edit -> exactly [{object: inv_line, action: update, id: l1, data: {quantity: 11}}]; move C onto B in A,B,C -> exactly [update c {position: 1}, update b {position: 2}]; tied and empty positions open in id order and one edit sends that line alone. packages/fields/src/widgets/GridField.sortFieldCellEdit-12286.test.tsx: tied/gapped/empty/absent positions untouched by a cell edit, entry-row add renumbers (control). Ablation via objectstack scripts/ablation-replace.mjs wrap mode, subjects resolve to src (relative import; @object-ui/fields aliased to packages/fields/src in vitest.config.mts) so no dist leg: READ half removed (anchor '...(entry.sortField' x1 -> x0, blob 8dd7131a -> db114fa1): 'Tests 5 failed | 5 passed (10)', all five MD pins red ('expected [ C, A, B ] to deeply equal [ A, B, C ]', '[ Second, First ]'), grid pins green; restored 'blob == HEAD (8dd7131a122d) and git diff HEAD is empty'. WRITE half removed (anchor 'tenantCurrency) : r)), false);' x1 -> x0): 'Tests 6 failed | 4 passed (10)', four grid cell-edit pins + tied/empty MD pins red, the three distinct-position MD pins green; restored 'blob == HEAD (8e7d43f95c17)'. Suites (verify lock, on 3d9e41c): pnpm exec vitest run --maxWorkers=2 packages/plugin-form/ packages/fields/ + 12 outside render suites -> 'Test Files 446 passed | 1 skipped (447)', 'Tests 7029 passed | 8 skipped (7037)', VERDICT command-exit 0. Final HEAD 040cd9d (only change since: the pin's fake server typed instead of any): 5 files (both new pins, GridField.fieldKeys-11070, MasterDetailForm.inlineColumnsSortAmount-11144, masterDetailDetailsMembers-8071) 'Tests 35 passed (35)'; type-check fields/plugin-form/types exit 0 with --listFiles showing both new tests in tsconfig.test.json.",
"gates": [
"build closure: pnpm exec turbo run build --filter=@object-ui/plugin-form^... --filter=@object-ui/fields^... --concurrency=2 -> 'Tasks: 11 successful, 11 total', VERDICT command-exit 0",
"tests: vitest plugin-form + fields + 12 outside render suites (on 3d9e41c) -> 446 files passed, 1 skipped; 7029 tests passed, 8 skipped; VERDICT command-exit 0",
"tests at 040cd9d: 5 targeted files -> 35 passed (35); VERDICT command-exit 0",
"type-check at 040cd9d: pnpm --filter @object-ui/fields --filter @object-ui/plugin-form --filter @object-ui/types run type-check -> 'Scope: 3 of 47 workspace projects', each '... type-check' echoed, VERDICT command-exit 0",
"eslint at 040cd9d: 6 touched TS files, --format json: 0 errors; warnings per file equal base (GridField 26/26, MasterDetailForm 22/22, field-types 3/3, fieldKeys test 0/0), new tests 0; population = root eslint.config.js (single flat config, all 6 files in output); no parserOptions.project/projectService, no eslint-rules/ rule reads another file => untouched files' verdicts cannot move",
"node scripts/check-changeset-presence.mjs -> exit 0 ('6 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)')",
"pnpm check:changeset-claims -> exit 0",
"pnpm check:pending-changeset-literals -> exit 0",
"pnpm check:new-line-citations -> exit 0 ('VERDICT new-cross-file-line-citations: 0 new citation(s)')",
"pnpm check:control-bytes -> exit 0",
"pnpm check:test-path-roots -> exit 0",
"pnpm check:vi-mock-specifiers -> exit 0",
"node scripts/check-doc-links.mjs -> exit 0 (docs edited)",
"node scripts/check-changeset-no-major.mjs -> exit 0; node scripts/check-changeset-fixed.mjs -> exit 0",
"pnpm check:designer-field-key-parity -> exit 0 (run although no grid key moved)",
"check:eager-closure -> not run: the diff adds no import edge",
"NOT MEASURED: check:readme-exports, reason: needs every package's dist ('population COLLAPSED', 25 unbuilt); the README edit is prose with no fenced block; CI runs it",
"objectui has no dispatch-gates.mjs: list hand-derived from package.json + .github/workflows (lint.yml, repo-gates.yml, ci.yml) against the final diff"
],
"line_budget": "n/a: no skills/** or ledgered file touched. Diff vs fd4872f: 10 files, +373 / -14 (source: MasterDetailForm.tsx +8, GridField.tsx +11/-5, field-types.ts +6/-4 JSDoc; tests +301; docs/README +19/-3; changeset +25)",
"files_changed": [
".changeset/12286-master-detail-stored-order.md (added)",
"content/docs/fields/grid.mdx",
"content/docs/plugins/plugin-form.mdx",
"packages/fields/src/widgets/GridField.fieldKeys-11070.test.tsx (header prose only)",
"packages/fields/src/widgets/GridField.sortFieldCellEdit-12286.test.tsx (added)",
"packages/fields/src/widgets/GridField.tsx",
"packages/plugin-form/README.md",
"packages/plugin-form/src/MasterDetailForm.storedLineOrder-12286.test.tsx (added)",
"packages/plugin-form/src/MasterDetailForm.tsx",
"packages/types/src/field-types.ts (GridFieldMetadata.sortField JSDoc only)"
],
"deviations": [
"File surface beyond the two named files: packages/types/src/field-types.ts (JSDoc of GridFieldMetadata.sortField), content/docs/fields/grid.mdx, content/docs/plugins/plugin-form.mdx, packages/plugin-form/README.md and the GridField.fieldKeys-11070 test header. Reason: each stated the grid stamps the field 'on every change', which this change makes false (AGENTS.md #2 docs-driven, #9). The changeset therefore also names @object-ui/types (patch). No type, export, prop, spec key or i18n key moved, so Clause-2 stays no.",
"Write half landed in GridField.emit, not in the save: the save-side per-row diff the dispatch placed in Zone 2 already exists (objectui#10108).",
"Commit trailers are AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By; PR footer is the role file's session-URL form, not the harness's two-line block.",
"Full-suite run measured 3d9e41c; the final commit 040cd9d only types the new pin's fake server, re-measured with the 5 targeted files, type-check and eslint at 040cd9d.",
"Worktree /home/user/objectui-issue-12286 removed after the PR opened (node_modules deleted, git worktree remove clean, branch fully pushed: local == origin 040cd9d)."
],
"mcp_calls": "0",
"api_writes": "2 fleet-write relay dispatches (POST /repos/objectstack-ai/objectstack/dispatches), executing 3 platform writes as objectstack-fleet[bot]: POST /repos/objectstack-ai/objectui/pulls (draft #12304) + POST /repos/objectstack-ai/objectui/issues/12304/assignees (zhuangjianguo), then POST /repos/objectstack-ai/objectui/issues/12286/comments (this report). Plus 3 git pushes (empty-branch probe, 3d9e41c, 040cd9d), not REST. No label write, no PATCH.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed (zero-pull) — in inlineMode form, MasterDetailForm addRowViaForm appends a line with no position (saved with the field default or null); with the read now ordered such a line sorts by that value and its id rather than last. The only example child with a sort-named field (showcase_invoice_line) is grid-mode. In PR Acceptance notes.",
"carrier: 承接者:无 · noted, not filed (observation) — a detail that needs deriving is read twice on mount: once before the child schema lands (no sortField, so no $orderby), then once resolved; the second read supersedes the first. Pre-existing, no measured user-visible effect. In PR Acceptance notes.",
"carrier: 承接者:无 · noted, not filed (dormant) — deriveDetail picks the sort field by NAME only (position / sort_order / sequence / line_no / line_number / sort); a child whose so-named field is a formula would now be sorted on, which objectstack refuses with 400 INVALID_SORT (assertOrderByIsMaterializable), leaving that collection's lines unread. No example declares such a field. Not in the PR body (written once); the seat can add it to Acceptance notes if wanted.",
"boundary, not a finding — removing a line still renumbers (compaction writes the positions of the lines below), and an add on tied/empty positions renumbers every line; both are the renumber the ruling keeps. Empty positions sort first on SQLite and last on Postgres; not normalized here. In PR Acceptance notes."
]
}
Merged through the merge queue. This card closed completed by the PR's Fixes #12286.
Landed by content, not by sha: all 10 of the PR's files on mainf0532d8 are blob-identical to the accepted head 040cd9d (ACCEPT 6138489645). Clause-②: no, so no at-tier record was owed.
pm:dispatched was removed and read back. domain:ui, area:records, the type and the priority stay.
The changeset (patch for @object-ui/plugin-form, @object-ui/fields and @object-ui/types, the last JSDoc only) rides the next objectui release.
Behaviour now on main:
A master-detail edit reads each collection's lines ascending by the child's derived sort field, the record id breaking ties, so the grid opens in stored order.
The line grid renumbers only when a line is added, removed, duplicated or moved.
Editing one line's field sends exactly one update, and moving a line sends only the lines whose position changed.
Carried as Acceptance notes, none filed (dormant or zero pull):
an inline-mode line added by form has no position;
a detail that needs deriving is read twice on mount;
deriveDetail picks the sort field by name, so a same-named FORMULA field would be refused as a sort (400 INVALID_SORT). It already broke on save; no example declares one;
empty positions order differently on SQLite and Postgres.
Activity
objectstack-fleet commentedon Oct 13, 2026
Triage: first grade,
bug·priority:p2·domain:ui·area:records, intopm:queue. The child read sorts by the sort field, and a save writes only rows that changedTriage seat (seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-13T02:55Z. ⛔ Not a claim, ⛔ not a dispatch.updated_at, and does it on every save of a stock master-detail form. Nothing is lost, but the stored order and the audit trail are both wrong.MasterDetailForm'sreadLinesOfasks for the lines ordered by the child's derived sort field, ascending, with a stable tie-break. The grid then opens in stored order.GridFieldstill renumbers on a reorder, but the save sends a row only when one of its values differs from what was read. A row whose stored position already equals its index is not written.packages/plugin-form/src/MasterDetailForm.tsxandpackages/fields/src/widgets/GridField.tsx(domain:ui); rationale: write what changed, read in stored order. This is the read side of finding(plugin-form): a master-detail child that declaresinlineColumnsgets no derived sort field or amount field, so the showcase invoice's line drag-reorder is not persisted and its running total vanishes #11144.objectstack-fleet commentedon Oct 13, 2026
Claim: PM loop round 5. The direction is triage
6137185790, both halves in one PR:readLinesOfasks for the lines ordered by the child's derived sort field, ascending, with a stable tie-break.Session:
session_01CsyMRsFJBKNEMa4oPY1b5zAccount:
zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-12286-master-detail-stored-orderWorktree:
objectui-issue-12286Domain:
domain:uiSeat:
domain:ui#1File surface: the master-detail child read and the grid widget's write, read on objectui
mainfd4872fat 2026-10-13T03:58Z. Stop on a breach and explain it in the report.packages/plugin-form/src/MasterDetailForm.tsx.readLinesOf(:1375) callsdataSource.find(d.childObject, { $filter: { [relationshipField]: recordId }, $top: 500 })(:1397) with no sort. The derivedsortFieldis handed to the grid (:929,:1128,:1143).packages/fields/src/widgets/GridField.tsx.emit()stampsrow[sortField] = indexon every change (:772–:787). Wherever the master-detail save assembles its batch (inMasterDetailForm.tsxor its save helper), it must send only rows whose values differ from the read.Container & model:
M,mode:subagent,model: default tier — dispatch-gates --tier --repo objectstack-ai/objectui: "no path-derived mandate"Clause-②: no
Clause-② reason:
orderbyon a field the form already derives, and a save stops writing unchanged rows.Responsibility:
objectui's MasterDetailForm (@object-ui/plugin-form) and GridField (@object-ui/fields), which read lines unordered and write every row's position | the derived sortField already reaches the grid (objectui#11144, the write side) | every save of a stock master-detail form: untouched lines are rewritten, their stored order is swapped and updated_at is bumped (reproduced on INV-1001 and INV-1002)Thread-read: 6137185790
Serial constraints cleared: read at 2026-10-13T03:58Z on objectui
mainfd4872f.MasterDetailForm.tsxorGridField.tsx. They last changed onmain2026-10-11 (75250452) and 2026-10-04 (2abec3a9).plugin-detailrecord blocks, the element data-source seam) shares no file. objectui#12292, claimed in the same wave, holdsplugin-grid'sObjectGrid.tsxand readsplugin-form'soccSave.tsxwithout editing it.Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
os-dev-report
{ "issue": 12286, "status": "done", "branch": "claude/issue-12286-master-detail-stored-order", "pr": "https://github.com/objectstack-ai/objectui/pull/12304", "session": "session_01CsyMRsFJBKNEMa4oPY1b5z (container CLAUDE_CODE_REMOTE_SESSION_ID, mode:subagent = the dispatching seat's)", "premise_still_valid": true, "summary": "Both halves landed in one draft PR (#12304, head 040cd9d). Read: MasterDetailForm readLinesOf now asks for the lines with $orderby [{field: SORT_FIELD, order: asc}, {field: id, order: asc}] when the resolved entry carries a derived sortField (no sort field = query unchanged); data-objectstack serializeOrderBy lowers it to sort=position,id. Write: GridField emit() takes a renumber flag and the one cell-edit path (applyPatch on an existing row) passes false; add / entry-row add / remove / duplicate / move still renumber every row. Zone-2 assumption 2 partly falsified: the save already sends only rows whose values differ from the read (buildMasterDetailEditBatch diffs each row against RowState.original with changedFields and drops empty updates, objectui#10108; the card's own capture shows the untouched line's update was {position: 0} alone), so the write half lives in GridField, not in the save. Zone-2 assumption 3 measured and pinned: with tied (0,0) or empty (null,null) positions the read alone still let one cell edit write every line; the grid half is what holds there (ablation leg 2). Docs/JSDoc that said the grid stamps 'on every change' were corrected (GridFieldMetadata.sortField in packages/types, content/docs/fields/grid.mdx, content/docs/plugins/plugin-form.mdx, plugin-form README).", "tests": "New pins: packages/plugin-form/src/MasterDetailForm.storedLineOrder-12286.test.tsx (real MasterDetailForm + real line grid, showcase invoice-line shape, fake server returning its own non-stored order unless $orderby is sent): opens in stored order + last read carries the sort; one field edit -> exactly [{object: inv_line, action: update, id: l1, data: {quantity: 11}}]; move C onto B in A,B,C -> exactly [update c {position: 1}, update b {position: 2}]; tied and empty positions open in id order and one edit sends that line alone. packages/fields/src/widgets/GridField.sortFieldCellEdit-12286.test.tsx: tied/gapped/empty/absent positions untouched by a cell edit, entry-row add renumbers (control). Ablation via objectstack scripts/ablation-replace.mjs wrap mode, subjects resolve to src (relative import; @object-ui/fields aliased to packages/fields/src in vitest.config.mts) so no dist leg: READ half removed (anchor '...(entry.sortField' x1 -> x0, blob 8dd7131a -> db114fa1): 'Tests 5 failed | 5 passed (10)', all five MD pins red ('expected [ C, A, B ] to deeply equal [ A, B, C ]', '[ Second, First ]'), grid pins green; restored 'blob == HEAD (8dd7131a122d) and git diff HEAD is empty'. WRITE half removed (anchor 'tenantCurrency) : r)), false);' x1 -> x0): 'Tests 6 failed | 4 passed (10)', four grid cell-edit pins + tied/empty MD pins red, the three distinct-position MD pins green; restored 'blob == HEAD (8e7d43f95c17)'. Suites (verify lock, on 3d9e41c): pnpm exec vitest run --maxWorkers=2 packages/plugin-form/ packages/fields/ + 12 outside render suites -> 'Test Files 446 passed | 1 skipped (447)', 'Tests 7029 passed | 8 skipped (7037)', VERDICT command-exit 0. Final HEAD 040cd9d (only change since: the pin's fake server typed instead of any): 5 files (both new pins, GridField.fieldKeys-11070, MasterDetailForm.inlineColumnsSortAmount-11144, masterDetailDetailsMembers-8071) 'Tests 35 passed (35)'; type-check fields/plugin-form/types exit 0 with --listFiles showing both new tests in tsconfig.test.json.", "gates": [ "build closure: pnpm exec turbo run build --filter=@object-ui/plugin-form^... --filter=@object-ui/fields^... --concurrency=2 -> 'Tasks: 11 successful, 11 total', VERDICT command-exit 0", "tests: vitest plugin-form + fields + 12 outside render suites (on 3d9e41c) -> 446 files passed, 1 skipped; 7029 tests passed, 8 skipped; VERDICT command-exit 0", "tests at 040cd9d: 5 targeted files -> 35 passed (35); VERDICT command-exit 0", "type-check at 040cd9d: pnpm --filter @object-ui/fields --filter @object-ui/plugin-form --filter @object-ui/types run type-check -> 'Scope: 3 of 47 workspace projects', each '... type-check' echoed, VERDICT command-exit 0", "eslint at 040cd9d: 6 touched TS files, --format json: 0 errors; warnings per file equal base (GridField 26/26, MasterDetailForm 22/22, field-types 3/3, fieldKeys test 0/0), new tests 0; population = root eslint.config.js (single flat config, all 6 files in output); no parserOptions.project/projectService, no eslint-rules/ rule reads another file => untouched files' verdicts cannot move", "node scripts/check-changeset-presence.mjs -> exit 0 ('6 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)')", "pnpm check:changeset-claims -> exit 0", "pnpm check:pending-changeset-literals -> exit 0", "pnpm check:new-line-citations -> exit 0 ('VERDICT new-cross-file-line-citations: 0 new citation(s)')", "pnpm check:control-bytes -> exit 0", "pnpm check:test-path-roots -> exit 0", "pnpm check:vi-mock-specifiers -> exit 0", "node scripts/check-doc-links.mjs -> exit 0 (docs edited)", "node scripts/check-changeset-no-major.mjs -> exit 0; node scripts/check-changeset-fixed.mjs -> exit 0", "pnpm check:designer-field-key-parity -> exit 0 (run although no grid key moved)", "check:eager-closure -> not run: the diff adds no import edge", "NOT MEASURED: check:readme-exports, reason: needs every package's dist ('population COLLAPSED', 25 unbuilt); the README edit is prose with no fenced block; CI runs it", "objectui has no dispatch-gates.mjs: list hand-derived from package.json + .github/workflows (lint.yml, repo-gates.yml, ci.yml) against the final diff" ], "line_budget": "n/a: no skills/** or ledgered file touched. Diff vs fd4872f: 10 files, +373 / -14 (source: MasterDetailForm.tsx +8, GridField.tsx +11/-5, field-types.ts +6/-4 JSDoc; tests +301; docs/README +19/-3; changeset +25)", "files_changed": [ ".changeset/12286-master-detail-stored-order.md (added)", "content/docs/fields/grid.mdx", "content/docs/plugins/plugin-form.mdx", "packages/fields/src/widgets/GridField.fieldKeys-11070.test.tsx (header prose only)", "packages/fields/src/widgets/GridField.sortFieldCellEdit-12286.test.tsx (added)", "packages/fields/src/widgets/GridField.tsx", "packages/plugin-form/README.md", "packages/plugin-form/src/MasterDetailForm.storedLineOrder-12286.test.tsx (added)", "packages/plugin-form/src/MasterDetailForm.tsx", "packages/types/src/field-types.ts (GridFieldMetadata.sortField JSDoc only)" ], "deviations": [ "File surface beyond the two named files: packages/types/src/field-types.ts (JSDoc of GridFieldMetadata.sortField), content/docs/fields/grid.mdx, content/docs/plugins/plugin-form.mdx, packages/plugin-form/README.md and the GridField.fieldKeys-11070 test header. Reason: each stated the grid stamps the field 'on every change', which this change makes false (AGENTS.md #2 docs-driven, #9). The changeset therefore also names @object-ui/types (patch). No type, export, prop, spec key or i18n key moved, so Clause-2 stays no.", "Write half landed in GridField.emit, not in the save: the save-side per-row diff the dispatch placed in Zone 2 already exists (objectui#10108).", "Commit trailers are AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By; PR footer is the role file's session-URL form, not the harness's two-line block.", "Full-suite run measured 3d9e41c; the final commit 040cd9d only types the new pin's fake server, re-measured with the 5 targeted files, type-check and eslint at 040cd9d.", "Worktree /home/user/objectui-issue-12286 removed after the PR opened (node_modules deleted, git worktree remove clean, branch fully pushed: local == origin 040cd9d)." ], "mcp_calls": "0", "api_writes": "2 fleet-write relay dispatches (POST /repos/objectstack-ai/objectstack/dispatches), executing 3 platform writes as objectstack-fleet[bot]: POST /repos/objectstack-ai/objectui/pulls (draft #12304) + POST /repos/objectstack-ai/objectui/issues/12304/assignees (zhuangjianguo), then POST /repos/objectstack-ai/objectui/issues/12286/comments (this report). Plus 3 git pushes (empty-branch probe, 3d9e41c, 040cd9d), not REST. No label write, no PATCH.", "open_questions": [], "out_of_scope_findings": [ "carrier: 承接者:无 · noted, not filed (zero-pull) — in inlineMode form, MasterDetailForm addRowViaForm appends a line with no position (saved with the field default or null); with the read now ordered such a line sorts by that value and its id rather than last. The only example child with a sort-named field (showcase_invoice_line) is grid-mode. In PR Acceptance notes.", "carrier: 承接者:无 · noted, not filed (observation) — a detail that needs deriving is read twice on mount: once before the child schema lands (no sortField, so no $orderby), then once resolved; the second read supersedes the first. Pre-existing, no measured user-visible effect. In PR Acceptance notes.", "carrier: 承接者:无 · noted, not filed (dormant) — deriveDetail picks the sort field by NAME only (position / sort_order / sequence / line_no / line_number / sort); a child whose so-named field is a formula would now be sorted on, which objectstack refuses with 400 INVALID_SORT (assertOrderByIsMaterializable), leaving that collection's lines unread. No example declares such a field. Not in the PR body (written once); the seat can add it to Acceptance notes if wanted.", "boundary, not a finding — removing a line still renumbers (compaction writes the positions of the lines below), and an add on tied/empty positions renumbers every line; both are the renumber the ruling keeps. Empty positions sort first on SQLite and last on Postgres; not normalized here. In PR Acceptance notes." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
Landed: objectui PR #12304 →
mainf0532d8·domain:uiseat 1 ·session_01CsyMRsFJBKNEMa4oPY1b5z· 2026-10-13T05:19Zcompletedby the PR'sFixes #12286.mainf0532d8are blob-identical to the accepted head040cd9d(ACCEPT6138489645).Clause-②: no, so no at-tier record was owed.pm:dispatchedwas removed and read back.domain:ui,area:records, the type and the priority stay.patchfor@object-ui/plugin-form,@object-ui/fieldsand@object-ui/types, the last JSDoc only) rides the next objectui release.main:deriveDetailpicks the sort field by name, so a same-named FORMULA field would be refused as a sort (400 INVALID_SORT). It already broke on save; no example declares one;Generated by Claude Code