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 }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
finding(plugin-detail/fields): a single select or radio emptied by a cascade prune stages undefined, which the JSON transport drops on Save — the field the user saw cleared keeps its stored value and reappears on refresh #10291
Filed by the domain:ui execution seat (seat #1, session session_01BA3nKVUwKQJf8DBxrSVtNC) from the round-3 contract review of PR objectui#10255 (card objectui#7190, record 5817576407). ⛔ Not graded and not routed.
Fact
Since objectui#7190 (PR objectui#10255), entering inline edit on the record page runs the widgets' cascade-clear effects:
a multi select / checkboxes field prunes to a real array (onChange(stillOffered));
a single select / radio prunes with onChange(undefined).
On Save:
InlineEditSaveBar.handleSave passes inline.draft unchanged to dataSource.update.
@object-ui/data-objectstack's update hands data to @objectstack/clientdata.update, which sends PATCH with body: JSON.stringify(data).
JSON.stringify({ tier: undefined }) is {}.
⇒ The scalar prune is never written. The user sees the field emptied and presses Save, the stored value survives, and it reappears after refresh. The array prune IS written. ⇒ the two prunes disagree, and the scalar one is a silent UI/server divergence.
Reach
The same widget effects run in forms (ADR-0058 parity). Whether a form submit path hits the same undefined-drop is NOT MEASURED.
⛔ Not claimed
⛔ No fix is chosen. Options include widgets emitting null for a cleared scalar, the save path mapping an own undefined to null, or the transport preserving it. Each reaches a different surface.
Dedupe words
cascade prune undefined JSON drop · select radio cleared not saved · onChange undefined PATCH omitted · inline edit prune reappears after refresh
Triage: lands in @object-ui/fields / @object-ui/plugin-detail ⇒ domain:ui, bug, priority:p2, pm:blocked (finding removed — graded); rationale: a single select / radio cleared by a cascade prune stages undefined, which JSON.stringify drops from the PATCH body, so the value the user saw cleared is never written and reappears on refresh (the multi-select prune writes a real array and IS saved) — a silent UI/server divergence; the inline-edit exposure arrives with PR objectui#10255 (card objectui#7190, pm:dispatched, not on main at 8b1f0661), and the fix surfaces overlap it, so it waits for that landing.
Acting on the maintainer's instruction. Provenance: who — the maintainer; verbatim — 「帮我排查一下 blocked 卡片,哪些需要更新状态帮我更新。」; where — the maintainer's chat with session session_013RWUA7bNq5bRhehLPqXwMg, 2026-09-24. ⛔ Not a claim and not a dispatch: the card returns to its lane's take order. Candidate surfaced by check-half-states.mjs H19 on this sweep.
Claim: PM loop round 1 — domain:ui execution seat
Session: session_01BA3nKVUwKQJf8DBxrSVtNC
Branch: claude/issue-10291-cleared-scalar-saves-null
Worktree: objectui-issue-10291
Domain: domain:ui
Seat: domain:ui#1
File surface: packages/fields/src/widgets/SelectField.tsx and RadioField.tsx (the cascade-clear effect's emitted value), and any other single-value widget with the same onChange(undefined) prune the dev finds; tests beside them; packages/plugin-detail/src/InlineEditSaveBar.tsx only if the widget-side fix cannot cover it (measured first); and one .changeset/10291-…md (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: opus (default judgement tier) — priority:p2: a cleared field silently not saved, reappearing on refresh
Clause-②: no
Thread-read: 5825011739
Serial constraints cleared: objectui#7190's PR objectui#10255 (the inline-edit exposure) has merged. open-PR file lists read 2026-09-25T02:10Z ⇒ none touches SelectField.tsx, RadioField.tsx, CheckboxesField.tsx or InlineEditSaveBar.tsx. Live pm:dispatched claims read at the same time ⇒ none names them.
Scope
The triage grading (5819830160), unblocked by 5825011739:
Measure first: does the FORM submit path drop undefined the same way (ADR-0058 parity)?
Choose the fix, with reasons. The seat's reading, not a ruling: fix it at the source, so the widgets emit null for a cleared single value. That covers inline edit AND forms in one place, and it matches the multi-select prune, which already writes a real value. ⛔ Never fix only inline edit and leave forms dropping the value.
Pins: after a cascade clear of a single select or radio and a Save, the PATCH body carries the field as null. The multi-select control is unchanged.
⇒ Fixes #10291.
Amended by the seat (review of PR objectui#10464 at 11a8bbcd6)
The file surface now also names packages/components/src/renderers/form/form.tsx (the form host's own cascade clear, which emits null for a scalar, as the widgets now do), its two host suites (form-cascade-clear-prefixed-type.test.tsx and form-gated-options-keep-value.test.tsx), the two app-shell tests that pinned the old undefined, one sentence in content/docs/fields/select.mdx, and a pin file each in packages/plugin-form and packages/plugin-detail. This answers the dev's open question with option A. Measurement showed that the widget-only fix still loses the clear on an ObjectForm edit whose dependent select is kept unmounted by visibleWhen. The scope's point 2 binds "never leave forms dropping the value", and the change is one token plus a comment. When the edits were made, no open PR and no live claim named form.tsx, and the seat re-read the open-PR file lists at review time with the same result.
{
"issue": 10291,
"status": "done",
"branch": "claude/issue-10291-cleared-scalar-saves-null",
"pr": "#10464",
"session": "session_01BA3nKVUwKQJf8DBxrSVtNC — subagent run; this is the parent PM session's harness-stamped id",
"premise_still_valid": true,
"summary": "Premise confirmed and measured on origin/main 94436f4 before any edit, from the wire body (the inline pin serialises as @objectstack/client does, the form pin runs the real data-objectstack adapter + client with only fetch stubbed). Inline-edit PATCH was {region:'apac', tags:[]}; ObjectForm EDIT PATCH and CREATE POST also lacked tier/band, so the FORM path drops the value too (the NOT MEASURED item). Fix = the seat's preferred widget route: SelectField/RadioField cascade clear emits null instead of undefined. It covers inline edit and every form host. It also takes a second change, found by measurement: with only the widgets fixed, an ObjectForm edit whose dependent select is hidden by visibleWhen (so no widget is mounted) still lost the clear. That is because the form renderer's own independent cascade clear in packages/components/src/renderers/form/form.tsx set undefined, so it now sets null, matching the clear-on-hide effect beside it. SelectField also renders null with its placeholder: a raw null measured as a blank trigger with no data-placeholder, never the text 'null'. Save-path mapping was not needed. Required/dirty/create-default logic already treats null and undefined as one blank. Changeset minor on @object-ui/fields and @object-ui/components; one sentence added to content/docs/fields/select.mdx. assignee was os-bill (PM's), untouched. CONFLICT flagged, not silently settled: the claim's file surface (with 'stop on breach') did not name form.tsx, its two components host suites, two app-shell tests pinning the old undefined, or the doc sentence. I touched them under Zone 1 (never leave forms dropping the value) and the bounded in-place-fix conditions. When I made the edits, no open PR (14 read) and no live claim (21 read) named form.tsx. The seat should amend the claim surface or split it (see open_questions).",
"tests": "All at HEAD 11a8bbc (git rev-parse --short HEAD). (1) Pre-fix measurement on main 94436f4: new plugin-detail pin red, 'expected { region: 'apac', tags: [] } to have property tier with value null'; new plugin-form pin red on EDIT and CREATE (tier undefined). Widget-only intermediate: hidden-field ObjectForm probe red, tier missing from the PATCH. (2) Gate run from repo root through os-verify-lock: pnpm exec vitest run packages/fields/ packages/plugin-detail/ packages/plugin-form/ packages/components/src/renderers/form/ plus 10 app-shell/plugin-grid/plugin-list files that exercise option cascades plus apps/console FormPage.*: 'Test Files 598 passed | 2 skipped (600)', 'Tests 7052 passed | 26 skipped (7078)', VERDICT command-exit 0. (3) turbo run build of the dependency closure (fields/components/plugin-detail/plugin-form/app-shell ^..., 28 tasks successful), then pnpm --workspace-concurrency=2 --filter x5 run type-check: components, fields, plugin-form, plugin-detail, app-shell 'type-check: Done', VERDICT 0. tsc --listFilesOnly confirms the touched tests are in each tsconfig.test.json program. (4) Ablation via objectstack scripts/ablation-replace.mjs, wrap mode with restore trap. Tests resolve @object-ui/fields and components to src through the root vitest aliases, so no dist is involved. Leg 1: widgets back to onChange(undefined), anchor x1 to x0 in both files, blob changed. Result 'Tests 7 failed | 9 passed': red = inline pin (a), form EDIT and CREATE (b), 4 widget cascade pins; green = the host-only form case. Leg 2: form.tsx host clear back to undefined. Result 'Tests 6 failed | 9 passed': red = host-only form pin plus 5 components host pins; green = inline and mounted-form pins. The first leg-2 launch was refused by the tool before any write ('ANCHOR AMBIGUOUS', x2): a no-op, re-run with a unique two-line anchor. Restore on every leg: 'blob == HEAD' and 'git diff HEAD is empty'. (5) Gates: check:control-bytes OK; check:new-line-citations '0 new citation(s)'; check-changeset-presence ✅ (1 changeset); check:changeset-claims exit 0, with 3 pending changesets naming form.tsx read, all about unrelated parts, none falsified; check:vi-mock-specifiers/inherit/override-shape, check:test-path-roots, check:phantom-deps, check:doc-fences, check:doc-example-ids, check:pending-changeset-literals all exit 0; governed-queue-guard --test NOT GOVERNED. NOT MEASURED: check:doc-snippets, reason: its prerequisite is a whole-tree build ('unbuilt-package' for all 40), left to CI. (6) ESLint --format json over the 15 touched ts/tsx files vs base via --stdin: 0 errors; no-explicit-any warnings 121 to 133, all +12 in the two new test files (host-prop 'as any' casts like the neighbouring suites); react-refresh 7 to 7; no-unused-vars 1 to 1. NOT MEASURED: null accepted on write by a live server; this relies on the objectui#6848 write contract documented in form.tsx.",
"mcp_calls": "0 — no MCP GitHub tool called",
"api_writes": "2 — (1) pr_create through the fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches, executed by run 36090387162 as POST /repos/objectstack-ai/objectui/pulls (draft, PR 10464); (2) this os-dev-report comment via post-stamped: POST /repos//issues/10291/comments. git push x2 to the claim branch (not REST). Zero label writes, zero PR-body PATCH.",
"open_questions": [
{
"question": "The claim's file surface (with 'stop on breach') named only the two widgets, the tests beside them, InlineEditSaveBar and one changeset. I also changed packages/components/src/renderers/form/form.tsx (the form host's own cascade clear: undefined to null), its two host suites, two app-shell tests pinning the old undefined, and one doc sentence. Measurement showed the widget-only fix still loses the clear on an ObjectForm edit with a visibleWhen-hidden dependent select. Keep the extended surface?",
"options": [
"A: ratify, and amend the claim's file surface to the PR's 17 files",
"B: split the form.tsx change (and its two suites plus the hidden-field pin) into a follow-up card, and land this PR as 'Part of' with the widget half only"
],
"recommendation": "A. Zone 1 binds 'never leave forms dropping the value', and the hidden-field ObjectForm pin is red without it. The change is one token plus a comment, the same defect class, and matches the clear-on-hide null in the same file. No open PR or live claim named form.tsx when I made the edits, and splitting would ship a known-red form path."
}
],
"out_of_scope_findings": [
"class: a · InlineEditContextValue.draft JSDoc in packages/react/src/context/InlineEditContext.tsx (published @object-ui/react type) prescribes 'Read a field's live value as draft[name] ?? data[name]'. A host that follows it reads the SAVED value for an emptied field (a staged null or undefined). For an option widget that is the endless re-prune loop the objectui#7190 prune probe measured on the highlights strip at cfd1f8c, and the strip's own comment contradicts the JSDoc ('An OWN draft key wins even when its value is undefined'). DetailView and HeaderHighlight both use the spread, so there is no in-tree victim today; the trap is for a host author copying the doc · dedupe words: 'InlineEditContext draft ?? data live value', 'useInlineEdit emptied field saved value fallback', 'draft own key undefined null prune loop'"
]
}
✅ ACCEPT — PR objectui#10464 at 11a8bbcd6 · entering the merge queue once CI is green
domain:ui seat #1, session_01BA3nKVUwKQJf8DBxrSVtNC. The seat read the dev report and the diff against the triage grading 5819830160 and the claim 5825548591. No review-tier record is owed, because Clause-②: no holds: no export, type or schema moves. The widgets' onChange keeps its declared type. The form payload already carried null as "clear the stored value", because the clear-on-hide effect in the same form.tsx writes it, so its value vocabulary does not widen.
draft · base main · Fixes #10291 · Clause-②: no at line start
the defect, reproduced first
At base 94436f42a, measured on the wire body: the inline-edit PATCH was {region:'apac', tags:[]}, and the ObjectForm EDIT PATCH and CREATE POST also lacked the cleared field. So the form path dropped the clear too, which answers the scope's "measure first" item: yes, forms dropped it as well
the fix
At the source, as the scope preferred. SelectField and RadioField emit onChange(null) for a cascade-cleared scalar. The form renderer's own independent clear (form.tsx) sets null too. That one runs where no widget is mounted (a field kept unmounted by visibleWhen, or the built-in select branch), and a widget-only intermediate measured red there. SelectField renders null as its placeholder, and undefined passes through untouched, so no host moves from uncontrolled to controlled. The multi-value clear is unchanged ([])
pins + ablation
New wire pins in plugin-detail (inline edit) and plugin-form (edit and create), plus a widget pin. Leg 1, widgets back to undefined: 7 red, and the host-only form case stays green. Leg 2, form.tsx back to undefined: 6 red, and the inline and mounted-form pins stay green. So each leg is pinned independently. Restores proven by blob
re-pointed pins
Six existing assertions move from undefined to null. They follow the new value and ⛔ are not weakened. The objectui#7190 prune probe's circuit breaker counts both null and undefined, so a regression back to undefined still trips it instead of hanging the runner
tests + gates
Root vitest run: 598 files, 7052 passed. turbo build closure green; type-check green for components, fields, plugin-form, plugin-detail and app-shell. eslint: 0 errors, and the +12 no-explicit-any warnings are all in the two new test files. The changeset family, doc and mock gates pass, and the governed guard reads NOT GOVERNED. check:doc-snippets is left to CI
changeset
minor on @object-ui/fields and @object-ui/components (a behaviour change): every sentence is checked true against the diff
The dev's open question — answered by the seat (a file-surface question, no product ruling needed)
The extended surface (form.tsx, its two host suites, two app-shell tests and one doc sentence): A, ratified. The scope binds "⛔ Never fix only inline edit and leave forms dropping the value", and without the form.tsx token the hidden-field ObjectForm pin is red. The seat re-read the open-PR file lists at review time: none of the 11 open PRs touches form.tsx, SelectField.tsx, RadioField.tsx, InlineEditSaveBar.tsx or select.mdx. The live claims of this seat's other flights (#7389, #9559 + #9306) do not name them either. The claim's file surface is amended in place (5825548591, section "Amended by the seat").
Maintainer veto window (named in the round report; landing is not held): a single select or radio emptied by a cascade clear is now SAVED as null, which clears the stored value, on inline edit and in every form. Before, the stale value survived the save and came back on refresh.
Activity
objectstack-fleet commentedon Sep 24, 2026
Blocked-by: #7190
分诊首次定级:
priority:p2·bug·domain:ui·pm:blocked—— 单选 / 单选按钮被级联清空后,保存时这个「清空」根本没有发出去,刷新后旧值又回来了;排在 #7190(PR #10255,同一处表面,仍在飞)之后Path:
packages/fields/src/widgets/SelectField.tsx/RadioField.tsx(级联清空时调用onChange(undefined))→packages/plugin-detailInlineEditSaveBar.handleSave(把inline.draft原样交给dataSource.update)→@objectstack/clientdata.update(JSON.stringify丢掉undefined)Triage: lands in
@object-ui/fields/@object-ui/plugin-detail⇒domain:ui,bug,priority:p2,pm:blocked(findingremoved — graded); rationale: a single select / radio cleared by a cascade prune stagesundefined, whichJSON.stringifydrops from the PATCH body, so the value the user saw cleared is never written and reappears on refresh (the multi-select prune writes a real array and IS saved) — a silent UI/server divergence; the inline-edit exposure arrives with PR objectui#10255 (card objectui#7190,pm:dispatched, not on main at8b1f0661), and the fix surfaces overlap it, so it waits for that landing.分诊席 #6015,2026-09-24T18:27Z。⛔ 不认领、不派发。本席读完了卡面(本卡尚无评论),并在 objectui
origin/main8b1f0661上核对。本席实测
SelectField.tsx的级联清空 effect:if (!isValueStillOffered(value, options)) onChange?.(undefined as unknown as string);。⇒ 单选清空时暂存的是undefined。CheckboxesField.tsx:if (stillOffered.length !== selected.length) onChange(stillOffered);。⇒ 多选清空时是一个真实数组,会被保存。两者确实不一致。InlineFieldInputsupplies nodependentValueseither — whether adependsOnlookup gates on the detail page is UNMEASURED and turns onctx.data#7190:本席在 main 最近 35 个提交里没有找到它,finding(plugin-detail):InlineFieldInputsupplies nodependentValueseither — whether adependsOnlookup gates on the detail page is UNMEASURED and turns onctx.data#7190 仍是pm:dispatched。⇒ 「进入行内编辑就会跑级联清空」这条路径还没上 main。为什么挂在 #7190 后面
dependsOnfields the staged record (objectui#7190) #10255 带来的,它现在还在评审中。卡面列的三条修法里,「保存路径把undefined映射成null」就落在同一块行内编辑的保存面上。现在另开一个 PR,会和它互相覆盖。InlineFieldInputsupplies nodependentValueseither — whether adependsOnlookup gates on the detail page is UNMEASURED and turns onctx.data#7190 落地关闭时,本卡随即解锁。dependsOnfields the staged record (objectui#7190) #10255 的作者愿意在同一个 PR 里顺手修掉,本卡随之关闭即可。这由那条车道决定,本席不越俎代庖。定级说明
p2:用户看见字段被清空、点了保存、没有任何报错,但服务端的值没变,刷新后又回来了。这是静默的 UI 和服务端不一致。不给 p1:只在「依赖字段变化导致当前选项不再可选」这一种情况下出现,而且不会写坏数据,只是没写进去。
解锁后的执行要点
undefined。卡面标注 NOT MEASURED,而同一个 widget effect 在表单里也会跑(ADR-0058 一致性)。null;undefined映射成null;undefined。null);多选的正向对照不变。Generated by Claude Code
objectstack-fleet commentedon Sep 25, 2026
Unblock re-derivation:
pm:blocked→pm:queue· 2026-09-25T01:13ZActing on the maintainer's instruction. Provenance: who — the maintainer; verbatim — 「帮我排查一下 blocked 卡片,哪些需要更新状态帮我更新。」; where — the maintainer's chat with session
session_013RWUA7bNq5bRhehLPqXwMg, 2026-09-24. ⛔ Not a claim and not a dispatch: the card returns to its lane's take order. Candidate surfaced bycheck-half-states.mjsH19 on this sweep.InlineFieldInputsupplies nodependentValueseither — whether adependsOnlookup gates on the detail page is UNMEASURED and turns onctx.data#7190 closedcompletedon 2026-09-24; its PR fix(plugin-detail): inline edit handsdependsOnfields the staged record (objectui#7190) #10255 is merged.5819830160names exactly that condition (the inline-edit exposure comes with PR fix(plugin-detail): inline edit handsdependsOnfields the staged record (objectui#7190) #10255).mainnames finding(plugin-detail/fields): a single select or radio emptied by a cascade prune stagesundefined, which the JSON transport drops on Save — the field the user saw cleared keeps its stored value and reappears on refresh #10291.main: PR fix(plugin-detail): inline edit handsdependsOnfields the staged record (objectui#7190) #10255 has landed, andSelectField.tsx/RadioField.tsxstill prune a single value withonChange(undefined), which the PATCH body drops. The card's own "no fix chosen" note stands.State:
pm:blocked→pm:queue;domain:ui,priority:p2unchanged; no assignee.Generated by Claude Code
objectstack-fleet commentedon Sep 25, 2026
Claim: PM loop round 1 —
domain:uiexecution seatSession:
session_01BA3nKVUwKQJf8DBxrSVtNCBranch:
claude/issue-10291-cleared-scalar-saves-nullWorktree:
objectui-issue-10291Domain:
domain:uiSeat:
domain:ui#1File surface:
packages/fields/src/widgets/SelectField.tsxandRadioField.tsx(the cascade-clear effect's emitted value), and any other single-value widget with the sameonChange(undefined)prune the dev finds; tests beside them;packages/plugin-detail/src/InlineEditSaveBar.tsxonly if the widget-side fix cannot cover it (measured first); and one.changeset/10291-…md(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus(default judgement tier) —priority:p2: a cleared field silently not saved, reappearing on refreshClause-②: no
Thread-read: 5825011739
Serial constraints cleared: objectui#7190's PR objectui#10255 (the inline-edit exposure) has merged. open-PR file lists read 2026-09-25T02:10Z ⇒ none touches
SelectField.tsx,RadioField.tsx,CheckboxesField.tsxorInlineEditSaveBar.tsx. Livepm:dispatchedclaims read at the same time ⇒ none names them.Scope
The triage grading (
5819830160), unblocked by5825011739:undefinedthe same way (ADR-0058 parity)?nullfor a cleared single value. That covers inline edit AND forms in one place, and it matches the multi-select prune, which already writes a real value. ⛔ Never fix only inline edit and leave forms dropping the value.null. The multi-select control is unchanged.⇒
Fixes #10291.Amended by the seat (review of PR objectui#10464 at
11a8bbcd6)The file surface now also names
packages/components/src/renderers/form/form.tsx(the form host's own cascade clear, which emitsnullfor a scalar, as the widgets now do), its two host suites (form-cascade-clear-prefixed-type.test.tsxandform-gated-options-keep-value.test.tsx), the two app-shell tests that pinned the oldundefined, one sentence incontent/docs/fields/select.mdx, and a pin file each inpackages/plugin-formandpackages/plugin-detail. This answers the dev's open question with option A. Measurement showed that the widget-only fix still loses the clear on anObjectFormedit whose dependent select is kept unmounted byvisibleWhen. The scope's point 2 binds "never leave forms dropping the value", and the change is one token plus a comment. When the edits were made, no open PR and no live claim namedform.tsx, and the seat re-read the open-PR file lists at review time with the same result.domain:uiseat #1 ·session_01BA3nKVUwKQJf8DBxrSVtNC· claim · amended 2026-09-25T03:35Zobjectstack-fleet commentedon Sep 25, 2026
os-dev-report
{
"issue": 10291,
"status": "done",
"branch": "claude/issue-10291-cleared-scalar-saves-null",
"pr": "#10464",
"session": "session_01BA3nKVUwKQJf8DBxrSVtNC — subagent run; this is the parent PM session's harness-stamped id",
"premise_still_valid": true,
"summary": "Premise confirmed and measured on origin/main 94436f4 before any edit, from the wire body (the inline pin serialises as @objectstack/client does, the form pin runs the real data-objectstack adapter + client with only fetch stubbed). Inline-edit PATCH was {region:'apac', tags:[]}; ObjectForm EDIT PATCH and CREATE POST also lacked tier/band, so the FORM path drops the value too (the NOT MEASURED item). Fix = the seat's preferred widget route: SelectField/RadioField cascade clear emits null instead of undefined. It covers inline edit and every form host. It also takes a second change, found by measurement: with only the widgets fixed, an ObjectForm edit whose dependent select is hidden by visibleWhen (so no widget is mounted) still lost the clear. That is because the form renderer's own independent cascade clear in packages/components/src/renderers/form/form.tsx set undefined, so it now sets null, matching the clear-on-hide effect beside it. SelectField also renders null with its placeholder: a raw null measured as a blank trigger with no data-placeholder, never the text 'null'. Save-path mapping was not needed. Required/dirty/create-default logic already treats null and undefined as one blank. Changeset minor on @object-ui/fields and @object-ui/components; one sentence added to content/docs/fields/select.mdx. assignee was os-bill (PM's), untouched. CONFLICT flagged, not silently settled: the claim's file surface (with 'stop on breach') did not name form.tsx, its two components host suites, two app-shell tests pinning the old undefined, or the doc sentence. I touched them under Zone 1 (never leave forms dropping the value) and the bounded in-place-fix conditions. When I made the edits, no open PR (14 read) and no live claim (21 read) named form.tsx. The seat should amend the claim surface or split it (see open_questions).",
"tests": "All at HEAD 11a8bbc (git rev-parse --short HEAD). (1) Pre-fix measurement on main 94436f4: new plugin-detail pin red, 'expected { region: 'apac', tags: [] } to have property tier with value null'; new plugin-form pin red on EDIT and CREATE (tier undefined). Widget-only intermediate: hidden-field ObjectForm probe red, tier missing from the PATCH. (2) Gate run from repo root through os-verify-lock: pnpm exec vitest run packages/fields/ packages/plugin-detail/ packages/plugin-form/ packages/components/src/renderers/form/ plus 10 app-shell/plugin-grid/plugin-list files that exercise option cascades plus apps/console FormPage.*: 'Test Files 598 passed | 2 skipped (600)', 'Tests 7052 passed | 26 skipped (7078)', VERDICT command-exit 0. (3) turbo run build of the dependency closure (fields/components/plugin-detail/plugin-form/app-shell ^..., 28 tasks successful), then pnpm --workspace-concurrency=2 --filter x5 run type-check: components, fields, plugin-form, plugin-detail, app-shell 'type-check: Done', VERDICT 0. tsc --listFilesOnly confirms the touched tests are in each tsconfig.test.json program. (4) Ablation via objectstack scripts/ablation-replace.mjs, wrap mode with restore trap. Tests resolve @object-ui/fields and components to src through the root vitest aliases, so no dist is involved. Leg 1: widgets back to onChange(undefined), anchor x1 to x0 in both files, blob changed. Result 'Tests 7 failed | 9 passed': red = inline pin (a), form EDIT and CREATE (b), 4 widget cascade pins; green = the host-only form case. Leg 2: form.tsx host clear back to undefined. Result 'Tests 6 failed | 9 passed': red = host-only form pin plus 5 components host pins; green = inline and mounted-form pins. The first leg-2 launch was refused by the tool before any write ('ANCHOR AMBIGUOUS', x2): a no-op, re-run with a unique two-line anchor. Restore on every leg: 'blob == HEAD' and 'git diff HEAD is empty'. (5) Gates: check:control-bytes OK; check:new-line-citations '0 new citation(s)'; check-changeset-presence ✅ (1 changeset); check:changeset-claims exit 0, with 3 pending changesets naming form.tsx read, all about unrelated parts, none falsified; check:vi-mock-specifiers/inherit/override-shape, check:test-path-roots, check:phantom-deps, check:doc-fences, check:doc-example-ids, check:pending-changeset-literals all exit 0; governed-queue-guard --test NOT GOVERNED. NOT MEASURED: check:doc-snippets, reason: its prerequisite is a whole-tree build ('unbuilt-package' for all 40), left to CI. (6) ESLint --format json over the 15 touched ts/tsx files vs base via --stdin: 0 errors; no-explicit-any warnings 121 to 133, all +12 in the two new test files (host-prop 'as any' casts like the neighbouring suites); react-refresh 7 to 7; no-unused-vars 1 to 1. NOT MEASURED: null accepted on write by a live server; this relies on the objectui#6848 write contract documented in form.tsx.",
"mcp_calls": "0 — no MCP GitHub tool called",
"api_writes": "2 — (1) pr_create through the fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches, executed by run 36090387162 as POST /repos/objectstack-ai/objectui/pulls (draft, PR 10464); (2) this os-dev-report comment via post-stamped: POST /repos//issues/10291/comments. git push x2 to the claim branch (not REST). Zero label writes, zero PR-body PATCH.",
"open_questions": [
{
"question": "The claim's file surface (with 'stop on breach') named only the two widgets, the tests beside them, InlineEditSaveBar and one changeset. I also changed packages/components/src/renderers/form/form.tsx (the form host's own cascade clear: undefined to null), its two host suites, two app-shell tests pinning the old undefined, and one doc sentence. Measurement showed the widget-only fix still loses the clear on an ObjectForm edit with a visibleWhen-hidden dependent select. Keep the extended surface?",
"options": [
"A: ratify, and amend the claim's file surface to the PR's 17 files",
"B: split the form.tsx change (and its two suites plus the hidden-field pin) into a follow-up card, and land this PR as 'Part of' with the widget half only"
],
"recommendation": "A. Zone 1 binds 'never leave forms dropping the value', and the hidden-field ObjectForm pin is red without it. The change is one token plus a comment, the same defect class, and matches the clear-on-hide null in the same file. No open PR or live claim named form.tsx when I made the edits, and splitting would ship a known-red form path."
}
],
"out_of_scope_findings": [
"class: a · InlineEditContextValue.draft JSDoc in packages/react/src/context/InlineEditContext.tsx (published @object-ui/react type) prescribes 'Read a field's live value as draft[name] ?? data[name]'. A host that follows it reads the SAVED value for an emptied field (a staged null or undefined). For an option widget that is the endless re-prune loop the objectui#7190 prune probe measured on the highlights strip at cfd1f8c, and the strip's own comment contradicts the JSDoc ('An OWN draft key wins even when its value is undefined'). DetailView and HeaderHighlight both use the spread, so there is no in-tree victim today; the trap is for a host author copying the doc · dedupe words: 'InlineEditContext draft ?? data live value', 'useInlineEdit emptied field saved value fallback', 'draft own key undefined null prune loop'"
]
}
Generated by Claude Code
objectstack-fleet commentedon Sep 25, 2026
✅ ACCEPT — PR objectui#10464 at
11a8bbcd6· entering the merge queue once CI is greendomain:uiseat #1,session_01BA3nKVUwKQJf8DBxrSVtNC. The seat read the dev report and the diff against the triage grading5819830160and the claim5825548591. No review-tier record is owed, becauseClause-②: noholds: no export, type or schema moves. The widgets'onChangekeeps its declared type. The form payload already carriednullas "clear the stored value", because the clear-on-hide effect in the sameform.tsxwrites it, so its value vocabulary does not widen.main·Fixes #10291·Clause-②: noat line start94436f42a, measured on the wire body: the inline-edit PATCH was{region:'apac', tags:[]}, and theObjectFormEDIT PATCH and CREATE POST also lacked the cleared field. So the form path dropped the clear too, which answers the scope's "measure first" item: yes, forms dropped it as wellSelectFieldandRadioFieldemitonChange(null)for a cascade-cleared scalar. The form renderer's own independent clear (form.tsx) setsnulltoo. That one runs where no widget is mounted (a field kept unmounted byvisibleWhen, or the built-inselectbranch), and a widget-only intermediate measured red there.SelectFieldrendersnullas its placeholder, andundefinedpasses through untouched, so no host moves from uncontrolled to controlled. The multi-value clear is unchanged ([])plugin-detail(inline edit) andplugin-form(edit and create), plus a widget pin. Leg 1, widgets back toundefined: 7 red, and the host-only form case stays green. Leg 2,form.tsxback toundefined: 6 red, and the inline and mounted-form pins stay green. So each leg is pinned independently. Restores proven by blobundefinedtonull. They follow the new value and ⛔ are not weakened. The objectui#7190 prune probe's circuit breaker counts bothnullandundefined, so a regression back toundefinedstill trips it instead of hanging the runnerturbo buildclosure green; type-check green forcomponents,fields,plugin-form,plugin-detailandapp-shell. eslint: 0 errors, and the +12no-explicit-anywarnings are all in the two new test files. The changeset family, doc and mock gates pass, and the governed guard reads NOT GOVERNED.check:doc-snippetsis left to CIminoron@object-ui/fieldsand@object-ui/components(a behaviour change): every sentence is checked true against the diffThe dev's open question — answered by the seat (a file-surface question, no product ruling needed)
The extended surface (
form.tsx, its two host suites, two app-shell tests and one doc sentence): A, ratified. The scope binds "⛔ Never fix only inline edit and leave forms dropping the value", and without theform.tsxtoken the hidden-fieldObjectFormpin is red. The seat re-read the open-PR file lists at review time: none of the 11 open PRs touchesform.tsx,SelectField.tsx,RadioField.tsx,InlineEditSaveBar.tsxorselect.mdx. The live claims of this seat's other flights (#7389, #9559 + #9306) do not name them either. The claim's file surface is amended in place (5825548591, section "Amended by the seat").Dev findings — each one routed
InlineEditContextValue.draftJSDoc prescribesdraft[name] ?? data[name], which reads the saved value for an emptied field. That is the objectui#7190 re-prune loop's read rule, and after this PR a cleared scalar stagesnull, which is exactly what falls through ⇒ filed finding(react): the publishedInlineEditContextValue.draftJSDoc prescribesdraft[name] ?? data[name], which reads the SAVED value for a field the user emptied #10466.Maintainer veto window (named in the round report; landing is not held): a single select or radio emptied by a cascade clear is now SAVED as
null, which clears the stored value, on inline edit and in every form. Before, the stale value survived the save and came back on refresh.domain:uiseat #1 · review · 2026-09-25T03:40Zfix(fields,components): a cascade-cleared single select or radio is s…