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
Studio publish sheet lists an "Options" change for a picklist-bound field nobody edited, because it compares the stored draft with the live read that carries the resolved options #29899
Filing gate: ① user-visible UX defect, stable and with a named landing point (reach: the Studio publish review sheet, reproduced twice: a Studio add-field draft and an API draft that changed only another field's label).
Business reason: the publish sheet is where an author checks what they are about to ship. On any object that has a field bound to a package picklist, it reports a change to that field's options on every publish, although nothing changed. An author either distrusts the sheet or goes looking for an edit they never made, and a real options change on the same field would read the same.
Reader who acts: domain:ui, ui/packages/app-shell/src/preview/DraftChangesPanel.tsx (the diff the sheet draws).
Dedupe (open and closed): picklist publish sheet options changed 11 hits, DraftChangesPanel 30. Nearest: #27835 (closed, naming each change in words) and #29704 (closed, plain words on the sheet). None is about a phantom change.
QA-source: #29893 · studio-authoring.picklist-bound-select · observed beside the clauses
What happens
Measured at framework 589049b0 on a stock showcase boot with the Console built from the same tree, as admin.
Declare a picklist in a package (this run added showcase_tier, Bronze / Silver / Gold, to a scratch copy of the showcase, as the item's fixtures.requires[0] asks). In a runtime package, publish an object with a field tier of {type: "select", picklist: "showcase_tier"} (stored without options, as a picklist-bound field must be).
Save a draft of that object that changes only fields.notes.label (through Studio or PUT /api/v1/meta/object/OBJ?mode=draft&package=PKG).
Open /_console/studio/PKG/data?surface=object:OBJ and click Publish.
Expected: one line, ~ Notes … · Label.
Actual: two lines, ~ Notes Again notes · Labeland~ Tier tier · Options.
Likely mechanism
The draft stores the bound field without options, while the live read serves it with the options the server resolved from the picklist. The sheet's diff compares the two and reads the resolved options as an edit. It should compare like with like (both stored, or both resolved), or ignore options on a field that declares picklist.
Read fresh. Reproduced twice: Studio's publish sheet lists an "Options" change on every picklist-bound field nobody edited, because it compares the stored draft with the live read that carries the resolved options. The author's review surface lies, so p2.
Claim: PM loop round 1 · 2026-11-03T15:49Z
Session: session_01WGufzfC4w4tZzdzVyV5FGX (card thread; dispatching seat session_01WGufzjDyeFwq6AuFu3vwTd)
Account: os-support-ai
Branch: claude/project-thread-xkxln8 (platform-assigned to the card thread; it can push only this branch)
Worktree: objectstack-issue-29899
Domain: domain:ui
Seat: domain:ui#1
File surface: ui/packages/app-shell/src/preview/DraftChangesPanel.tsx and the diff builder it uses, their tests, one root .changeset/ file (stop on breach; explain in the report)
Container & model: S/M, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate), effort: high
Clause-②: no
Metadata-protocol: none
Responsibility: ui code produces it (domain:ui) | no platform path covers it | every Studio/Console user who reaches the surface named in the card body
Thread-read: 6601964711
Premise: holds (re-read on origin/main21510bba: the code the card names is unchanged)
Serial constraints cleared: none (no other domain:ui claim touches ui/packages/app-shell/src/preview/**; #29897 is in views/metadata-admin/ResourceEditPage.tsx)
Order: /mnt/project-files/ui-orders/29899.md · maintainer scope 「车队可以继续派发了,只派发 p1 p2 ,总并发不超过 20」 (2026-11-03T15:40Z)
{
"issue": 29899,
"status": "done",
"branch": "claude/project-thread-xkxln8",
"pr": "https://github.com/objectstack-ai/objectstack/pull/29949",
"session": "session_01WGufzfC4w4tZzdzVyV5FGX",
"premise_still_valid": true,
"summary": "computeChangeDetail (the diff behind Studio's publish review sheet) now passes both envelope-free bodies of an object through dropServedPicklistOptions (@object-ui/data-objectstack, the writers' served-to-authored step), so a picklist-bound field is compared in its stored form on both sides. Chose 'compare like with like (both stored)' over 'skip options in the diff': it touches only definitions that name a picklist, so inline-options changes stay listed and a move between picklist and inline options still lists both keys. Mechanism confirmed in code: readMetaItem returns early for state=draft (stored row, no fold), while the active read folds through finishServedObject → resolvePicklistFieldsOnto. Serving resolved options is the declared PicklistServedFieldSchema contract, so the fix is ui-side, not a server fork. No path found where the draft itself carries resolved options.",
"tests": "New DraftChangesPanel.picklistServedOptions-29899.test.tsx: 3 passed (bound field + label-only draft renders exactly one row '~ Again notes notes · Label'; CONTROL inline-options select with changed options renders '~ Status status · Options'; computeChangeDetail direct incl. bound/inline moves and array shape). Ablation at f4d64a21 via scripts/ablation-replace.mjs (anchor 1 → 0, blob 694ec6852677 → eb00fb8b831c; test imports src, no dist hop): 3 failed, e.g. 'expected [ ~ Again notes notes · Label, …(1) ] to have a length of 1 but got 2'; restored blob == HEAD, git diff HEAD empty. app-shell narrowed vitest over the 84 files naming DraftChangesPanel: Test Files 84 passed (84), Tests 536 passed (536), VERDICT command-exit 0. Full app-shell suite declared to CI (Test UI).",
"gates": "At f4d64a21 under os-verify-lock: closure build turbo --filter=@object-ui/app-shell^... VERDICT command-exit 0 (64/64); pnpm --filter @object-ui/app-shell build VERDICT command-exit 0; pnpm --filter @object-ui/app-shell type-check VERDICT command-exit 0; node scripts/check-sdui-manifest.mjs exit 0. dispatch-gates --commands: 35 derived, all 35 exit 0; dispatch-gates --ran exit 0 (35 run, 0 NOT-MEASURED, 0 UNRUN). PR check-runs: in_progress at report time (not awaited).",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "0 — no MCP GitHub calls",
"api_writes": "2 — relay pr_create (POST /repos/objectstack-ai/objectstack/dispatches, run 41676451797, PR #29949 read back identical, assignees none); this os-dev-report comment via post-stamped.mjs (POST /repos/objectstack-ai/objectstack/issues/29899/comments). git push not counted.",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": []
}
Activity
objectstack-fleet commentedon Nov 3, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: first-touch audit · p2 ·
domain:ui·pm:queue· 2026-11-03T10:59Z⛔ Not a claim.
Generated by Claude Code
objectstack-fleet commentedon Nov 3, 2026
domain:ui#1 · os-support-ai · session_01WGufzjDyeFwq6AuFu3vwTd
Claim: PM loop round 1 · 2026-11-03T15:49Z
Session:
session_01WGufzfC4w4tZzdzVyV5FGX(card thread; dispatching seatsession_01WGufzjDyeFwq6AuFu3vwTd)Account:
os-support-aiBranch:
claude/project-thread-xkxln8(platform-assigned to the card thread; it can push only this branch)Worktree:
objectstack-issue-29899Domain:
domain:uiSeat:
domain:ui#1File surface:
ui/packages/app-shell/src/preview/DraftChangesPanel.tsxand the diff builder it uses, their tests, one root.changeset/file (stop on breach; explain in the report)Container & model:
S/M,mode:thread,model: TIER_DEFAULT(dispatch-gates --tier: no path-derived mandate),effort: highClause-②: no
Metadata-protocol: none
Responsibility: ui code produces it (
domain:ui) | no platform path covers it | every Studio/Console user who reaches the surface named in the card bodyThread-read: 6601964711
Premise: holds (re-read on
origin/main21510bba: the code the card names is unchanged)Serial constraints cleared: none (no other
domain:uiclaim touchesui/packages/app-shell/src/preview/**; #29897 is inviews/metadata-admin/ResourceEditPage.tsx)Order:
/mnt/project-files/ui-orders/29899.md· maintainer scope 「车队可以继续派发了,只派发 p1 p2 ,总并发不超过 20」 (2026-11-03T15:40Z)objectstack-fleet commentedon Nov 3, 2026
domain:ui#1 · os-support-ai · session_01WGufzfC4w4tZzdzVyV5FGX
os-dev-report
Stamp: 2026-11-03T16:41Z
{ "issue": 29899, "status": "done", "branch": "claude/project-thread-xkxln8", "pr": "https://github.com/objectstack-ai/objectstack/pull/29949", "session": "session_01WGufzfC4w4tZzdzVyV5FGX", "premise_still_valid": true, "summary": "computeChangeDetail (the diff behind Studio's publish review sheet) now passes both envelope-free bodies of an object through dropServedPicklistOptions (@object-ui/data-objectstack, the writers' served-to-authored step), so a picklist-bound field is compared in its stored form on both sides. Chose 'compare like with like (both stored)' over 'skip options in the diff': it touches only definitions that name a picklist, so inline-options changes stay listed and a move between picklist and inline options still lists both keys. Mechanism confirmed in code: readMetaItem returns early for state=draft (stored row, no fold), while the active read folds through finishServedObject → resolvePicklistFieldsOnto. Serving resolved options is the declared PicklistServedFieldSchema contract, so the fix is ui-side, not a server fork. No path found where the draft itself carries resolved options.", "tests": "New DraftChangesPanel.picklistServedOptions-29899.test.tsx: 3 passed (bound field + label-only draft renders exactly one row '~ Again notes notes · Label'; CONTROL inline-options select with changed options renders '~ Status status · Options'; computeChangeDetail direct incl. bound/inline moves and array shape). Ablation at f4d64a21 via scripts/ablation-replace.mjs (anchor 1 → 0, blob 694ec6852677 → eb00fb8b831c; test imports src, no dist hop): 3 failed, e.g. 'expected [ ~ Again notes notes · Label, …(1) ] to have a length of 1 but got 2'; restored blob == HEAD, git diff HEAD empty. app-shell narrowed vitest over the 84 files naming DraftChangesPanel: Test Files 84 passed (84), Tests 536 passed (536), VERDICT command-exit 0. Full app-shell suite declared to CI (Test UI).", "gates": "At f4d64a21 under os-verify-lock: closure build turbo --filter=@object-ui/app-shell^... VERDICT command-exit 0 (64/64); pnpm --filter @object-ui/app-shell build VERDICT command-exit 0; pnpm --filter @object-ui/app-shell type-check VERDICT command-exit 0; node scripts/check-sdui-manifest.mjs exit 0. dispatch-gates --commands: 35 derived, all 35 exit 0; dispatch-gates --ran exit 0 (35 run, 0 NOT-MEASURED, 0 UNRUN). PR check-runs: in_progress at report time (not awaited).", "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm", "mcp_calls": "0 — no MCP GitHub calls", "api_writes": "2 — relay pr_create (POST /repos/objectstack-ai/objectstack/dispatches, run 41676451797, PR #29949 read back identical, assignees none); this os-dev-report comment via post-stamped.mjs (POST /repos/objectstack-ai/objectstack/issues/29899/comments). git push not counted.", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [] }Generated by Claude Code
objectstack-fleet commentedon Nov 3, 2026
domain:ui#1 · os-support-ai · session_01WGufzjDyeFwq6AuFu3vwTd
ACCEPT → PR #29949 comment 6609921443 · head
f4d64a21· 2026-11-03T16:44Z. Landing waits for every check green on that head.objectstack-fleet commentedon Nov 3, 2026
domain:ui#1 · os-support-ai · session_01WGufzjDyeFwq6AuFu3vwTd
Landed · PR #29949 ·
40fa433539· 2026-11-03T17:30Z40fa433539origin/maincontentui/packages/app-shell/src/preview/DraftChangesPanel.tsx:698runsdropServedPicklistOptionson both bodies forobjectACCEPT 6609921443 · card closed by
Fixes #29899(completed);pm:dispatchedremoved in this stroke.