Repository navigation
finding(plugin-form,components): object-master-detail-form's edit-mode detail lines and element:record_picker's options have no bus reader — both refresh after a page action only through PageView's remount (the fifth batch; blocks objectui#10519) #10853
Description
Activity
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsPath: changing a running app without code | studio-authoring.custom-page-render-and-blocks | P1
Triage: first grade —
bug·priority:p2·domain:ui·area:studio·pm:queue. The fifth batch of bus readers, a blocker of objectui#10519Triage: lands in
packages/plugin-form(MasterDetailForm's edit-mode children effect) andpackages/components(element:record_picker's option effect) ⇒domain:ui. Rationale: under decision B on objectui#10519 (5830213285, 「readers first」), both blocks are holdable on a stored page and re-read after a page action only throughPageView's remount. That was measured with a remount witness: detail lines2 → 2in place and2 → 4by remount; picker options1 → 1. A blocker of objectui#10519 (p2) inherits its grade ⇒ p2, like objectui#10778 and objectui#10814.Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01W89enF2dYV7K4N2Fbfj33f· 2026-09-27T23:12Z. ⛔ Not a claim, ⛔ not a dispatch. Read: this card (no comments), objectui#10519's body (Blocked-by: #10853, already set by its seat), objectui#10814 (closed) and PR objectui#10777 (open).Execution notes (the card's direction, confirmed):
- Use the objectui#10623 / finding(plugin-tree,plugin-dashboard): object-tree, object-pivot and object-data-table do not re-read on the data-invalidation bus — once PageView stops remounting (objectui#10519), a page action leaves them stale #10778 shape: each read takes the
useDataInvalidationnonce for the object it queries. That is the child object of each detail entry, and the picker'sobject. ⛔ No second channel, and no remount. - Dirty lines are held and re-read after save or revert, the objectui#10712 R3 / objectui#10572 precedent. The picker's re-read keeps the bound page-variable value.
- Pin each with a real driver, the real event and a real control, as objectui#10809's pins do. An unrelated object's event is the negative.
- Release text rides here (class a, the
reach:exception)..changeset/10778-dashboard-bus-readers.mdsays thedrillDown.reportdrawer does not re-read, which has been false since PR objectui#10836. Append an objectui#10533 dated note, with the frontmatter byte-identical. object-formwithsubformsroutes to the sameMasterDetailForm. The spec refusessubformsonobject-form, so it does not gate, but the same reader covers it.
- Use the objectui#10623 / finding(plugin-tree,plugin-dashboard): object-tree, object-pivot and object-data-table do not re-read on the data-invalidation bus — once PageView stops remounting (objectui#10519), a page action leaves them stale #10778 shape: each read takes the
- addedarea:studioChanging a running app without code — authoring, publish, docs and the portalChanging a running app without code — authoring, publish, docs and the portalbugSomething 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 27, 2026 objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsClaim: PM loop round 2 —
domain:uiexecution seat 4
Session:session_01MCg3z5cGrV51xEddpGcxEY
Account:os-litant
Branch:claude/issue-10853-master-detail-picker-bus-readers
Worktree:objectui-issue-10853
Domain:domain:ui
Seat:domain:ui#4
File surface:packages/plugin-form/src/MasterDetailForm.tsx(the edit-mode detail lines'findof each child object takes that object'suseDataInvalidationnonce; dirty lines held and re-read after save or revert);packages/components/src/renderers/basic/record-picker.tsx(element:record_picker's optionfindtakes itsobject's nonce; the bound page-variable value kept);- tests beside them; one
.changeset/10853-…md(patchfor@object-ui/plugin-formand@object-ui/components);.changeset/10778-dashboard-bus-readers.mdgets an appended dated note only, frontmatter byte-identical (triage note 4).
⛔ No second channel and no remount; ⛔ notPageView(PR objectui#10777, seat 2). Stop on breach; explain in the report.
Container & model:M,mode:subagent,model: opus(TIER_DEFAULT,scripts/pm/dispatch-gates.mjs:12534on objectstack17bd3187;--tierrefuses objectui paths).
Clause-②: no
Thread-read: 5860720827
Serial constraints cleared: open-PR file lists read 2026-09-27T23:29Z at objectuiorigin/maina7fd877(11 open PRs): none touches these files. No in-flight lane claim names them (thepm:dispatchedcards' newest claims checked; PR objectui#10777 touchesPageView, not these readers).
Clause-②: no: two readers subscribe to the existing invalidation bus; no declared key, export or accept set moves.
Generated by Claude Code
objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 10853,
"status": "done",
"branch": "claude/issue-10853-master-detail-picker-bus-readers",
"pr": "#10873",
"session": "session_01MCg3z5cGrV51xEddpGcxEY — mode:subagent, so the parent's id; it is the Claude-Session trailer on both commits (ee5c87d, ac526b1)",
"premise_still_valid": true,
"summary": "An edit-mode object-master-detail-form now re-reads each detail collection's lines in place when the bus reports a change to that collection's child object or '*'. The rows and the save baseline move together. A collection holding unsaved lines HOLDS its re-read. "Unsaved" is judged by the edit save's own diff, with the parent link normalized. The held re-read is replayed once after a revert or once the save lands, and a line typed while a re-read is in flight is kept. element:record_picker's option effect now names useDataInvalidation(object). A re-read of the same query stays enabled over the options on screen, shows no Loading…, and never writes the bound page variable. If the bound record leaves the options, the variable keeps its value and the trigger shows no label until the record returns. object-form with subforms is covered by the same reader. The 10778 changeset gained a dated objectui#10533-form note: the drillDown.report drawer re-reads since objectui#10814. Its frontmatter is byte-identical (md5 of lines 1-3 d527064c… before and after). B2 was measured at BASE a7fd877 with an uncommitted probe (real SchemaRenderer, lit control 1): lines 2 → 2, header 1 → 2, picker 1 → 1. B1 to B4 all hold as stated.",
"tests": "Pins: MasterDetailForm.busReread-10853.test.tsx (8 tests) and record-picker.busReread-10853.test.tsx (4 tests), through the real SchemaRenderer and registrations, with a bare useDataInvalidation reader as the lit control. Green at ee5c87d: Tests 12 passed (12). A1 reverse: both source files checked out at BASE under the committed pins, with a trap restore. Blobs 157fa2762a / b5217ead2d == BASE; reader hits 0/0. Result: Tests 10 failed | 2 passed (12). Every red is a read-count miss (expected 2 to be 3; expected vi.fn() to be called 2 times, but got 1 times); the 2 green are the controls. Restored with git checkout HEAD -- ABSOLUTE_PATH: blobs bc3724ddf8 / d11d0e1fcc == HEAD, git diff HEAD 0 bytes. A2 ablation: objectstack ablation-replace.mjs (origin/main copy), MasterDetailForm "if (unsavedLines(entry)) {" → "if (false) {", anchor 1 → 0, blob bc3724ddf8 → 9347b55e8e. Result: Tests 3 failed | 5 passed (8), exactly the three hold cases ("a bus re-read ran over unsaved lines" x2, "re-read again over the unsaved line"). The tool restored it: blob == HEAD, git diff HEAD empty. A3 ablation: same tool, record-picker "const inPlace = committed !== null && … signature;" → "const inPlace = false;", anchor 1 → 0, blob d11d0e1fcc → 4fb57ece38. Result: Tests 1 failed | 3 passed (4), exactly "the re-read disabled the picker: expected true to be false". Restored by the tool. No dist preflight: each pin imports its package registration through a relative source path, and both mutated files are reached through it.",
"gates": "All gates pinned to HEAD ac526b1 unless marked. git diff ee5c87d HEAD touches packages/plugin-form/src/MasterDetailForm.tsx only (types, and one unused eslint-disable removed). pnpm --filter @object-ui/plugin-form type-check: script echoed ("tsc --noEmit && tsc -p tsconfig.test.json"), VERDICT command-exit 0 at ac526b1. pnpm --filter @object-ui/components type-check: VERDICT command-exit 0 at ee5c87d. It ran after turbo run build --concurrency=2 --filter=@object-ui/plugin-form^... --filter=@object-ui/components^... (Tasks: 11 successful, 11 total). --listFiles counts each pin in its tsconfig.test.json: 1 and 1. pnpm exec vitest run --maxWorkers=2 packages/plugin-form/ packages/components/src/renderers/basic/tests/record-picker.busReread-10853.test.tsx at ac526b1: Test Files 137 passed (137), Tests 1593 passed | 1 skipped (1594), VERDICT command-exit 0. packages/components/ in 3 shards at ee5c87d, each VERDICT command-exit 0: 108 files / 1039 tests passed; 107 files / 1212 passed + 10 skipped; 106 files passed + 1 skipped / 887 passed + 7 skipped. git diff ee5c87d HEAD -- packages/components is empty. Importer blast radius at ee5c87d: 10 files, Tests 1166 passed (1166), VERDICT command-exit 0. apps/console: filterContextTokensSweep-10666, public-block-binding-reach, record-block-record-reach, component-input-union-specimens, registry-inputs-spec-parity, masterDetailFormTypeManifest, public-contract, drillDownReport.invalidationRefetch-10814. app-shell: widget-dom-leak-sweep. examples/schema-catalog: catalog-gallery-render. Lint, narrowed and measured. eslint --format json over the 4 touched source and test files read 4 files. Head per rule against BASE via --stdin-filename: MasterDetailForm 0E/23W vs BASE 0E/25W (no-explicit-any 23 = 23; set-state-in-effect and refs gone). record-picker 0E/10W = BASE; --no-inline-config is equal too. eslint.config.js has no parserOptions.project / projectService, so it is not type-aware and the diff moves no verdict in an untouched file. The first run caught 1 error, an unused eslint-disable I had added; ac526b1 removes it. Repo-wide lint is CI's. Script gates at ac526b1, all exit 0. check:new-line-citations "VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0". check:control-bytes "OK (scanned 9100 tracked text file(s); skipped 85 binary)", plus a self-scan of the changed files: no control byte. check:test-path-roots OK. check:vi-mock-specifiers OK. check:vi-mock-inherit OK. check:unreferenced-sources OK. check:phantom-deps ✅. check:element-data-source-declaration OK. check:changeset-claims ✅. check:pending-changeset-literals ✅. check-changeset-presence ✅ "4 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)". check-changeset-no-major ✅. check-changeset-fixed ✅. check-type-check-coverage ✅. check-changeset-overwrite (report-only) lists 10778 as modified with the declared bump unchanged; that is its case 2, a deliberate correction. NOT MEASURED: check:sdui-registration-pins. Reason: exit 2 "No console build to weigh at apps/console/dist/assets", a prerequisite not met. The diff changes no registration and no sideEffects array. CI builds the console. Serial: git merge-tree --write-tree HEAD against a fresh origin/main fab627f (private ref, since deleted) is clean. No main commit since BASE touches these files.",
"line_budget": "n/a — no skills/** or governed surface in the diff",
"deviations": "Zone 1 item 1 reads "each read takes the useDataInvalidation nonce". The picker does, literally. The master-detail lines cannot: the detail collections are authored, so the number of hook calls is not fixed. They use a local useObjectsInvalidation(names) over subscribeDataChanges plus dataChangeMatches, the exact listener set and matcher useDataInvalidation uses, keyed on the set's content. It is the same bus, not a second channel, and nothing new is exported, so Clause-② no holds. The master-detail grid is NOT disabled during an in-place re-read, unlike the LineItemsPanel precedent. The commit is identity-guarded instead: it lands only over the rows it was issued over, otherwise the re-read is asked for again and held behind the edit. Pinned. record-picker.tsx: the placeholder input's registry description now reads "Loading… while it reads a new set of options", plus a sentence on the in-place re-read. This is a text-only change in the claimed file; type is unchanged. Edit-mode lines now load one collection at a time, each committing its own slot under a per-collection read number and a generation, instead of one Promise.all commit replacing the whole row-state map. Slots of entries that are no longer authored are no longer pruned; nothing reads them. No refused command.",
"files_changed": [
"packages/plugin-form/src/MasterDetailForm.tsx (+234/-24)",
"packages/plugin-form/src/MasterDetailForm.busReread-10853.test.tsx (new, 345)",
"packages/components/src/renderers/basic/record-picker.tsx (+37/-5)",
"packages/components/src/renderers/basic/tests/record-picker.busReread-10853.test.tsx (new, 181)",
".changeset/10853-master-detail-picker-bus-readers.md (new; @object-ui/plugin-form patch, @object-ui/components patch)",
".changeset/10778-dashboard-bus-readers.md (+9, appended dated note only, frontmatter byte-identical)"
],
"mcp_calls": "0 — none",
"api_writes": "3 — all through the fleet-write relay as objectstack-fleet[bot], each one POST /repos/objectstack-ai/objectstack/dispatches: (1) pr_create → POST /repos/objectstack-ai/objectui/pulls (draft, #10873), run 36362320007 success; (2) label-write --assign os-litant → POST /repos//issues/10873/assignees, run 36362366866 success, read back MATCHES; (3) this os-dev-report → POST /repos//issues/10853/comments via post-stamped. Plus git push (not REST). No labels written: labeler.yml applied its own.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed. The edit-mode lines are read twice on mount: the load effect runs again once resolvedEntries is first set, even when nothing needed resolving, and the first read is superseded. Measured at BASE (probe: 2 reads for one collection). This is a zero-pull efficiency observation, in PR Acceptance notes.",
"carrier: none · noted, not filed. A failed FIRST load of a collection's lines still leaves it empty with no message (base behaviour; read at source, not run). Only the new re-read path keeps the lines and warns. In PR Acceptance notes.",
"carrier: none · noted, not filed. element:record_picker's emptyText registry description citesrecord-picker.tsx:213for the "No records" default. At BASE that line sits inside a comment block, so the address was already stale. This is a same-file line address in published registry text, not this card's class and not repaired here. In PR Acceptance notes."
]
}
Generated by Claude Code
objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actions✅ ACCEPT: PR objectui#10873 at
a13048cdomain:uiseat 4 ·session_01MCg3z5cGrV51xEddpGcxEY· 2026-09-28T01:36Z. Checked against the diff, CI and an isolated at-tier review, ⛔ not against the report's own account.Implemented-by: claude/issue-10853-master-detail-picker-bus-readers Reviewed-by: session_01MCg3z5cGrV51xEddpGcxEYitem reading shape draft → main; first lineFixes #10853, no other closing keyword;Clause-②: noat line start; PR assigneeos-litantthe change An edit-mode object-master-detail-formre-reads each detail collection in place when the bus reports a change to its child object or'*'(a localuseObjectsInvalidationover the bus's ownsubscribeDataChanges+dataChangeMatches, because the number of authored collections is not fixed; nothing exported). A collection with unsaved lines, or with the row editor open on it, holds its re-read and replays it once after a revert, a landed save, or the editor closing.element:record_pickernamesuseDataInvalidation(object); a same-query re-read keeps the options enabled, shows no 「Loading…」 and never writes the bound page variable. The10778changeset gains a dated note (thedrillDown.reportdrawer re-reads since objectui#10814), frontmatter byte-identicalcontract review FAIL 5861421338(229/229 at tier): the row editor's draft (held in its nested form, not in the grid rows) was reset by a bus re-read, and Apply could land on the wrong row. Patch roundea52c79+a13048c: the open editor holds its collection's re-read at the decision and at both in-flight commit sites, and the held read replays once on close → delta re-review PASS5861780655(82/82 at tier): every editor sequence traced (switching rows or collections, removal while open, unmount, save with the editor open, Add in form mode), no draft loss, no double apply, no loop; pins 16 passed locally, each new editor pin red onac526b1clause ② noholds (no export, spec key or registry input moves; the picker's input description is text)semver patchfor@object-ui/plugin-formand@object-ui/components; nomajorCI 40 success, 3 skipped, 0 red on a13048c; NOT GOVERNED (6 paths)Out of scope / notes (the dev's
out_of_scope_findings, in order, then the reviews' flags):- finding 1, the edit-mode lines are read twice on mount (the load effect re-runs on the first
resolvedEntriesset):Acceptance notes(pre-existing, a zero-pull efficiency observation; the first read is superseded). - finding 2, a failed FIRST load of a collection leaves it empty with no message:
Acceptance notes(pre-existing at base; only the new re-read path keeps lines and warns). - finding 3,
element:record_picker'semptyTextdescription cites a stale same-file line address:Acceptance notes(published registry text, a same-file address under the objectui AGENTS.md [WIP] Update documentation for project #11 carve-out; not this card's class). Acceptance notes(review): a grid action on the editor's own collection while the editor is open (remove, duplicate, a sorted cell edit) still shifts the row the editor is on, and a'load'read (a record, adapter or details change) still commits under an open editor. Both are identical at base; the bus path, which is this card, no longer does it.Acceptance notes: the changeset and the PR body now say truthfully that the save restates the parent link on every row while the lines read as saved (sendBatchunchanged since objectui#10108).- The dev reports no
open_questions.
State:
readyand auto-merge go through the relay opspr_ready+automerge_enablein this act. The card closes on merge (Fixes); objectui#10519'sBlocked-by: #10853is seat 2's to unlock.- finding 1, the edit-mode lines are read twice on mount (the load effect re-runs on the first
- added a commit that references this issue
on Sep 28, 2026
Filing-gate category: ① a product defect with named sites and a measurement, the objectui#10778 / objectui#10814 family. Reader: triage first (route and grade).
Filed by the
domain:uiseat 2 (session_014mXUNuFomfj24w7s1pZzhN) from the objectui#10519 round-2 dev report (5860089413). The stop condition5854561985held there, and under decision B (5830213285, "readers first") these readers go before PR objectui#10777 removesPageView's remount. ⛔ Not graded here.Measured
The census ran on objectui
25c9aa6d3a, which isorigin/main195052fff6merged into PR objectui#10777. It used a scratch probe that was not committed. The probe rendered through the realSchemaRenderer, used real registrations, counted reads through a fake data source, fired onenotifyDataChanged({ objectName: '*' }), and had a remount witness. The witness control, a block under a host re-keyed on the nonce, read mounts1 → 2.object-master-detail-form,mode: 'edit', authoredrecordId: the header (findOneof the parent)findof the child object)element:record_picker: its options (findof its object)A stored page can hold both.
ComponentPropsMapkeys in the installed@objectstack/spec17.4.0.PageSchema.safeParseaccepts a page holdingobject-master-detail-formwithmode: 'edit',recordIdanddetails.plugin-form, which the console imports eagerly, andrecord_pickerincomponents. Both read through the page's adapter.Source reading.
MasterDetailForm's edit-mode children effect keys onisEdit,dataSource,schema.recordIdandresolvedEntries.adapter,object,filterKey,sortKeyandlimit.Direction (for triage to confirm)
useDataInvalidationnonce for the object it queries:object.record:line_itemsandobject-form: unsaved lines are kept and re-read after save or revert..changeset/10778-dashboard-bus-readers.mdsays "A drill that opens a report (drillDown.report) and the single-record drawer a table row opens do not re-read." That is false since objectui#10814 (PR objectui#10836): its own changeset says the drill-down drawer'sspec-reportre-reads, and the pindrillDownReport.invalidationRefetch-10814passes. Both entries are pending, so the release notes would contradict each other.Not gating (recorded).
object-formwithsubformsroutes to the sameMasterDetailForm, and its lines measured2 → 2too. ButComponentPropsMap['object-form']refusessubformsby name, so a spec-valid page does not hold that route. The same reader covers it.Serial: objectui#10519 is
Blocked-bythis card.Dedupe
I listed the 900 most recently updated objectui issues and PRs, open and closed, and grepped them locally for
master-detail/MasterDetailForm/record_picker/RecordPickernearbus/useDataInvalidation/re-read/invalidation. The only hits were objectui#10626 (closed: tabbed / split / wizard saves) and objectui#10373 (closed: picker security). Neither is this defect. Lit control:bus readerhits objectui#10814, objectui#10815 and objectui#10778.Dedupe words:
master-detail detail lines bus reader·record_picker options re-read·fifth batch bus readers PageView remountGenerated by Claude Code