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
console(form): a lookup with dependsOn keeps its value when the parent changes — an invoice saves a contact from a different account #11631
A clause of records-forms.cascading-multilevel-and-clear fails in the ObjectStack 17.7 pre-release checklist run objectstack-ai/objectstack#21784 (framework subject 316be321e, console pin 2e818d0b51ec). An independent verifier (VF3, RUNNER rule 7) confirmed it: P2 (confirmed by code; runner reproduced live twice). It predates the 17.6.0 console pin 31971ff1e; no open duplicate was found. Owner: objectui.
Reproduction
Open /_console/apps/com.example.showcase/showcase_invoice → New.
Account → Northwind; Contact → "Prospect 18" (a Northwind contact). Candidate request GET /data/showcase_contact?filter=["account","=",{NW}] → 26 rows (correct).
Switch Account to Contoso (pick directly, or remove the chip then pick).
Status → Draft; Create.
Expected: the contact clears on the account change. Actual: the chip stays; GET of the new invoice reads account:{Contoso id}, contact:{the Northwind contact}. With the account removed the field still shows "Prospect 18 · Select Account first".
Mechanism
packages/components/src/renderers/form/form.tsx:1982-2045 — the only cascade-clear effect skips every field whose type is not in CASCADE_OPTION_WIDGET_TYPES (:2004, no lookup member; app-shell/src/utils/paramToField.ts:200 says so) and requires static options (:2005-2006).
packages/fields/src/widgets/LookupField.tsx:640-654 only re-scopes the candidate query from dependentValues; its sole onChange(null) is the manual remove (:1012).
Fixture: examples/app-showcase/src/data/objects/invoice.object.ts:96 contact dependsOn: ['account']. The server reference gate checks existence only, so the client clear is the only guard. Predates 17.6.0 by code.
Done when
Changing (or clearing) a lookup's dependsOn parent clears a selection that no longer satisfies the dependent filter.
Path: forms that work | records-forms.cascading-multilevel-and-clear | P2
Triage: first grade — bug · priority:p2 · domain:ui · area:records · pm:queue. A dependent lookup clears when its dependsOn parent changes or clears
Triage: lands in packages/components/src/renderers/form/form.tsx (the cascade-clear effect) ⇒ domain:ui; rationale: the lookup's candidate scope already follows the parent; only the clear is missing.
Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-05T02:03Z. ⛔ Not a claim, ⛔ not a dispatch.
Why p2. The form saves a record whose references contradict each other (an invoice's contact from a different account), with no warning. The server checks only that a reference exists, so today the client clear is the only guard. The verifier confirmed it by code (VF3), and the runner reproduced it live twice.
Direction. The form's cascade-clear (form.tsx about :1982–:2045) covers lookup. Clearing on any change or clear of the parent satisfies the Done-when. Keeping a selection the new scope still admits is optional.
Not filed: a server-side check that a dependent reference matches its parent. That is a new server gate, and new gates default to no.
Claim: PM loop round 2
Session: session_015W8GBu6sBiqus2L2xjMsAL
Account: os-steve
Branch: claude/issue-11631-lookup-cascade-clear
Worktree: objectui-issue-11631
Domain: domain:ui
Seat: domain:ui#1
File surface: packages/components/src/renderers/form/form.tsx (the cascade-clear effect, about :1982–:2045), the tests beside it, and .changeset/11631-*.md (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: default tier (dispatch-gates --tier --repo objectstack-ai/objectui: no path-derived mandate; floor sonnet · default opus · ceiling fable)
Clause-②: no
Thread-read: 5986868484
Serial constraints cleared: none, read 2026-10-05T03:51Z. The open objectui PRs (#11639 and #11640app-shell, from this seat; #11069packages/cli; #11600 the release PR) touch no packages/components file. The in-flight objectui#11630 declares plugin-detail and possibly types.
Why Clause-②: no: a dependent lookup whose dependsOn parent changes or clears drops a value its new scope may not admit. That is a form behaviour fix, and nothing in the published accept set or public surface widens. The dispatch fences the exported CASCADE_OPTION_WIDGET_TYPES, which at least three other readers share, plus any new export, prop, type member or i18n key: a route that needs one stops and reports.
os-dev-report
{
"issue": 11631,
"status": "done",
"branch": "claude/issue-11631-lookup-cascade-clear",
"pr": "#11644",
"session": "session_015W8GBu6sBiqus2L2xjMsAL — the harness-stamped id of this run (subagent, so the parent seat's)",
"premise_still_valid": true,
"summary": "A lookup whose field-level dependsOn names a sibling is now cleared (null, or [] when multi-value) the moment any parent that scopes it takes a different value. The rule is a form.watch subscription in packages/components/src/renderers/form/form.tsx. Its parents are read from the same slot LookupField scopes its query by (dependentLookupParents, module scope, not exported), so the clear and the scope cannot disagree. A defaultValues reset (record landing after first paint, recordId swap, carried input) and an unnamed whole-record reset (resetOnSubmit, Cancel) re-base the comparison and clear nothing, so an opened record keeps its saved pair. A cleared lookup cascades to a lookup it scopes; the field that started a pass is never cleared by it; an empty lookup is never written. CASCADE_OPTION_WIDGET_TYPES is untouched, and no export, prop, type member or i18n key is added (Clause-② no). Landing site: form.tsx as expected, but beside the defaultValues reset rather than inside the cascade-clear effect (reason in deviations). Live before and after on objectstack main 75ddcd1b showcase: the card repro now saves contact null instead of the Northwind contact.",
"tests": "PREMISE (Zone 2, measured): (1) on the unchanged file the cascade-clear effect skips the lookup at the CASCADE_OPTION_WIDGET_TYPES test and the static-options test: leg A0, all seven clear rows red. (3) The served /meta/object/showcase_invoice contact field carries dependsOn ["account"] (field-level array; read live). The candidate query under Northwind is filter=["account","=",NORTHWIND_ID]. In the object form that array arrives on the stashed field metadata (FormField.field, built by plugin-form fromObjectSchema), not on the top-level FormField.dependsOn. (4) Every host that draws a form schema shares form.tsx (ObjectForm and its Drawer, Modal, Split, Tabbed and Wizard variants). The console FormPage and app-shell ActionParamDialog have their own state and keep the bug, see out_of_scope_findings. || UNIT: new packages/components/src/renderers/form/tests/form-dependent-lookup-clear-11631.test.tsx, 14 rows (6 clear, 8 keep). The field:lookup probe calls onChange with an id or null, the same channel LookupField uses. || REVERSE VERIFICATION + ABLATIONS on the final head fea2341, fix committed first. Each leg went through objectstack scripts/ablation-replace.mjs: anchor hit 1 to 0, blob changed, restore proven by blob == HEAD blob 9c47e4b8 and empty git diff HEAD. Direction predicted before running: the clear rows go red in the STALE direction, the keep rows stay green, and each guard reds only its own row. Observed exactly that. A0 (form.tsx at base c4c506b, marker count 0): Tests 7 failed | 7 passed (14). Red: the 6 clear rows plus the after-paint row (its live-afterwards half). Every failure is a retained value: expected prospect-18 / b1 / the two-id list to be null, empty string or []. A1 (subscription never established): the same 7 red. A2 (drop the resetInFlightRef re-base): 1 red, the carried-input row. A3 (drop the unnamed-notification re-base): 1 red, resetOnSubmit. A4 (drop the origin guard): 1 red, the lookups-naming-each-other row. A5 (drop the empty skip): 1 red, the empty lookup not written row. A6 (accept a non-array dependsOn): 1 red, the bare-name boundary. The same seven legs also ran at d7417c8 before the types-only refactor and the merge, with identical rows. Final git status clean. || GATES at fea2341 (merge of main 0abd4f9 into the branch): pnpm --filter @object-ui/components type-check EXIT=0 (tsc --noEmit && tsc -p tsconfig.test.json; --listFiles of the test project names the new file once). pnpm --filter @object-ui/components lint EXIT=0 (0 errors, 972 warnings; none falls in the added hunks after the types-only commit; the two any warnings in neighbouring lines are pre-existing). pnpm exec vitest run packages/components/src/renderers/form/ EXIT=0: Test Files 70 passed (70), Tests 483 passed | 17 skipped (500). At a3e25ff, before the types-only refactor 243dfa6 and the merge: pnpm exec vitest run packages/components/ EXIT=0, Test Files 360 passed | 1 skipped (361), Tests 3664 passed | 24 skipped (3688). The build closure pnpm --workspace-concurrency=2 --filter @object-ui/components^... build EXIT=0. Root checks at fea2341, all EXIT=0: check:new-line-citations (VERDICT 0 new citation(s)), check:control-bytes, check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:changeset-claims, check:pending-changeset-literals, check:shell-escape-residue, check:handler-key-reads, check:i18n-keys, check:unreferenced-sources, check-changeset-presence.mjs (2 source files of 1 released package, 1 changeset), check-changeset-no-major.mjs. Every locked run went through os-verify-lock.sh, VERDICT command-exit 0. || LIVE before and after (Zone 2 item 5, measured). Backend: objectstack origin/main 75ddcd1b in a private worktree, built with turbo (61 tasks, EXIT 0, under the lock), showcase served by objectstack dev --seed-admin --fresh on a private port. Consoles: vite from this branch (after) and from a detached base worktree at c4c506b (before), both proxied to that backend. Driven with headless Chromium. (a) Card repro: New invoice, Account Northwind, Contact Prospect 18, switch Account to Contoso, Status Draft, Create. BEFORE: the chip stays Prospect 18; the batch create body carries account Contoso and contact Prospect 18; read back: account Contoso, contact Prospect 18, whose own account reads Northwind. AFTER: the chip clears on the switch; the create body carries contact null (key present); read back: contact null. (b) Same but remove the Account chip: BEFORE shows Prospect 18 beside Select Account first; AFTER shows only Select Account first. (c) EDIT an API-created Northwind / Prospect 18 invoice, change only Tax Rate, Save (after): the contact shows on open, the update body is tax_rate 5 alone, and the read-back keeps account and contact. (d) EDIT the same invoice and switch Account to Contoso: BEFORE the update body is account alone and the stored contact stays the Northwind one; AFTER the update body is account plus contact null, and the read-back contact is null. Servers stopped by recorded port and PID; the --fresh database went with them.",
"mcp_calls": "0 — no MCP GitHub tool called",
"api_writes": "3 relay strokes (each one POST /repos/objectstack-ai/objectstack/dispatches answered 204, executed as objectstack-fleet[bot]): (1) pr_create = POST /repos/objectstack-ai/objectui/pulls (draft forced; read back 9671 bytes sent, 9671 stored, identical) — PR 11644; (2) label-write --assign os-steve = POST /repos//issues/11644/assignees (read back MATCHES, labels untouched); (3) this os-dev-report = POST /repos//issues/11631/comments via post-stamped.mjs. Not REST: git push of the branch (seven pushes, the first one the empty-branch probe). Reads used gh api GET on the card and PR only.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed — apps/console/src/components/FormPage.tsx (the form-view page, /forms/:name and /f/:slug) is a second renderer with its own values state. It feeds EXPANDABLE_FIELD_TYPES widgets dependentValues, but its onChange only sets the changed field, so a dependent lookup keeps a stale pick there. Read, not measured live: the only dependent lookup in the examples is showcase_invoice.contact, and no form view renders it, so no reach: line can be written. Dedupe words: FormPage dependent lookup clear, form view dependsOn stale, forms slug lookup parent change.",
"carrier: 承接者:无 · noted, not filed — packages/app-shell/src/views/ActionParamDialog.tsx: a lookup parameter with dependsOn is scoped by the dialog values (paramNeedsDependentValues), and updateValue writes only the changed parameter. Same defect shape, read only, no measured producer. Dedupe words: ActionParamDialog dependsOn clear, action param lookup stale parent.",
"carrier: 承接者:无 · noted, not filed — inline single-field edits (plugin-detail DetailSection / InlineFieldInput, plugin-grid ObjectGrid inline row edit) save a changed parent without touching its dependent lookup. Different interaction, observation only.",
"carrier: 承接者:无 · noted, not filed — a view-level dependsOn on a lookup (form-view FormField.dependsOn, copied by plugin-form sectionFields onto the runtime field top level) does not scope LookupField when object metadata is stashed, because the widget reads only the field-level array, and so it does not drive this clear either. The boundary row pins the form side. No producer measured authoring it, so it is not filed as class c. Dedupe words: view-level dependsOn lookup ignored, FormField dependsOn LookupField scope."
],
"gates": {
"head": "fea2341",
"components type-check": 0,
"components lint": 0,
"vitest packages/components/src/renderers/form/": 0,
"vitest packages/components/ (at a3e25ff)": 0,
"build closure components^... (at a3e25ff)": 0,
"ablation legs A0-A6 (predicted reds observed)": 0,
"check:new-line-citations": 0,
"check:control-bytes": 0,
"check:test-path-roots": 0,
"check:vi-mock-specifiers": 0,
"check:vi-mock-inherit": 0,
"check:vi-mock-override-shape": 0,
"check:changeset-claims": 0,
"check:pending-changeset-literals": 0,
"check:shell-escape-residue": 0,
"check:handler-key-reads": 0,
"check:i18n-keys": 0,
"check:unreferenced-sources": 0,
"check-changeset-presence.mjs": 0,
"check-changeset-no-major.mjs": 0,
"ci": "in_progress (not awaited)"
},
"line_budget": "n/a — no skills/** and no governed-surface file touched",
"files_changed": [
".changeset/11631-lookup-cascade-clear.md (+13, @object-ui/components patch)",
"packages/components/src/renderers/form/form.tsx (+147 −1)",
"packages/components/src/renderers/form/tests/form-dependent-lookup-clear-11631.test.tsx (+434, new)"
],
"deviations": [
"Landing site: inside form.tsx as expected, but as a form.watch subscription beside the defaultValues reset (keyed on dependentLookupsKey), not inside the cascade-clear effect. That effect is a passive ruleRecord effect that tests a value against a static option set. A lookup has none, so its rule is a transition, and only a subscriber notified inside the synchronous resetInFlightRef window can tell the record landing from an edit (A0/A1 and A2/A3 measure both halves). The option effect's comment points at the new rule. Stated in the PR body.",
"Merge once: origin/main had not moved at the first check. It then advanced to 0abd4f9 (app-shell) and was merged as fea2341, and the gates reran on the merged head. Afterwards main advanced again to 59917c4 (app-shell, 11625). Not merged a second time, per the merge-once instruction.",
"Declared narrowing: on the final head the vitest run is the form directory (70 files). The whole-package components run (361 files) is from a3e25ff. Between them came only a types-only commit (the carrier read as a record of unknown values, the probe props typed) and the merge, whose diff touches app-shell and a changeset only. The full farm is CI.",
"Commit trailers are the model-free pair (Claude-Session + Co-authored-by: Claude) per objectui AGENTS.md. The harness reminder asked for a model-named Co-Authored-By; the AGENTS.md rule takes precedence. Reported, not a defect.",
"Lock: one gates run and two final runs hit queue-timeout 99 while another seat (issue-11630 round1.sh, an app-shell vitest at about 350 percent CPU, not stalled) held the lock. Each was re-requested with its kept slot. The objectstack live build held the lock 5m12s.",
"pnpm install in the private objectstack worktree ran its prepare script (setup-git-hooks), which addresses the shared objectstack .git config. core.hooksPath read .githooks afterwards; the value before was not read.",
"No label written: the brief names none, and objectui path labels come from labeler.yml (the PR carries package: components and tests). skip-changeset is not used in objectui."
]
}
ACCEPT: PR objectui#11644, head fea2341. It lands when every check on this head is green
domain:ui execution seat 1 @ objectui · session_015W8GBu6sBiqus2L2xjMsAL (os-steve) · 2026-10-05T05:39Z. Reviewed against GitHub and origin/main, not against the report's prose.
Shape. The PR is a draft against main, and its assignee is os-steve. Its first line is Fixes #11631, the only line with a closing keyword next to an issue number. Clause-②: no is on its own line. Against main the diff is 3 files, +594/−1, inside the claim's surface: form.tsx, one pin file and the changeset.
The fence held.CASCADE_OPTION_WIDGET_TYPES is unchanged, and no export, prop, type member or i18n key is added. The parents that count are read from the same field-level dependsOn slot LookupField scopes its query by (dependentLookupParents, module scope), so the clear and the scope cannot disagree.
The landing site moved inside form.tsx, with its reason in the PR body. The seat accepts it. The cascade-clear effect tests a value against a static option set, and a lookup has none. Its rule is a transition, so it is a form.watch subscription beside the defaultValues reset, and the reset window re-bases the comparison instead of clearing. The subscription is set up only when the form has a dependent lookup, and it unsubscribes on cleanup.
The pins cover the Done-when and the fences. There are 14 rows: 6 clear and 8 keep. Opening a record, a value that lands after paint, a drawer record swap, resetOnSubmit/Cancel, an unrelated field, and an empty lookup all keep the value. The ablation legs A0–A6 each turned red only their own rows, in the stale direction, and every restore was proven by blob hash.
Live, both directions (objectstack main75ddcd1b showcase). Before: the card's repro saved a Contoso invoice with a Northwind contact. After: the chip clears on the switch, the create body carries contact: null, and it reads back null. Editing only Tax Rate keeps the saved pair. Switching Account on edit sends contact: null with it.
Changeset, sentence by sentence. It is @object-ui/components: patch. The "what does not clear it" list matches the keep rows, and the coverage paragraph matches the renderer set the dev measured. It also states the console form-view page is not changed. The Clause-②: no line is true.
Left as noted, not filed: the same defect shape in apps/consoleFormPage.tsx and app-shellActionParamDialog.tsx, and inline single-field edits. They were read, not measured, and no example renders a dependent lookup there, so there is no reach:.
Landing: on all-green checks on this head, ready, then auto-merge into the merge queue.
Merged through the merge queue as squash commit c000398 (one parent, 59917c4), an ancestor of origin/main. Its +/- lines are identical to the PR head fea2341 diffed from its merge base: 3 files, +594/−1.
Content check against the first parent, in packages/components/src/renderers/form/form.tsx:
reading
59917c4
c000398
dependentLookupParents
0
3
dependentLookupsKey
0
6
form-dependent-lookup-clear-11631.test.tsx exists
no
yes
CASCADE_OPTION_WIDGET_TYPES.has (control: the fenced shared list's readers are unchanged)
3
3
The card closed completed through the PR's Fixes line. It was the only issue closed in that window. pm:dispatched is removed in this stroke.
No downstream. The same defect shape in apps/consoleFormPage.tsx and app-shellActionParamDialog.tsx is noted in the ACCEPT (5988786652) without reach:, so it is not filed.
Activity
objectstack-fleet commentedon Oct 5, 2026
Path: forms that work | records-forms.cascading-multilevel-and-clear | P2
Triage: first grade —
bug·priority:p2·domain:ui·area:records·pm:queue. A dependent lookup clears when itsdependsOnparent changes or clearsTriage: lands in
packages/components/src/renderers/form/form.tsx(the cascade-clear effect) ⇒domain:ui; rationale: the lookup's candidate scope already follows the parent; only the clear is missing.Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-05T02:03Z. ⛔ Not a claim, ⛔ not a dispatch.form.tsxabout:1982–:2045) coverslookup. Clearing on any change or clear of the parent satisfies the Done-when. Keeping a selection the new scope still admits is optional.Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
Claim: PM loop round 2
Session:
session_015W8GBu6sBiqus2L2xjMsALAccount:
os-steveBranch:
claude/issue-11631-lookup-cascade-clearWorktree:
objectui-issue-11631Domain:
domain:uiSeat:
domain:ui#1File surface:
packages/components/src/renderers/form/form.tsx(the cascade-clear effect, about:1982–:2045), the tests beside it, and.changeset/11631-*.md(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: default tier(dispatch-gates --tier --repo objectstack-ai/objectui: no path-derived mandate; floor sonnet · default opus · ceiling fable)Clause-②: no
Thread-read: 5986868484
Serial constraints cleared: none, read 2026-10-05T03:51Z. The open objectui PRs (#11639 and #11640
app-shell, from this seat; #11069packages/cli; #11600 the release PR) touch nopackages/componentsfile. The in-flight objectui#11630 declaresplugin-detailand possiblytypes.Why
Clause-②: no: a dependent lookup whosedependsOnparent changes or clears drops a value its new scope may not admit. That is a form behaviour fix, and nothing in the published accept set or public surface widens. The dispatch fences the exportedCASCADE_OPTION_WIDGET_TYPES, which at least three other readers share, plus any new export, prop, type member or i18n key: a route that needs one stops and reports.Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
os-dev-report
{
"issue": 11631,
"status": "done",
"branch": "claude/issue-11631-lookup-cascade-clear",
"pr": "#11644",
"session": "session_015W8GBu6sBiqus2L2xjMsAL — the harness-stamped id of this run (subagent, so the parent seat's)",
"premise_still_valid": true,
"summary": "A lookup whose field-level dependsOn names a sibling is now cleared (null, or [] when multi-value) the moment any parent that scopes it takes a different value. The rule is a form.watch subscription in packages/components/src/renderers/form/form.tsx. Its parents are read from the same slot LookupField scopes its query by (dependentLookupParents, module scope, not exported), so the clear and the scope cannot disagree. A defaultValues reset (record landing after first paint, recordId swap, carried input) and an unnamed whole-record reset (resetOnSubmit, Cancel) re-base the comparison and clear nothing, so an opened record keeps its saved pair. A cleared lookup cascades to a lookup it scopes; the field that started a pass is never cleared by it; an empty lookup is never written. CASCADE_OPTION_WIDGET_TYPES is untouched, and no export, prop, type member or i18n key is added (Clause-② no). Landing site: form.tsx as expected, but beside the defaultValues reset rather than inside the cascade-clear effect (reason in deviations). Live before and after on objectstack main 75ddcd1b showcase: the card repro now saves contact null instead of the Northwind contact.",
"tests": "PREMISE (Zone 2, measured): (1) on the unchanged file the cascade-clear effect skips the lookup at the CASCADE_OPTION_WIDGET_TYPES test and the static-options test: leg A0, all seven clear rows red. (3) The served /meta/object/showcase_invoice contact field carries dependsOn ["account"] (field-level array; read live). The candidate query under Northwind is filter=["account","=",NORTHWIND_ID]. In the object form that array arrives on the stashed field metadata (FormField.field, built by plugin-form fromObjectSchema), not on the top-level FormField.dependsOn. (4) Every host that draws a form schema shares form.tsx (ObjectForm and its Drawer, Modal, Split, Tabbed and Wizard variants). The console FormPage and app-shell ActionParamDialog have their own state and keep the bug, see out_of_scope_findings. || UNIT: new packages/components/src/renderers/form/tests/form-dependent-lookup-clear-11631.test.tsx, 14 rows (6 clear, 8 keep). The field:lookup probe calls onChange with an id or null, the same channel LookupField uses. || REVERSE VERIFICATION + ABLATIONS on the final head fea2341, fix committed first. Each leg went through objectstack scripts/ablation-replace.mjs: anchor hit 1 to 0, blob changed, restore proven by blob == HEAD blob 9c47e4b8 and empty git diff HEAD. Direction predicted before running: the clear rows go red in the STALE direction, the keep rows stay green, and each guard reds only its own row. Observed exactly that. A0 (form.tsx at base c4c506b, marker count 0): Tests 7 failed | 7 passed (14). Red: the 6 clear rows plus the after-paint row (its live-afterwards half). Every failure is a retained value: expected prospect-18 / b1 / the two-id list to be null, empty string or []. A1 (subscription never established): the same 7 red. A2 (drop the resetInFlightRef re-base): 1 red, the carried-input row. A3 (drop the unnamed-notification re-base): 1 red, resetOnSubmit. A4 (drop the origin guard): 1 red, the lookups-naming-each-other row. A5 (drop the empty skip): 1 red, the empty lookup not written row. A6 (accept a non-array dependsOn): 1 red, the bare-name boundary. The same seven legs also ran at d7417c8 before the types-only refactor and the merge, with identical rows. Final git status clean. || GATES at fea2341 (merge of main 0abd4f9 into the branch): pnpm --filter @object-ui/components type-check EXIT=0 (tsc --noEmit && tsc -p tsconfig.test.json; --listFiles of the test project names the new file once). pnpm --filter @object-ui/components lint EXIT=0 (0 errors, 972 warnings; none falls in the added hunks after the types-only commit; the two any warnings in neighbouring lines are pre-existing). pnpm exec vitest run packages/components/src/renderers/form/ EXIT=0: Test Files 70 passed (70), Tests 483 passed | 17 skipped (500). At a3e25ff, before the types-only refactor 243dfa6 and the merge: pnpm exec vitest run packages/components/ EXIT=0, Test Files 360 passed | 1 skipped (361), Tests 3664 passed | 24 skipped (3688). The build closure pnpm --workspace-concurrency=2 --filter @object-ui/components^... build EXIT=0. Root checks at fea2341, all EXIT=0: check:new-line-citations (VERDICT 0 new citation(s)), check:control-bytes, check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:changeset-claims, check:pending-changeset-literals, check:shell-escape-residue, check:handler-key-reads, check:i18n-keys, check:unreferenced-sources, check-changeset-presence.mjs (2 source files of 1 released package, 1 changeset), check-changeset-no-major.mjs. Every locked run went through os-verify-lock.sh, VERDICT command-exit 0. || LIVE before and after (Zone 2 item 5, measured). Backend: objectstack origin/main 75ddcd1b in a private worktree, built with turbo (61 tasks, EXIT 0, under the lock), showcase served by objectstack dev --seed-admin --fresh on a private port. Consoles: vite from this branch (after) and from a detached base worktree at c4c506b (before), both proxied to that backend. Driven with headless Chromium. (a) Card repro: New invoice, Account Northwind, Contact Prospect 18, switch Account to Contoso, Status Draft, Create. BEFORE: the chip stays Prospect 18; the batch create body carries account Contoso and contact Prospect 18; read back: account Contoso, contact Prospect 18, whose own account reads Northwind. AFTER: the chip clears on the switch; the create body carries contact null (key present); read back: contact null. (b) Same but remove the Account chip: BEFORE shows Prospect 18 beside Select Account first; AFTER shows only Select Account first. (c) EDIT an API-created Northwind / Prospect 18 invoice, change only Tax Rate, Save (after): the contact shows on open, the update body is tax_rate 5 alone, and the read-back keeps account and contact. (d) EDIT the same invoice and switch Account to Contoso: BEFORE the update body is account alone and the stored contact stays the Northwind one; AFTER the update body is account plus contact null, and the read-back contact is null. Servers stopped by recorded port and PID; the --fresh database went with them.",
"mcp_calls": "0 — no MCP GitHub tool called",
"api_writes": "3 relay strokes (each one POST /repos/objectstack-ai/objectstack/dispatches answered 204, executed as objectstack-fleet[bot]): (1) pr_create = POST /repos/objectstack-ai/objectui/pulls (draft forced; read back 9671 bytes sent, 9671 stored, identical) — PR 11644; (2) label-write --assign os-steve = POST /repos//issues/11644/assignees (read back MATCHES, labels untouched); (3) this os-dev-report = POST /repos//issues/11631/comments via post-stamped.mjs. Not REST: git push of the branch (seven pushes, the first one the empty-branch probe). Reads used gh api GET on the card and PR only.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed — apps/console/src/components/FormPage.tsx (the form-view page, /forms/:name and /f/:slug) is a second renderer with its own values state. It feeds EXPANDABLE_FIELD_TYPES widgets dependentValues, but its onChange only sets the changed field, so a dependent lookup keeps a stale pick there. Read, not measured live: the only dependent lookup in the examples is showcase_invoice.contact, and no form view renders it, so no reach: line can be written. Dedupe words: FormPage dependent lookup clear, form view dependsOn stale, forms slug lookup parent change.",
"carrier: 承接者:无 · noted, not filed — packages/app-shell/src/views/ActionParamDialog.tsx: a lookup parameter with dependsOn is scoped by the dialog values (paramNeedsDependentValues), and updateValue writes only the changed parameter. Same defect shape, read only, no measured producer. Dedupe words: ActionParamDialog dependsOn clear, action param lookup stale parent.",
"carrier: 承接者:无 · noted, not filed — inline single-field edits (plugin-detail DetailSection / InlineFieldInput, plugin-grid ObjectGrid inline row edit) save a changed parent without touching its dependent lookup. Different interaction, observation only.",
"carrier: 承接者:无 · noted, not filed — a view-level dependsOn on a lookup (form-view FormField.dependsOn, copied by plugin-form sectionFields onto the runtime field top level) does not scope LookupField when object metadata is stashed, because the widget reads only the field-level array, and so it does not drive this clear either. The boundary row pins the form side. No producer measured authoring it, so it is not filed as class c. Dedupe words: view-level dependsOn lookup ignored, FormField dependsOn LookupField scope."
],
"gates": {
"head": "fea2341",
"components type-check": 0,
"components lint": 0,
"vitest packages/components/src/renderers/form/": 0,
"vitest packages/components/ (at a3e25ff)": 0,
"build closure components^... (at a3e25ff)": 0,
"ablation legs A0-A6 (predicted reds observed)": 0,
"check:new-line-citations": 0,
"check:control-bytes": 0,
"check:test-path-roots": 0,
"check:vi-mock-specifiers": 0,
"check:vi-mock-inherit": 0,
"check:vi-mock-override-shape": 0,
"check:changeset-claims": 0,
"check:pending-changeset-literals": 0,
"check:shell-escape-residue": 0,
"check:handler-key-reads": 0,
"check:i18n-keys": 0,
"check:unreferenced-sources": 0,
"check-changeset-presence.mjs": 0,
"check-changeset-no-major.mjs": 0,
"ci": "in_progress (not awaited)"
},
"line_budget": "n/a — no skills/** and no governed-surface file touched",
"files_changed": [
".changeset/11631-lookup-cascade-clear.md (+13, @object-ui/components patch)",
"packages/components/src/renderers/form/form.tsx (+147 −1)",
"packages/components/src/renderers/form/tests/form-dependent-lookup-clear-11631.test.tsx (+434, new)"
],
"deviations": [
"Landing site: inside form.tsx as expected, but as a form.watch subscription beside the defaultValues reset (keyed on dependentLookupsKey), not inside the cascade-clear effect. That effect is a passive ruleRecord effect that tests a value against a static option set. A lookup has none, so its rule is a transition, and only a subscriber notified inside the synchronous resetInFlightRef window can tell the record landing from an edit (A0/A1 and A2/A3 measure both halves). The option effect's comment points at the new rule. Stated in the PR body.",
"Merge once: origin/main had not moved at the first check. It then advanced to 0abd4f9 (app-shell) and was merged as fea2341, and the gates reran on the merged head. Afterwards main advanced again to 59917c4 (app-shell, 11625). Not merged a second time, per the merge-once instruction.",
"Declared narrowing: on the final head the vitest run is the form directory (70 files). The whole-package components run (361 files) is from a3e25ff. Between them came only a types-only commit (the carrier read as a record of unknown values, the probe props typed) and the merge, whose diff touches app-shell and a changeset only. The full farm is CI.",
"Commit trailers are the model-free pair (Claude-Session + Co-authored-by: Claude) per objectui AGENTS.md. The harness reminder asked for a model-named Co-Authored-By; the AGENTS.md rule takes precedence. Reported, not a defect.",
"Lock: one gates run and two final runs hit queue-timeout 99 while another seat (issue-11630 round1.sh, an app-shell vitest at about 350 percent CPU, not stalled) held the lock. Each was re-requested with its kept slot. The objectstack live build held the lock 5m12s.",
"pnpm install in the private objectstack worktree ran its prepare script (setup-git-hooks), which addresses the shared objectstack .git config. core.hooksPath read .githooks afterwards; the value before was not read.",
"No label written: the brief names none, and objectui path labels come from labeler.yml (the PR carries package: components and tests). skip-changeset is not used in objectui."
]
}
objectstack-fleet commentedon Oct 5, 2026
ACCEPT: PR objectui#11644, head
fea2341. It lands when every check on this head is greendomain:uiexecution seat 1 @ objectui ·session_015W8GBu6sBiqus2L2xjMsAL(os-steve) · 2026-10-05T05:39Z. Reviewed against GitHub andorigin/main, not against the report's prose.main, and its assignee isos-steve. Its first line isFixes #11631, the only line with a closing keyword next to an issue number.Clause-②: nois on its own line. Againstmainthe diff is 3 files, +594/−1, inside the claim's surface:form.tsx, one pin file and the changeset.CASCADE_OPTION_WIDGET_TYPESis unchanged, and no export, prop, type member or i18n key is added. The parents that count are read from the same field-leveldependsOnslotLookupFieldscopes its query by (dependentLookupParents, module scope), so the clear and the scope cannot disagree.form.tsx, with its reason in the PR body. The seat accepts it. The cascade-clear effect tests a value against a static option set, and a lookup has none. Its rule is a transition, so it is aform.watchsubscription beside thedefaultValuesreset, and the reset window re-bases the comparison instead of clearing. The subscription is set up only when the form has a dependent lookup, and it unsubscribes on cleanup.resetOnSubmit/Cancel, an unrelated field, and an empty lookup all keep the value. The ablation legs A0–A6 each turned red only their own rows, in the stale direction, and every restore was proven by blob hash.main75ddcd1bshowcase). Before: the card's repro saved a Contoso invoice with a Northwind contact. After: the chip clears on the switch, the create body carriescontact: null, and it reads back null. Editing only Tax Rate keeps the saved pair. Switching Account on edit sendscontact: nullwith it.@object-ui/components: patch. The "what does not clear it" list matches the keep rows, and the coverage paragraph matches the renderer set the dev measured. It also states the console form-view page is not changed. TheClause-②: noline is true.Left as noted, not filed: the same defect shape in
apps/consoleFormPage.tsxandapp-shellActionParamDialog.tsx, and inline single-field edits. They were read, not measured, and no example renders a dependent lookup there, so there is noreach:.Landing: on all-green checks on this head, ready, then auto-merge into the merge queue.
Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
Landed: PR objectui#11644 →
mainc000398, verified by contentdomain:uiexecution seat 1 @ objectui ·session_015W8GBu6sBiqus2L2xjMsAL(os-steve) · 2026-10-05T06:12Z.Merged through the merge queue as squash commit
c000398(one parent,59917c4), an ancestor oforigin/main. Its +/- lines are identical to the PR headfea2341diffed from its merge base: 3 files, +594/−1.Content check against the first parent, in
packages/components/src/renderers/form/form.tsx:59917c4c000398dependentLookupParentsdependentLookupsKeyform-dependent-lookup-clear-11631.test.tsxexistsCASCADE_OPTION_WIDGET_TYPES.has(control: the fenced shared list's readers are unchanged)The card closed
completedthrough the PR'sFixesline. It was the only issue closed in that window.pm:dispatchedis removed in this stroke.No downstream. The same defect shape in
apps/consoleFormPage.tsxandapp-shellActionParamDialog.tsxis noted in the ACCEPT (5988786652) withoutreach:, so it is not filed.Generated by Claude Code
fix(components): a dependent lookup clears when its dependsOn parent …