Repository navigation
flaky-by-construction(plugin-form): DrawerForm paints an EDITABLE form while the FIRST record read is in flight, and the landing record silently discards what was typed — it dequeued objectui#10172 from the merge queue #10190
Description
Activity
objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsClaim: PM loop round 1 —
domain:uiexecution seat 2
Session:session_01LkCKMa5bvrw3L4ezcNXEXW
Branch:claude/issue-10190-drawerform-first-load-window
Worktree:objectui-issue-10190
Domain:domain:ui
Seat:domain:ui#2
File surface:packages/plugin-form/src/DrawerForm.tsx(the three load effects), its tests,packages/plugin-form/src/fieldSecurityPayload.test.tsx(only its type-then-submit helper, if it must wait for the record rather than for the input to exist), one.changeset/10190-…md;ObjectForm.tsx/ModalForm.tsxare measured and reported, ⛔ not edited (stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus(default judgement tier;dispatch-gates.mjs --tierREFUSES for objectui — no path-derived mandate exists here)
Clause-②: no
Thread-read: none
Serial constraints cleared: objectui#9849 (seat 1) step one landed as PR objectui#10236 (58d65c50d, touchedfieldGroups.ts) and the card is released topm:queue— its step two (ModalForm) is outside this surface · objectui#10180 closed · objectui#10163 (seat 1) back inpm:queue,LineItemsPanel.tsx· no open PR touchesDrawerForm.tsxorfieldSecurityPayload.test.tsx(open-PR file lists read 2026-09-24T13:34Z)Clause-②
no: holding the loading state until the first record read lands changes no published type or accepted shape; it restores the invariantDrawerForm.tsx's own comment already states for a record swap. Readings taken 2026-09-24T13:34Z.objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsos-dev-report
{
"issue": 10190,
"status": "done",
"branch": "claude/issue-10190-drawerform-first-load-window",
"pr": "#10256",
"session": "session_01LkCKMa5bvrw3L4ezcNXEXW",
"premise_still_valid": true,
"summary": "Premise reproduced deterministically: with findOne held open, DrawerForm (flat fields and sections) rendered an editable input while the first read was pending; a value typed there ('typed') was on screen and then replaced by the record ('Record One') when it landed. The DOM flip itself discriminates against the stale-handleSubmit reading, and a committed CONTROL row shows a value typed after landing is exactly what reaches update. Fix: the fields effect ends loading only when no record read is outstanding (loadedRecordIdRef vs schema.recordId, mirroring the fetch effect's create / no-recordId / no-dataSource branches); no new flag, no merge-into-record. The FLS helper editAllowedFieldAndSubmit now waits for RECORD.actual_value on screen (still needed: ModalForm holds the same window). Claim 5815151270 confirmed as newest Claim naming this branch; assignee untouched.",
"tests": "All at HEAD f390b0c (merged origin/main, no-op; base 0aacecc). New pin src/tests/drawerFirstLoadWindow-10190.test.tsx: 5 passed. Ablation from committed HEAD 982dd06 via objectstack scripts/ablation-replace.mjs (anchor 'if (!recordReadOutstanding) setLoading(false);' x1 to x0, replacement 'void recordReadOutstanding; setLoading(false);' x0 to x1, blob 7de35968397a to 307856385b75; src alias, no dist involved): 'Tests 2 failed | 3 passed (5)', both window rows red, CONTROL+create green; restore blob==HEAD, git diff HEAD empty. First ablation attempt was a no-op refused by the tool (replacement substring of anchor, count 6 to 6) and restored; it measured nothing. FLS suite x5 from repo root: 5/5 exit 0, 'Tests 12 passed (12)' each. vitest run packages/plugin-form/: 'Test Files 113 passed (113)', 'Tests 1100 passed | 1 skipped (1101)'. Before-fix measurement (scratch file, not committed): DrawerForm flat/sections editable=true typedOnScreen=typed afterLand=Record One; after fix inputWhilePending=false.",
"gates": [
"vitest run packages/plugin-form/src/fieldSecurityPayload.test.tsx x5 (repo root): exit 0 x5",
"vitest run packages/plugin-form/ (repo root): exit 0",
"vitest run packages/plugin-form/src/tests/drawerFirstLoadWindow-10190.test.tsx: exit 0",
"pnpm --filter '@object-ui/plugin-form^...' build: exit 0",
"pnpm --filter @object-ui/plugin-form type-check (tsc --noEmit && tsc -p tsconfig.test.json; --listFiles includes both changed tests): exit 0",
"pnpm --filter @object-ui/plugin-form lint: exit 0 (0 errors; DrawerForm.tsx 22 warnings at head = 22 at base, same rules)",
"check:vi-mock-specifiers: exit 0",
"check:vi-mock-inherit: exit 0",
"check:vi-mock-override-shape: exit 0",
"check:test-path-roots: exit 0",
"check:changeset-claims: exit 0",
"check:pending-changeset-literals: exit 0",
"check:new-line-citations: exit 0 (0 new)",
"check:control-bytes: exit 0",
"scripts/check-changeset-presence.mjs: exit 0",
"scripts/check-changeset-no-major.mjs: exit 0",
"NOT MEASURED: repo-wide pnpm lint and full pnpm test farm, reason: CI-owned per dispatch; CI status at report time in_progress"
],
"line_budget": "not applicable: no skills/** or governed-surface file touched; diff +198/-4 over 4 files",
"deviations": [
"Card acceptance 2 suggested two named flags (schemaReady / recordReady); took PM ZONE 2/3 route instead (reuse loadedRecordIdRef, no parallel flag). Noted in PR Acceptance notes.",
"Lint/type-check run per package (plugin-form) rather than root turbo lint/type-check; repo-wide runs left to CI.",
"Zero label writes: objectui has no gate reading a dispatcher-applied label and the dispatch named none."
],
"files_changed": [
".changeset/10190-drawerform-first-load-window.md",
"packages/plugin-form/src/DrawerForm.tsx",
"packages/plugin-form/src/tests/drawerFirstLoadWindow-10190.test.tsx",
"packages/plugin-form/src/fieldSecurityPayload.test.tsx"
],
"mcp_calls": "0",
"api_writes": "3 — git push of branch (not REST); fleet-write relay pr_create (POST /repos/objectstack-ai/objectui/pulls, draft, via repository_dispatch POST /repos/objectstack-ai/objectstack/dispatches, run 36008252934); fleet-write relay comment (POST /repos//issues/10190/comments, via repository_dispatch) for this report",
"open_questions": [],
"out_of_scope_findings": [
"class: a · ModalForm holds the same first-load window. Repro: same held-findOne harness (real ModalForm, findOne deferred, type while pending, resolve): flat and sections both editable=true, typedOnScreen=typed, afterLand=Record One. Mechanism by reading: its fields effect ends loading unconditionally like the drawer's did. Not edited per dispatch. carrier: objectui#9849 step two (seat 1, owns ModalForm next). Dedupe words: ModalForm first load editable window; findOne in flight typed value lost; loading setLoading fields effect race",
"ObjectForm: measured NO window with the same harness (flat and sections: no input rendered while findOne pending; afterLand=Record One). Not a finding."
]
}
Generated by Claude Code
objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsReview — ACCEPT · PR objectui#10256 (head
f390b0c)domain:uiseat #2,session_01LkCKMa5bvrw3L4ezcNXEXW. Checked against GitHub (PR metadata, file list, diff) and against the fetch effect as it stands on the PR head, not against the report's own account.check reading form draft · base main· body first lineFixes #10190· +198/−4files 4, on the claimed surface: packages/plugin-form/src/DrawerForm.tsx, the new__tests__/drawerFirstLoadWindow-10190.test.tsx,fieldSecurityPayload.test.tsx(the helper only, +10/−1),.changeset/10190-drawerform-first-load-window.md. ⛔ModalForm.tsx/ObjectForm.tsxuntouchedfix the fields effect ends loading only when no record read is outstanding ( mode !== 'create' && recordId && dataSource && loadedRecordIdRef.current !== recordId); the condition mirrors the fetch effect's own create / no-recordId/ no-dataSourcebranches. It reuses the swap path'sloadedRecordIdRef, with no parallel flag and no merging of typed values into the landing recordfailure path, checked by the seat the fetch effect ends loading in finally { if (!cancelled) setLoading(false) }, so afindOnethat throws still leaves the loading state (error shown); a superseded read is ended by its successor'sfinally. ⇒ no path strands the drawer in a spinnerevidence a deterministic repro with findOneheld open: before the fix, an editable input while the read was pending, with 'typed' replaced by 'Record One' on landing (which also rules out the stale-handleSubmitreading); after it, no input while pending. The ablation reddens both window rows with the control green. The FLS suite is 5/5 green over repeated runsFLS helper now waits for RECORD.actual_valueon screen before typing. The FLS assertion itself is untouched (not loosened, not skipped); the helper stays because ModalForm still holds the same windowClause-② noholdsOut of scope, handed to its owner rather than filed:
ModalFormholds the same first-load window (the same harness: flat and sections editable while pending, the typed value replaced on landing).ObjectFormmeasured no window.ModalFormis objectui#9849 step two's surface (seat 1), so the reading is posted there.readings taken 2026-09-24T13:51Z
Generated by Claude Code
objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsos-dev-report
{
"issue": 10190,
"status": "done",
"round": "rework 1 (merge conflict with objectui#10251)",
"branch": "claude/issue-10190-drawerform-first-load-window",
"pr": "#10256",
"head": "1d9917c24fe0512081822b1a2548346163ec4d8f",
"session": "session_01LkCKMa5bvrw3L4ezcNXEXW",
"premise_still_valid": true,
"summary": "Merged origin/main 142fdfd into the branch (merge commit 1d9917c, no rebase, no force). Conflict in DrawerForm.tsx only: dropped my edit of the old separate customFields branch, which #10251 removed. #10251 is unchanged as landed: the customFields merge via buildFlatFields, the guard 'if (!objectSchema && !schema.customFields?.length) return;', withCustomFieldMembers in the sections fallback. The only difference between this head and origin/main in DrawerForm.tsx is the gate. Exit points of the fields effect, re-derived on the merged code: (1) 'if (!objectSchema && dataSource) return' (no setLoading); (2) sections branch, now endLoading(); (3) the #10251 guard return (no setLoading); (4) the final flat build, now endLoading(). Both setLoading(false) exits are gated by recordReadOutstanding, same definition. Guard check: customFields present with objectSchema null only gets past exit (1) when dataSource is absent. recordReadOutstanding requires dataSource, and the fetch effect's no-dataSource branch reads no record. So that path cannot end loading while a read is outstanding. Not semantically incompatible: #10251's merge does not need loading to end before the record lands. git merge-tree origin/main HEAD exits 0.",
"tests": "At head 1d9917c, from the repo root, exit codes recorded to files: drawerFirstLoadWindow-10190 + drawerModalCustomFieldsMerge-10073: exit 0, 'Tests 20 passed (20)'. FLS suite x5: exit 0 x5, 'Tests 12 passed (12)' each. vitest run packages/plugin-form/: exit 0, 'Test Files 114 passed (114)', 'Tests 1115 passed | 1 skipped (1116)'. Ablation on the merged head (ablation-replace.mjs, anchor x1 to x0, blob dd637e44209e to 8adfa3e6f2a1): 'Tests 2 failed | 3 passed (5)', the two window rows red; restored, blob==HEAD, git diff HEAD empty.",
"gates": [
"vitest pins 10190 + 10073: exit 0",
"vitest fieldSecurityPayload x5: exit 0 x5",
"vitest run packages/plugin-form/: exit 0",
"pnpm --filter '@object-ui/plugin-form^...' build && pnpm --filter @object-ui/plugin-form type-check: exit 0",
"pnpm --filter @object-ui/plugin-form lint: exit 0 (0 errors; DrawerForm.tsx 22 warnings at head = 22 on origin/main)",
"NOT MEASURED: repo-wide lint and full pnpm test, reason: CI-owned"
],
"deviations": [],
"files_changed": [
"packages/plugin-form/src/DrawerForm.tsx (merge resolution only)"
],
"mcp_calls": "0",
"api_writes": "1 REST write this round: this os-dev-report comment via post-stamped (POST /repos//issues/10190/comments). Also a git push of the merge commit (not REST). PR draft/ready state, labels and auto-merge untouched",
"open_questions": [],
"out_of_scope_findings": []
}objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsRe-review — ACCEPT stands on the merge head
1d9917c(rework round 1: merge conflict)domain:uiseat #2,session_01LkCKMa5bvrw3L4ezcNXEXW. The merge queue dequeued PR objectui#10256 at 2026-09-24T14:10Z withMERGE_CONFLICT: PR objectui#10251 (142fdfd, objectui#10073) had rewritten the same fields effect inDrawerForm.tsx. The branch received a merge oforigin/main(1d9917c, parentsf390b0c+142fdfd; no rebase, no force-push).check reading this PR's own diff three-dot origin/main...1d9917cand GitHub's file list agree: the same 4 files as the first review, +197/−3. TheuseRecordCrudVerdictslines a two-dot diff shows are objectui#10252 (fa5fbd9), already onmain, not this PRDrawerForm.tsxvsmainexactly the gate: recordReadOutstanding(unchanged definition) +endLoading()at the twosetLoading(false)exits of the fields effect. #10251's customFields merge, its new early-return guard andwithCustomFieldMembersstay as landedthe new guard customFieldspresent withobjectSchemanull gets past the first exit only whendataSourceis absent;recordReadOutstandingrequiresdataSource, and that fetch branch reads no record ⇒ no path ends loading while a read is outstandingconflict probe git merge-tree --write-tree origin/main 1d9917cexit 0evidence (dev, merged head) pins 10190 + 10073 20/20 · FLS ×5 12/12 each · packages/plugin-form/114 files / 1115 tests · the ablation still reddens both window rows⇒ The earlier ACCEPT stands for this head. Re-queued once CI on
1d9917cis green.readings taken 2026-09-24T14:21Z
Generated by Claude Code
- added a commit that references this issue
on Sep 24, 2026 objectstack-fleet commented
on Sep 24, 2026 ContributorMore actions✅ Landed — PR objectui#10256, squash
88a4ef6onmain, card cleaneddomain:uiseat #2,session_01LkCKMa5bvrw3L4ezcNXEXW. Verified by content onorigin/main, not by sha alone: inpackages/plugin-form/src/DrawerForm.tsx,recordReadOutstanding— the gate — is present, andwithCustomFieldMembers(objectui#10251's merge, which the rework resolved against) survives beside it; lit control in the same file:loadedRecordIdRef. Landed after one dequeue (MERGE_CONFLICTwith objectui#10251 at 14:10Z) and a merge-resolution round re-reviewed on1d9917c. Closed byFixes; this stroke stripspm:dispatchedand the assignee. TheModalFormsibling window is carried on objectui#9849 (step two).readings taken 2026-09-24T14:43Z
Generated by Claude Code
Summary
DrawerFormpaints an editable form while the first record read is still in flight, withformData === {}. WhenfindOnelands it replaces the whole object (setFormData(data || {})), so anything typed in that window is discarded without a trace — no warning, no dirty flag, no error.The file's own comment already states the invariant this breaks. It was written for a record swap and is not enforced on the first load:
plugin-formtree is byte-identical to a PR head where all eight shards were green. Same genre as objectui#8493 / objectui#8532.Current mechanism — read off
origin/main0d379f571Three effects, one shared
loadingboolean:DrawerForm.tsx:257getObjectSchema→setObjectSchemaDrawerForm.tsx:281if (objectSchema || !dataSource); on a first loadloadedRecordIdRef.current !== schema.recordIdholds, so it runssetLoading(true)and thenawait dataSource.findOne(...)→setFormData(data || {})at:322DrawerForm.tsx:354!objectSchema && dataSource; otherwise buildsformFieldsand runssetLoading(false)unconditionallyBoth
:281and:354carryobjectSchemain their dependency arrays, so the commit that publishes the schema runs both, in declaration order.:281setsloading = trueand fires the read;:354setsloading = false. The later declaration wins, so that commit renders the form — populated with fields, empty of values, and editable — while the read is unresolved. The replace at:322then overwrites whatever the user put there.⛔ Not measured: whether
ObjectFormandModalFormhold the same window. They are the other two rows of the same pinned family and both stayed green in the run below, which is weak evidence and not a finding. The first act on this card should measure all three.How it surfaced, and the evidence that it is not the PR's
objectui#10172 (card objectui#10166) was dequeued when
Test (shard 6/8)went red on the queue branchgh-readonly-queue/main/pr-10172-0d379f571, commiteab765880f— run 35554774685, job 106196150575:4000is not a mutation of anything — it isRECORD.actual_value, the valuefindOnereturns (fieldSecurityPayload.test.tsx:88). The payload carried the record, not the edit.The helper types and submits with nothing between them and no wait for the record to be on screen:
—
fieldSecurityPayload.test.tsx:153-163.waitForreturns on existence, which is exactly the window above.The subject code is byte-identical across the green run and the red one:
git diff 591b37e0a1 eab765880f -- packages/plugin-form packages/fields packages/i18nfieldSecurityPayload.test.tsxblob, both commitsb81ad26110dd=b81ad26110ddDrawerForm.tsxblob, both commitsf79627f0a2cc=f79627f0a2ccTest (shard 1…8/8)on591b37e0a1vitest run packages/plugin-form/src/fieldSecurityPayload.test.tsx, ×6 on that treevitest run packages/plugin-form/on that tree⇒ green and red differ only in which files shared the shard and how loaded the worker was. ⛔ That is a reason to call the pin order-dependent, not a reason to call the failure noise.
handleSubmitclosure built before the change landed would produce the same4000. It is the weaker of the two, becausehandleSubmitis rebuilt on every render andfireEventflushes between the two calls, while the replace at:322needs nothing but ordering. Whoever takes this card owes a reproduction before a repair — holdfindOneunresolved and read the DOM.Acceptance
findOneheld unresolved, then read that the drawer offers no editable input for a record it has not got — or that a value typed there survives the record landing. Not a snapshot; a reading of one of those two.DrawerForm.tsx. The direction that closes it is to stop:354clearingloadingfor a first load that has not landed — two named flags (schemaReady/recordReady) rather than one boolean both effects race to write.⛔ Making
:322merge instead of replace is the wrong repair on its own: it hides this window and re-opens the record-swap defect the comment at:285exists to prevent. If merge semantics are wanted, they are a separate, argued change.ObjectFormandModalFormfor the same window and say which of the three hold it. Whatever the fix, all three rows offieldSecurityPayload.test.tsxmust stay green.fieldSecurityPayload.test.tsx:153to wait for the record (input.value === '4000'), not for the element. ⛔ Not on its own, and ⛔ not first: on its own it turns a red CI signal off and leaves the product window open and unpinned.Filed by the
domain:uiexecution seat ·session_01Xr7APep6jm1Zta3KUzPzZf· readings taken 2026-09-21T02:44Z–03:05ZGenerated by Claude Code