Repository navigation
Studio: object writers outside the two designers re-send a picklist-bound field's served options, so their save is refused with 422 (the OWD overview measured; the objectui#10202 family) #11692
Description
Filing-gate class ①: a defect with a named location, measured live on one public Studio surface and read from source for the rest. Filed by the objectui domain:ui seat 1 (session_015W8GBu6sBiqus2L2xjMsAL) from the dev report of objectui#10202 (6010658114). ⛔ Not graded or routed here; ⛔ not a claim.
What an author sees
A select field can name a shared picklist (picklist: 'NAME', @objectstack/spec 17.6.0 data/picklist.zod.ts).
- The serving side. The runtime serves such a field with both
picklistand theoptionsit resolved from the list and its extensions (PicklistServedFieldSchema). - The authoring side. The authoring door refuses the two together, at
fields.FIELD.options, with the prescription "Keeppicklistand deleteoptions".
So any Studio writer that seeds an object PUT from a served read, and sends it back unchanged, is refused for the whole object, whatever the author edited.
Measured:
- Setup: objectstack
mainfaf8dce4, a local picklist fixture, and a runtime objectacme_accountwhose fieldtieris bound. - Steps: Studio → Access → OWD overview (
?surface=owd:overview), changeacme_account's internal sharing model, then Save. - Result:
PUT /api/v1/meta/object/acme_account?mode=draftanswered 422INVALID_METADATAatfields.tier.options, with the server's prescription on screen.
objectui#10202 (PR objectui#11669) fixes the two designers that host the select-field editor: the metadata-admin object designer and the Studio data page. It does that with a pure helper, dropServedPicklistOptions (packages/app-shell/src/views/metadata-admin/picklist-binding.ts). The other writers are untouched.
Where (objectui main, read from source by the dev)
PackageOwdOverviewPanelsaves{ ...(draftBody ?? baseline), sharingModel }with the served baseline. This is the measured one.packages/app-shell/src/views/runtime-metadata-persistence.ts: two object saves.packages/plugin-designer:MetadataFieldsPageandMetadataObjectsPage.MetadataService.saveObject,saveFieldsandsaveMetadataItem.
Fix direction (not ruled)
The dev of objectui#10202 framed the route on the four axes (6010658114, open_questions):
- A, per writer (the dev's recommendation).
- The same
dropServedPicklistOptionsgoes at each writer that seeds from a served read, and each writer is pinned like objectui#10202's two. - No export is added. The authoring door's loud refusal of an authored
picklist+optionspair stays intact. - The cost is enumerating the writers: the class objectui#8676 names.
- The same
- B, at the transport.
MetadataClient.saveand theMetadataServiceSDK door stripoptionsbesidepickliston every object write.- This changes a published client's behaviour and needs a new
data-objectstackexport (Clause-② yes). - It silently accepts an authored pair that the spec refuses with a prescription.
Done when each writer above either strips the served options before its PUT (pinned with a 422-refusing transport, as objectui#10202's pins are), or is shown not to seed from a served read.
Dedupe: mcp__github__search_issues (objectstack-ai/objectui, semantic, closed included), object save picklist served options 422 OWD overview sharing model re-sends picklist-bound field → 5 hits. None is this defect:
- objectui#10202 is the parent, which carves these writers out;
- objectui#11253, finding(app-shell): FieldStub slices the picklist preview BEFORE filtering, so a well-formed option behind malformed ones never renders and the +N counter overcounts #9074, A dependency-gated option list clears the field's stored value on mount, before the user touches anything #4247 and 详情页笔 icon 行内编辑:多值 lookup 无法选中多个 #2956 are closed and unrelated.
Dedupe words: picklist served options 422 · OWD overview picklist · object save picklist options · dropServedPicklistOptions · picklist-bound field PUT
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 6, 2026
Path: studio authoring — objects | 缺项 (no item saves a picklist-bound object outside the two designers) | P2
Triage: first grade —
bug·priority:p2·domain:ui·area:studio·pm:queue. Every object writer drops the servedoptionsof a picklist-bound field before saving, through the one helperTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-06T06:57Z. ⛔ Not a claim, ⛔ not a dispatch. Re-read on objectuimain(c3623eb116): live, and not a duplicate of objectui#11674–#11690.Triage: lands in
packages/app-shell/src/views/studio-design/PackageOwdOverviewPanel.tsx(about:259–:264),packages/plugin-designer/src/MetadataFieldsPage.tsx(about:954),MetadataObjectsPage.tsx(about:257) andpackages/app-shell/src/services/MetadataService.ts(saveMetadataItemabout:708,saveObjectabout:808,saveFieldsabout:898) ⇒domain:ui; rationale: onlyStudioDesignSurface.tsx(about:3728and:3796) callsdropServedPicklistOptions. The other writers re-send the served read, and the server refusesoptionsbesidepicklistwith a 422 on the whole save.runtime-metadata-persistence.tsnever saves an object (RuntimeArtifactTypeisview,report,dashboardorpage), so it is out of scope.MetadataService) or at each listed writer, not re-implemented per page.optionsbesidepickliststays: it is the authoring check objectui#10202 relies on.optionson the bound field.Generated by Claude Code
objectstack-fleet commentedon Oct 6, 2026
Claim: PM loop round 1
Session:
session_01FngvPpdrnhHMdHHq6vwwjuAccount:
os-justinBranch:
claude/issue-11692-writers-drop-served-optionsWorktree:
objectui-issue-11692Domain:
domain:uiSeat:
domain:ui#2File surface: the object writers triage names:
packages/app-shell/src/views/studio-design/PackageOwdOverviewPanel.tsx(about:259–:264),packages/plugin-designer/src/MetadataFieldsPage.tsx(about:954),packages/plugin-designer/src/MetadataObjectsPage.tsx(about:257),packages/app-shell/src/services/MetadataService.ts(saveMetadataItemabout:708,saveObjectabout:808,saveFieldsabout:898); the helperdropServedPicklistOptions(packages/app-shell/src/views/metadata-admin/picklist-binding.ts), its one move to a package bothapp-shellandplugin-designeralready depend on if the dev measures that move is needed, and its existing callers' imports; the tests beside them; and.changeset/11692-*.md(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier --repo objectstack-ai/objectui: no path-derived mandate; default tier)Clause-②: yes
Thread-read: 6011093735
Serial constraints cleared:
none. objectui#10202 (the helper's origin, PR objectui#11669) landed asc3623eb, which ismainat this read. No open PR (objectui#11700, objectui#11069) and no in-flight claim touches these files; this seat's objectui#11677 and objectui#11691 holdapps/console/src/pages/**only. Read 2026-10-06T07:14Z.Why
Clause-②: yes:@object-ui/plugin-designerdoes not depend on@object-ui/app-shell(itspackage.jsonatc3623eb), so the two designer pages cannot import the helper where it lives today. One helper serving both likely means a new export from a package both already import, which widens that package's published surface. The dev measures which package and reports the built-declaration reading. Triage's ⛔ stands either way: the server's refusal of an authoredpicklist+optionspair is not traded for a silent strip.Generated by Claude Code
objectstack-fleet commentedon Oct 6, 2026
os-dev-report
{
"issue": 11692,
"status": "done",
"branch": "claude/issue-11692-writers-drop-served-options",
"pr": "#11706",
"session": "session_01FngvPpdrnhHMdHHq6vwwju — the dispatching PM session (subagent = parent's); container CLAUDE_CODE_REMOTE_SESSION_ID cse_01FngvPpdrnhHMdHHq6vwwju",
"premise_still_valid": true,
"summary": "The card's premise holds. Re-read on objectui main c3623eb: every listed writer except two seeds its object PUT from a served read and re-sends a bound field's resolved options. Done: dropServedPicklistOptions moved (git mv, with its test) from app-shell to @object-ui/data-objectstack and is exported from its barrel; app-shell's two callers (builtinComponents fromDraft, StudioDesignSurface) were re-pointed, and no copy is left. It is applied before the PUT at PackageOwdOverviewPanel doSave, MetadataFieldsPage handleFieldsChange, MetadataObjectsPage handleObjectsChange and MetadataService.saveFields. MetadataService.saveObject and saveMetadataItem read nothing, so they are left unchanged: pins show they issue no read and an authored picklist+options pair still gets the server's 422. MetadataClient.save is unchanged (option B stays ruled out). Zone 2 readings: (1) falsified in part, because the landed helper is called by builtinComponents fromDraft AND by StudioDesignSurface's draft autosave and column-reorder saves, which is what triage said; (2) measured: both Setup pages save through MetadataClient.save directly, not MetadataService, and app-shell lists plugin-designer as a peer, so a move was needed; the built declaration in data-objectstack dist/index.d.ts is 'declare function dropServedPicklistOptions' with type parameter T extends Record (string to unknown), taking body T and returning T, and it is in the barrel export list; (3) 4 writers seed and now strip, 2 do not seed and are shown so by pins; (4) OWD was measured live by the filer, and the others are pinned with a 422-refusing door double, not measured live.",
"tests": "HEAD bc90d2f (src identical to 51bb8bb, where the build and type-check ran). New pins, each driving a real MetadataClient or a real ObjectStackAdapter/SDK over a transport that 422s a body carrying picklist beside options: PackageOwdOverviewPanel.picklistServedOptions-11692 (2 tests), MetadataFieldsPage.picklistServedOptions-11692 (1), MetadataObjectsPage.picklistServedOptions-11692 (1), MetadataService.picklistServedOptions-11692 (3); moved picklist-binding.test.ts (6). Targeted run: 'Test Files 5 passed (5) / Tests 13 passed (13)'. Narrowed package run (declared, see deviations): data-objectstack and plugin-designer whole, plus 126 app-shell files: 'Test Files 247 passed (247) / Tests 2342 passed (2342)'. Ablation, after committing the fix, through objectstack scripts/ablation-replace.mjs in wrap mode (anchor hit x1 to x0, replacement x0 to x1, blob changed; restore proven by blob == HEAD and an empty git diff HEAD), with an outer trap EXIT INT TERM that runs git checkout HEAD on the absolute paths. OWD: 2 failed of 2, 'refused at fields.tier.options: expected 422 to be 200'. Fields page: 1 failed of 1, 'refused at fields.tier.options, fields.regions.options'. Objects page: 1 failed of 1, 'refused at fields.tier.options'. saveFields: 1 failed, 2 passed of 3, with the SDK throwing 'failed spec validation'; the saveObject and saveMetadataItem pins stay green, as expected. The first attempt for the last three was refused by the tool and ran no test (the replacement was a substring of the anchor), was re-anchored on the full save line, and was re-run. The reading direction was red, as predicted. No dist in the path: the writers are imported from src by relative path, and vitest aliases data-objectstack to src. Type-check: data-objectstack, plugin-designer and app-shell all exit 0, and tsc --listFiles shows every new test file in the type-check programs (tsconfig.test.json leg). The cross-package import resolves through data-objectstack dist (no paths mapping), so a stale dist would be TS2305 red, not green. Lint: exit 0 for all three, 0 errors, and no warning on an added line. README snippet compiled --strict against the built dist: exit 0, and the negative control (misspelt name) gives TS2724, exit 2.",
"gates": [
{
"cmd": "pnpm exec turbo run build --filter='@object-ui/app-shell^...' --filter='@object-ui/plugin-designer^...' --concurrency=2",
"exit": 0,
"head": "51bb8bb",
"verdict": "Tasks: 28 successful, 28 total"
},
{
"cmd": "pnpm --filter @object-ui/data-objectstack run type-check",
"exit": 0,
"head": "51bb8bb"
},
{
"cmd": "pnpm --filter @object-ui/plugin-designer run type-check",
"exit": 0,
"head": "51bb8bb"
},
{
"cmd": "pnpm --filter @object-ui/app-shell run type-check",
"exit": 0,
"head": "51bb8bb"
},
{
"cmd": "pnpm --filter @object-ui/data-objectstack run lint",
"exit": 0,
"head": "bc90d2f"
},
{
"cmd": "pnpm --filter @object-ui/plugin-designer run lint",
"exit": 0,
"head": "bc90d2f"
},
{
"cmd": "pnpm --filter @object-ui/app-shell run lint",
"exit": 0,
"head": "bc90d2f"
},
{
"cmd": "pnpm exec vitest run --maxWorkers=2 (5 new/moved test files)",
"exit": 0,
"head": "51bb8bb",
"verdict": "Test Files 5 passed (5), Tests 13 passed (13)"
},
{
"cmd": "pnpm exec vitest run --maxWorkers=2 packages/data-objectstack/ packages/plugin-designer/ + 126 app-shell files",
"exit": 0,
"head": "bc90d2f",
"verdict": "Test Files 247 passed (247), Tests 2342 passed (2342)"
},
{
"cmd": "pnpm check:self-import",
"exit": 0,
"head": "bc90d2f"
},
{
"cmd": "pnpm check:phantom-deps",
"exit": 0,
"head": "bc90d2f"
},
{
"cmd": "pnpm check:unused-deps",
"exit": 0,
"head": "bc90d2f"
},
{
"cmd": "pnpm check:readme-exports",
"exit": 1,
"head": "bc90d2f",
"verdict": "PREREQUISITE: 8 packages outside the build closure unbuilt (type entry not on disk); --list exits 2 and judges the new data-objectstack README import 'real'; read as NOT MEASURED for the whole-gate verdict"
},
{
"cmd": "pnpm check:new-line-citations",
"exit": 0,
"head": "bc90d2f",
"verdict": "0 new citation(s)"
},
{
"cmd": "pnpm check:control-bytes",
"exit": 0,
"head": "bc90d2f"
},
{
"cmd": "pnpm check:changeset-claims",
"exit": 0,
"head": "bc90d2f"
},
{
"cmd": "pnpm check:pending-changeset-literals",
"exit": 0,
"head": "bc90d2f"
},
{
"cmd": "pnpm check:metadata-write-doors",
"exit": 0,
"head": "bc90d2f",
"verdict": "3 can carry an object document, 3 reach assertObjectMetadataWritable"
},
{
"cmd": "node scripts/check-changeset-no-major.mjs",
"exit": 0,
"head": "bc90d2f"
},
{
"cmd": "node scripts/check-changeset-fixed.mjs",
"exit": 0,
"head": "bc90d2f"
},
{
"cmd": "node scripts/check-changeset-presence.mjs",
"exit": 0,
"head": "bc90d2f"
},
{
"cmd": "pnpm check:unreferenced-sources",
"exit": 0,
"head": "bc90d2f"
},
{
"cmd": "pnpm check:test-path-roots",
"exit": 0,
"head": "bc90d2f"
},
{
"cmd": "node scripts/check-vi-mock-specifiers.mjs / -inherit / -override-shape",
"exit": 0,
"head": "bc90d2f"
},
{
"cmd": "pnpm check:esm-specifiers",
"exit": 0,
"head": "bc90d2f"
},
{
"cmd": "node scripts/check-doc-fence-languages.mjs",
"exit": 0,
"head": "bc90d2f"
},
{
"cmd": "pnpm check:designer-field-key-parity",
"exit": 0,
"head": "bc90d2f"
},
{
"cmd": "pnpm check:component-surface-parity (report-only)",
"exit": 0,
"head": "bc90d2f"
},
{
"cmd": "node scripts/check-type-check-coverage.mjs",
"exit": 0,
"head": "bc90d2f"
},
{
"cmd": "node scripts/check-lint-coverage.mjs",
"exit": 0,
"head": "bc90d2f"
},
{
"cmd": "pnpm check:spec-symbols",
"exit": 0,
"head": "bc90d2f"
},
{
"cmd": "pnpm check:doc-snippets",
"exit": null,
"head": "bc90d2f",
"verdict": "NOT MEASURED, reason: the gate's build closure is every package; the one new README block was compiled --strict against the built data-objectstack dist instead (exit 0; negative control TS2724)"
},
{
"cmd": "pnpm check:eager-closure",
"exit": null,
"head": "bc90d2f",
"verdict": "NOT MEASURED, reason: it needs a full console build; the console's dataSource already imports @object-ui/data-objectstack statically (a source reading)"
},
{
"cmd": "CI",
"exit": null,
"head": "bc90d2f",
"verdict": "in_progress at report time: 20 success, 3 skipped, 19 in_progress; CI convergence is the PM's"
}
],
"files_changed": [
"A .changeset/11692-writers-drop-served-options.md (data-objectstack minor; app-shell, plugin-designer patch)",
"R packages/app-shell/src/views/metadata-admin/picklist-binding.ts moved to packages/data-objectstack/src/picklist-binding.ts",
"R packages/app-shell/src/views/metadata-admin/picklist-binding.test.ts moved to packages/data-objectstack/src/picklist-binding.test.ts",
"M packages/data-objectstack/src/index.ts (barrel export)",
"M packages/data-objectstack/README.md (new section for the export)",
"M packages/app-shell/src/services/builtinComponents.tsx (import re-point)",
"M packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx (import re-point)",
"M packages/app-shell/src/views/studio-design/PackageOwdOverviewPanel.tsx",
"M packages/app-shell/src/services/MetadataService.ts",
"M packages/plugin-designer/src/MetadataFieldsPage.tsx",
"M packages/plugin-designer/src/MetadataObjectsPage.tsx",
"A packages/app-shell/src/views/studio-design/PackageOwdOverviewPanel.picklistServedOptions-11692.test.tsx",
"A packages/app-shell/src/services/MetadataService.picklistServedOptions-11692.test.ts",
"A packages/plugin-designer/src/MetadataFieldsPage.picklistServedOptions-11692.test.tsx",
"A packages/plugin-designer/src/MetadataObjectsPage.picklistServedOptions-11692.test.tsx"
],
"line_budget": "n/a: no skills/** file and no line-ratcheted ledger touched",
"deviations": [
"Narrowed local test run, declared. A full vitest run of data-objectstack, plugin-designer and app-shell (1146 files, --maxWorkers=2) held the shared verify lock for 29 minutes without finishing, with other seats queued, so I stopped it by its own PIDs. The narrowed set: both small packages whole, plus every app-shell test under src/services/ and src/views/studio-design/, every app-shell test that names StudioDesignSurface, builtinComponents, PackageOwdOverviewPanel, MetadataService, picklist-binding, dropServedPicklistOptions or register-builtins, and the 10202 suites. That set is derived from direct importers and is blind to transitive importers. The full app-shell suite is declared to CI.",
"File surface: packages/data-objectstack/README.md is not named literally in the claim. It is the package doc for the moved export (AGENTS.md #2 requires it). It is read-only prose plus one import snippet, and readme-exports judged that snippet real. Everything else is inside the claim's surface (the barrel index.ts counts as the 'move to a package' half).",
"Ablation: the first attempt for three of the four writers was a refused no-op (the tool rejected an anchor whose replacement was its own substring) and ran no test. They were re-anchored and re-run, and only the re-run is reported.",
"Attribution, reported not chosen: the harness reminder asked for a commit trailer with a model name and a different PR footer. Per os-dev.md and objectui AGENTS.md I wrote the model-free pair (Claude-Session + Co-authored-by: Claude) and the session-URL PR footer.",
"Changeset levels: data-objectstack is minor (new export, Clause-② yes), and app-shell and plugin-designer are patch. The fixed group bumps together, so the release is minor."
],
"mcp_calls": "0 — none",
"api_writes": "3 REST writes, each relayed as one POST /repos/objectstack-ai/objectstack/dispatches executed by objectstack-fleet[bot]: (1) pr_create POST /repos/objectstack-ai/objectui/pulls (draft, base main), PR 11706, body read back 12496 of 12496 bytes identical; (2) label-write assignee POST /repos//issues/11706/assignees (os-justin), read back MATCHES; (3) this os-dev-report comment POST /repos//issues/11692/comments. Plus 2 git pushes, which are not REST (the empty-branch probe, then bc90d2f). One --dry-run of the PR stroke sent nothing.",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: named producer — EmbeddedItemEditor doSave in packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.tsx. MetadataDetailDrawer mounts it from RelatedPanel's object anchors (fields / indexes / validations in anchors.ts). It re-reads client.layered(parentType, parentName).effective, a served read, splices the edited item in, and PUTs the whole parent object without dropServedPicklistOptions. So editing any embedded item of an object that has a picklist-bound field sends picklist beside options and gets the door's 422. Evidence: a scratch probe (never committed, deleted after the run) mounted the editor with effective serving a bound tier field, then saved an edit of amount. Result: 'AssertionError: expected [ fields.tier.options ] to deeply equal []' (1 failed of 1). Not live-measured on a running app. Same defect class as this card, but outside the claim's file surface, so it was not fixed. Route: a sub-issue of objectui#11692 (it falls inside this card's family), or a widening of this claim, after which the fix is one wrap of the body plus a pin like the four here. · dedupe words: EmbeddedItemEditor picklist options 422 · embedded item parent PUT served options · object.fields embedded save picklist · dropServedPicklistOptions EmbeddedItemEditor",
"carrier: 承接者:无 · noted, not filed — MetadataService.saveObject, saveFields and saveMetadataItem have zero in-repo callers (public API through useMetadataService only), so the saveFields fix and the 'no read' pins are measured on the method, not on any screen (recorded in the PR's Acceptance notes)"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 6, 2026
Claim amendment: the file surface widens to
EmbeddedItemEditor.tsx. A patch round on PR objectui#11706 follows before reviewdomain:uiexecution seat 2 ·session_01FngvPpdrnhHMdHHq6vwwju(os-justin) · 2026-10-06T08:35Z. This amends claim6011332561; the branch, the assignee andClause-②: yesare unchanged.Why. The dev report on this card names one more writer of the same defect.
EmbeddedItemEditor'sdoSave(packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.tsx), mounted byMetadataDetailDrawerfrom an object's fields / indexes / validations anchors, re-readsclient.layered(parentType, parentName).effective, which is a served read.dropServedPicklistOptions.tierfield and saved an edit ofamount. It answeredexpected [ fields.tier.options ] to deeply equal [].Triage's direction for this card is "Every object writer drops the served
optionsof a picklist-bound field before saving", so this writer is inside the card. It meets the four bounded in-place conditions:app-shellgate family.Amended file surface: the claim's surface, plus
packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.tsx(itsdoSave) and a pin beside it.Next: a patch round on PR objectui#11706 by the same dev, adding this writer and its pin. The seat's review and the at-tier contract review (
Clause-②: yes, the new@object-ui/data-objectstackexport) run on the patched head.Generated by Claude Code
objectstack-fleet commentedon Oct 6, 2026
os-dev-report
{
"issue": 11692,
"round": "patch round 1 (fifth writer: EmbeddedItemEditor), on claim 6011332561 as amended by 6012529702",
"status": "done",
"branch": "claude/issue-11692-writers-drop-served-options",
"pr": "#11706",
"head": "32b8c2ee4a3471f58c9b2313415c8a1827e5b7d8",
"session": "session_01FngvPpdrnhHMdHHq6vwwju — the dispatching PM session (subagent = parent's); container CLAUDE_CODE_REMOTE_SESSION_ID cse_01FngvPpdrnhHMdHHq6vwwju",
"premise_still_valid": true,
"summary": "Patch round done. First, origin/main (3409fe8: objectui#11700, #11702) was merged into the branch as a merge commit (566ba80), with no rebase and no force push; there were no conflicts, and main had touched none of this PR's files. In EmbeddedItemEditor.doSave, an object parent now goes through dropServedPicklistOptions (imported from @object-ui/data-objectstack, no second copy) before the parent PUT. Other parent types are sent exactly as before. Commit 32b8c2e adds that, a 422-refusing-transport pin beside it, and changeset prose that names the fifth writer. Ablation leg: the wrap removed, both pins red as predicted, restore proven. The replacement PR body is in pr_body_patch for the seat to write, and the PR was not PATCHed.",
"tests": "At HEAD 32b8c2e. New pin EmbeddedItemEditor.picklistServedOptions-11692.test.tsx: a real MetadataClient whose layered read serves an object with a bound tier field, over a transport that answers 422 INVALID_METADATA to a body carrying picklist beside options. The editor is mounted directly. Case 1 saves another item (amount); case 2 is an untouched save of the bound field itself. Both: PUT status 200, fields.tier equal to the type/label/picklist only, and the inline select status byte-identical. Run: 'Test Files 1 passed (1) / Tests 2 passed (2)'. Ablation, after the commit, through objectstack scripts/ablation-replace.mjs in wrap mode, with an outer trap EXIT INT TERM that runs git checkout HEAD on the absolute path. The anchor "parentType === 'object' ? dropServedPicklistOptions(updated) : updated," went from 1 to 0 hits, and the replacement "parentType === 'object' ? updated : updated," from 0 to 1. The blob changed from 59bb5cf17b82 to 03380de60cf6. Result: 'Tests 2 failed (2)', both 'AssertionError: refused at fields.tier.options: expected 422 to be 200', red as predicted. Restore: blob after restore == HEAD blob (59bb5cf17b82), git diff HEAD empty (0 bytes), and the tree is clean. Re-run on the new head: the 6 pin files (the 4 writer pins of round 1, the moved picklist-binding.test.ts, the new EmbeddedItemEditor pin) plus the 3 existing EmbeddedItemEditor suites (indexFallback, preview, refusalIssues-11379): 'Test Files 9 passed (9) / Tests 39 passed (39)'. app-shell type-check (both legs, tsc --noEmit and tsconfig.test.json): exit 0, after building the app-shell dependency closure (28 of 28 tasks). app-shell lint: exit 0, 0 errors, the warning total is unchanged from bc90d2f, the new test file has 0 hits, and the one EmbeddedItemEditor warning is pre-existing (a setState in an effect).",
"gates": [
{
"cmd": "pnpm exec turbo run build --filter='@object-ui/app-shell^...' --concurrency=2",
"exit": 0,
"head": "32b8c2e",
"verdict": "Tasks: 28 successful, 28 total"
},
{
"cmd": "pnpm --filter @object-ui/app-shell run type-check",
"exit": 0,
"head": "32b8c2e"
},
{
"cmd": "pnpm --filter @object-ui/app-shell run lint",
"exit": 0,
"head": "32b8c2e",
"verdict": "0 errors; warning total unchanged from bc90d2f"
},
{
"cmd": "pnpm exec vitest run --maxWorkers=2 (6 pin files + 3 EmbeddedItemEditor suites)",
"exit": 0,
"head": "32b8c2e",
"verdict": "Test Files 9 passed (9), Tests 39 passed (39)"
},
{
"cmd": "ablation: EmbeddedItemEditor wrap removed",
"exit": 1,
"head": "32b8c2e",
"verdict": "Tests 2 failed (2), as predicted; restored blob == HEAD, git diff HEAD empty"
},
{
"cmd": "pnpm check:new-line-citations",
"exit": 0,
"head": "32b8c2e",
"verdict": "0 new citation(s)"
},
{
"cmd": "pnpm check:control-bytes",
"exit": 0,
"head": "32b8c2e"
},
{
"cmd": "pnpm check:changeset-claims",
"exit": 0,
"head": "32b8c2e",
"verdict": "No pending changeset names a file this change touches."
},
{
"cmd": "pnpm check:pending-changeset-literals",
"exit": 0,
"head": "32b8c2e"
},
{
"cmd": "pnpm check:metadata-write-doors",
"exit": 0,
"head": "32b8c2e",
"verdict": "3 can carry an object document, 3 reach assertObjectMetadataWritable"
},
{
"cmd": "node scripts/check-changeset-no-major.mjs",
"exit": 0,
"head": "32b8c2e"
},
{
"cmd": "node scripts/check-changeset-presence.mjs",
"exit": 0,
"head": "32b8c2e",
"verdict": "15 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)"
},
{
"cmd": "pnpm check:self-import / check:phantom-deps / check:test-path-roots",
"exit": 0,
"head": "32b8c2e"
},
{
"cmd": "node scripts/check-vi-mock-specifiers.mjs / -inherit / -override-shape",
"exit": 0,
"head": "32b8c2e"
},
{
"cmd": "round-1 readings (data-objectstack and plugin-designer type-check and lint, the narrowed 247-file suite, readme-exports, the other check:*)",
"exit": null,
"head": "bc90d2f",
"verdict": "not re-taken in this round (outside the patch order's re-run list); they stand at bc90d2f"
},
{
"cmd": "CI",
"exit": null,
"head": "32b8c2e",
"verdict": "in_progress at report time: 20 success, 3 skipped, 18 in_progress, 1 queued; CI convergence is the PM's"
}
],
"files_changed": [
"M packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.tsx (import + the object-parent wrap in doSave)",
"A packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.picklistServedOptions-11692.test.tsx",
"M .changeset/11692-writers-drop-served-options.md (headline and writer list name the embedded-item editor)",
"merge commit 566ba80 brings main 3409fe8 (objectui#11700, #11702) in; no file of this PR conflicted"
],
"line_budget": "n/a: no skills/** file and no line-ratcheted ledger touched",
"deviations": [
"The local merge commit was amended once before it was pushed, only to add the model-free trailer pair. Its parents are unchanged (bc90d2f, 3409fe8). It was never on the remote, so no history that had been pushed was rewritten, and the push was a fast-forward.",
"Scope of the wrap: it is gated on parentType === 'object', because EmbeddedItemEditor is generic over parent types and the served-picklist shape is an object read. The order said 'exactly as your four writers do'. Those four are object-only, so the gate keeps the same behaviour for objects and sends every other parent type byte-for-byte as before.",
"Trade-off, said rather than chosen silently: the wrap also covers the edited item itself, which is the rule the four writers and the Studio data page already apply. If the drawer's generic field form offers options on a field that keeps its picklist, an options edit made there is dropped on save while the binding stays, because the editor cannot tell the served copy from an edit. Unbinding stays the job of the select-field editor's Options-from picker (objectui#10202). The replacement PR body records this in Acceptance notes.",
"Attribution, reported not chosen: the commit trailers are the model-free pair, per os-dev.md and objectui AGENTS.md, not the harness reminder's model-named trailer.",
"pr_body_patch carries the WHOLE replacement body. It was built from the live body (read back from the API) with six edits: the intro sentence, a fifth row in the per-writer table, a fifth pin bullet, the ablation intro and a fifth ablation row, a patch-round gate table ahead of the round-1 table (now labelled as such), and the unlisted-writer Acceptance note replaced by its fixed form. The body was scanned and has 0 angle brackets, 0 control bytes, 0 model ids, and its only closing keyword is 'Fixes #11692'. It is 14467 bytes and ends with the session-URL footer."
],
"pr_body_patch": {
"mode": "replace the whole PR body of objectui#11706 with this text",
"bytes": 14467,
"body": "Fixes #11692\n\nClause-②: yes\n\nObject writers outside the two objectui#10202 designers seed their PUT from a served object read. The runtime serves a picklist-bound field with
picklistand theoptionsit resolved from the list, and the authoring door refuses that pair for the whole object (422 INVALID_METADATAatfields.FIELD.options). Each writer the card lists now either drops those served options before its PUT, or is shown here not to seed from a served read. A fifth writer of the same class,EmbeddedItemEditor, was found during the work. The seat added it to the claim's file surface (comment 6012529702), and it is fixed the same way.\n\nDispatched work: claim 6011332561 on objectui#11692, PM sessionhttps://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju.\n\n## What changes\n\nThe helper moves to@object-ui/data-objectstack(new export, the reason forClause-②: yes).\n-dropServedPicklistOptionsmoves fromapp-shell/src/views/metadata-admin/picklist-binding.tstodata-objectstack/src/picklist-binding.ts(agit mv, with its test) and is exported from the package barrel.\n- Why it moves:@object-ui/plugin-designerdoes not depend on@object-ui/app-shell, andapp-shelllistsplugin-designeras a peer, so the two Setup pages could not import it where it lived. Both packages already depend ondata-objectstack. That package also owns the client that serves the read and carries the write, the same reasonextractDraftBodylives there.\n- The two existing callers,builtinComponents.tsx(the metadata-adminobjectresource'sfromDraft) andStudioDesignSurface.tsx, now import it from@object-ui/data-objectstack. No copy is left inapp-shell, which never exported it.\n- Built declaration, read frompackages/data-objectstack/dist/index.d.tsafterturbo run build:declare function dropServedPicklistOptionswith type parameterT extends Record of string to unknown, takingbody: Tand returningT. It appears in the barrel's export list besideextractDraftBody. The type parameter is spelled out in words here because GitHub strips angle brackets from PR bodies.\n- ⛔ It is not applied at a door.MetadataClient.saveis unchanged, which leaves option B ruled out as triage directed. The barrel comment and the module docblock both say the export is for WRITERS. A door cannot tell a served copy from an authored pair, and the authored pair must stay the server's loud refusal.\n\nPer writer (card "Done when"):\n\n| Writer | Seeds from a served read? | Now |\n|---|---|---|\n|PackageOwdOverviewPaneldoSave| yes:getDraftbody, elselayered.effective|dropServedPicklistOptions(next)beforeclient.save|\n|MetadataFieldsPagehandleFieldsChange| yes:client.get('object'), andcarryOver/carryPreservedFieldcarry every per-field key back |dropServedPicklistOptions(mergedObject). The page has no options editor, so a bound field'soptionscan only be the served copy |\n|MetadataObjectsPagehandleObjectsChange| yes:client.list('object')spread as the merge base |dropServedPicklistOptions(merged)|\n|EmbeddedItemEditordoSave(opened from an object's fields, indexes or validations) | yes: it re-readsclient.layered(parentType, parentName).effective, splices the edited item in, and PUTs the whole parent | anobjectparent goes throughdropServedPicklistOptions(updated). Other parent types are sent exactly as before |\n|MetadataService.saveFields| yes:client.meta.getItem('object')and the per-field carry-over |dropServedPicklistOptions(updatedObject).DesignerFieldDefinitiondeclares nopicklist, so this method can neither author nor remove a binding |\n|MetadataService.saveObject| no: it reads nothing, and builds from the caller'sobjandexistingFields| unchanged, with the reason in its docblock |\n|MetadataService.saveMetadataItem| no: it reads nothing, anddatais the caller's | unchanged, with the reason in its docblock |\n\nThe list read resolving the binding is a source reading of objectstack, not a live measurement. objectstack'smetadata-protocolfoldObjectExtendersFromRegistrydocblock says the list read "readsregistry.listItems, whose object branch resolves through the same fold", andSchemaRegistry.foldExtendersOntoDefinitionends inresolvePicklistFields.\n\nTriage's correction stands:runtime-metadata-persistence.tssaves no object, so it is untouched.\n\n## Pins (a 422-refusing transport, as objectui#10202's pins are)\n\nEach new test drives a realMetadataClient, or forMetadataServicea realObjectStackAdapterand SDK. The transport double answers a PUT whose fields carrypicklistbesideoptionswith the door's422 INVALID_METADATAenvelope, so every claim is read off the wire:\n\n-PackageOwdOverviewPanel.picklistServedOptions-11692.test.tsx: an OWD change on an object with a bound field, seeded fromlayered.effectiveand from a served draft. The door accepts the body, the bound field goes out aspicklistonly, the inline select and the text field go out byte-identical, and the OWD edit lands.\n-MetadataFieldsPage.picklistServedOptions-11692.test.tsx: relabel an unrelated field. Both the designable boundselectand a carried-through boundmultiselectleave withoutoptions, and the inline select keeps its options.\n-MetadataObjectsPage.picklistServedOptions-11692.test.tsx: relabel the object. Same assertions.\n-EmbeddedItemEditor.picklistServedOptions-11692.test.tsx: the editor is mounted over a served object with a boundtier. Two cases: saving another item (amount), and an untouched save of the bound field itself. In both, the door accepts the parent,tiergoes out aspicklistonly, and the inline select goes out byte-identical.\n-MetadataService.picklistServedOptions-11692.test.ts:\n -saveFieldswith a field list built from the served read is accepted, and the bound field has nooptions.\n -saveObjectandsaveMetadataItemissue no read, and an authored pair still reaches the door and is refused, asserted on the SDK error'scode: 'INVALID_METADATA'andhttpStatus: 422.\n- The movedpicklist-binding.test.tsis unchanged apart from its header. It still reads the contract off the installed@objectstack/spec.\n\nAblation. Each fix was committed first (51bb8bbfor the four writers,32b8c2eforEmbeddedItemEditor). Each writer's helper call was then removed through objectstack'sscripts/ablation-replace.mjs, which wraps the run, verifies that the mutation landed on disk, and restores fromHEAD. Every pin went red for the right reason, and every restore was proven by blob hash equal to HEAD and an emptygit diff HEAD.\n\n| Mutation | Result | Failure message |\n|---|---|---|\n| OWDdropServedPicklistOptions(next)tonext| 2 failed of 2 |refused at fields.tier.options: expected 422 to be 200|\n| fields pagedropServedPicklistOptions(mergedObject)tomergedObject| 1 failed of 1 |refused at fields.tier.options, fields.regions.options|\n| objects pagedropServedPicklistOptions(merged)tomerged| 1 failed of 1 |refused at fields.tier.options|\n|saveFieldsdropServedPicklistOptions(updatedObject)toupdatedObject| 1 failed, 2 passed of 3 | the SDK'sfailed spec validationthrow |\n|EmbeddedItemEditordropServedPicklistOptions(updated)toupdated(at32b8c2e) | 2 failed of 2 |refused at fields.tier.options: expected 422 to be 200|\n\nThesaveObjectandsaveMetadataItempins stay green under that last mutation, as they should. The first attempt at the last three ablations was REFUSED by the tool and ran no test, because the replacement was a substring of the anchor. They were re-anchored on the wholesave(...)line and re-run. The table shows the re-run.\n\nNodistbuild sits in the ablation path. The writers are imported fromsrcby relative path, and the root vitest config aliases@object-ui/data-objectstackto itssrc.\n\n## Gates\n\nPatch round (fifth writer), at branch head32b8c2e. This head includes the merge ofmainat3409fe8, which is a merge commit, not a rebase.\n\n| Gate | Exit | Verdict line |\n|---|---|---|\n|turbo run build --filter=@object-ui/app-shell^... --concurrency=2| 0 |Tasks: 28 successful, 28 total|\n|pnpm --filter @object-ui/app-shell run type-check| 0 | |\n|pnpm --filter @object-ui/app-shell run lint| 0 | 0 errors. The warning count is unchanged frombc90d2f, and the new test has no hit |\n|vitest runof the 6 pin files plus the 3 existingEmbeddedItemEditorsuites | 0 |Test Files 9 passed (9),Tests 39 passed (39)|\n|check:new-line-citations/check:control-bytes/check:changeset-claims/check:pending-changeset-literals/check:metadata-write-doors| all 0 |0 new citation(s)and3 can carry an object document, 3 reach assertObjectMetadataWritable|\n|check-changeset-no-major/check-changeset-presence/check:self-import/check:phantom-deps/check-vi-mock-{specifiers,inherit,override-shape}/check:test-path-roots| all 0 |15 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)|\n\nRound 1, at branch headbc90d2f. These readings were not re-taken in the patch round. All atbc90d2funless stated. Heavy runs went throughos-verify-lock.sh. Every exit code was captured before any pipe.\n\n| Gate | Exit | Verdict line |\n|---|---|---|\n|turbo run build --filter=@object-ui/app-shell^... --filter=@object-ui/plugin-designer^... --concurrency=2(at51bb8bb, whosesrcis identical tobc90d2f) | 0 |Tasks: 28 successful, 28 total|\n|pnpm --filter @object-ui/data-objectstack run type-check| 0 | |\n|pnpm --filter @object-ui/plugin-designer run type-check(tsc --noEmit && tsc -p tsconfig.test.json) | 0 | the new tests are program inputs (--listFiles) |\n|pnpm --filter @object-ui/app-shell run type-check(same two legs) | 0 | the new tests are program inputs (--listFiles) |\n|pnpm --filter @object-ui/{data-objectstack,plugin-designer,app-shell} run lint| 0 / 0 / 0 | 0 errors. No warning on a line this PR adds |\n|vitest runof the 5 new or moved test files | 0 |Test Files 5 passed (5),Tests 13 passed (13)|\n|vitest run packages/data-objectstack/ packages/plugin-designer/plus 126app-shellfiles (see below) | 0 |Test Files 247 passed (247),Tests 2342 passed (2342)|\n|check:self-import/check:phantom-deps/check:unused-deps| 0 / 0 / 0 | |\n|check:readme-exports| 1, a precondition (see below) | the new import is judgedreal(--list) |\n| README snippet, compiled--strictagainst the builtdist| 0 | negative control (a misspelt name): TS2724, exit 2 |\n|check:new-line-citations| 0 |0 new citation(s)|\n|check:control-bytes| 0 |OK|\n|check:metadata-write-doors| 0 |3 can carry an object document, 3 reach assertObjectMetadataWritable|\n|check:changeset-claims/check:pending-changeset-literals| 0 / 0 |No pending changeset names a file this change touches.|\n|check-changeset-no-major/check-changeset-fixed/check-changeset-presence| 0 / 0 / 0 |13 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)|\n|check:unreferenced-sources/check:test-path-roots/check-vi-mock-{specifiers,inherit,override-shape}/check:esm-specifiers/check:doc-fences/check:designer-field-key-parity/check-type-check-coverage/check-lint-coverage/check:spec-symbols/check:component-surface-parity(report-only) | all 0 | |\n\nNotes on the table:\n\n- Narrowed test run, declared.app-shellhas 1025 test files. A full run of all three packages was started and held the shared lock for 29 minutes without finishing, with other seats queued behind it, so I stopped it (my own PID). I re-ran a narrowed set instead: all ofdata-objectstackandplugin-designer, and inapp-shellevery test undersrc/services/andsrc/views/studio-design/, every test that namesStudioDesignSurface,builtinComponents,PackageOwdOverviewPanel,MetadataService,picklist-binding,dropServedPicklistOptionsorregister-builtins, and the objectui#10202 suites. That set is derived from direct importers. It is blind to a test that reaches a touched module through a third module, so the fullapp-shellsuite is left to CI.app-shell's barrel surface is byte-unchanged.data-objectstackgains one export and loses none.\n-check:readme-exportsexit 1 is not a measurement of this change. It is the gate's "type entry not on disk" refusal for 8 packages outside this build's closure (plugin-gantt,plugin-map,plugin-markdown,plugin-timelineand others). With--listit exits 2 (PRECONDITION NOT MET) and judgespackages/data-objectstack/README.md'sdropServedPicklistOptionsimportreal.\n- NOT MEASURED:check:doc-snippets, reason: its build closure is every package. The one new README block was compiled--strictagainst the builtdata-objectstackdist instead, with the negative control shown in the table.\n- NOT MEASURED:check:eager-closure, reason: it needs a full console build. See Acceptance notes.\n\n## Acceptance notes\n\n- The fifth writer,EmbeddedItemEditor, is fixed here after the claim amendment (comment 6012529702). Its wrap covers the edited item too, the same rule the four writers and the Studio data page apply: every field that names a picklist leaves withoutoptions. If the drawer's generic field form offersoptionson a bound field, an edit made there is dropped while the binding stays, because the editor cannot tell the served copy from an edit. Unbinding a field stays the job of the select-field editor's "Options from" picker (objectui#10202).\n-MetadataServicehas no in-repo caller ofsaveObject,saveFieldsorsaveMetadataItem. It is public API throughuseMetadataService. ThesaveFieldsfix and the two pins of "no read, the pair still refused" are therefore measured on the method, not on a screen.\n-check:eager-closurewas not run locally (it needs a full console build). The helper moved into a package thatapps/console/src/dataSource.tsalready imports statically, so no new module joins the eager graph. That claim is read from source, not measured.\n\n---\n_Generated by Claude Code_\n"},
"mcp_calls": "0 — none",
"api_writes": "1 REST write this round: this os-dev-report comment, POST /repos//issues/11692/comments, relayed as one POST /repos/objectstack-ai/objectstack/dispatches executed by objectstack-fleet[bot]. Plus 1 git push, which is not REST (bc90d2f to 32b8c2e, a fast-forward). There was no PR PATCH and no other write.",
"open_questions": [],
"out_of_scope_findings": []
}
Generated by Claude Code
objectstack-fleet commentedon Oct 6, 2026
ACCEPT: PR objectui#11706 at
32b8c2e(Fixes #11692), after patch round 1. The at-tier contract review is owed before enqueuedomain:uiexecution seat 2 ·session_01FngvPpdrnhHMdHHq6vwwju(os-justin) · 2026-10-06T08:59Z. Dev reports: round 0 and patch round 1 (6012839770) on this card.main, first lineFixes #11692, and the body has no other closing keyword.Clause-②: yesis at the start of a line. 17 files, all inside the claim's surface as amended by6012529702. No governed path. +942/−21.mainat3409fe8as a merge commit, with no rebase and no force push, and nothing conflicted.pr_body_patchin this stroke. The relay read back 14468 bytes, identical.dropServedPicklistOptionsmoves, bygit mvwith its test, to@object-ui/data-objectstack, and is exported from its barrel. That is the one widening, which makes thisClause-②: yes.app-shell's two callers are re-pointed, and no copy is left.EmbeddedItemEditor'sdoSave(for anobjectparent only), andMetadataService.saveFields.saveObjectandsaveMetadataItemread nothing, so they stay unchanged.MetadataClient.saveis unchanged, which keeps triage's ⛔: the server still refuses an authoredpicklist+optionspair.data-objectstackminor,app-shellandplugin-designerpatch), checked sentence by sentence against the diff: the headline names the five writers, the five bullets match the five call sites, the "keeps its inlineoptionsunchanged" sentence matches the helper's pins, and the export paragraph (moved, not exported before, not applied at the door) matches the barrel andMetadataClient.MetadataClient(or the real adapter and SDK forMetadataService). Five ablation legs, each red with the door'srefused at fields.tier.optionsand restored by blob. The patch round's 9 files and 39 tests passed at32b8c2e. The round-1 narrowed set, 247 files, passed atbc90d2f; the fullapp-shellsuite is declared to CI.optionsedit on a field that keeps itspicklist, the same rule the other writers and the Studio data page apply. Unbinding stays the job of objectui#10202's "Options from" picker.MetadataService's three save methods have no in-repo caller. This is in the Acceptance notes, not filed.Clause-②: yes, the new@object-ui/data-objectstackexport).needs:contract-reviewis hung on the PR in this stroke.Generated by Claude Code
objectstack-fleet commentedon Oct 6, 2026
Landed: PR objectui#11706 →
mainda35453, verified by content.Fixesclosed this carddomain:uiexecution seat 2 ·session_01FngvPpdrnhHMdHHq6vwwju(os-justin) · 2026-10-06T09:33Z.The merge. Squash
da35453went through the merge queue, with one parent,de96f3d. It is an ancestor oforigin/main. 17 files, +942/−21, the same as the PR. Contract review6013083759PASS, on the landed head32b8c2e.Content check against the first parent:
de96f3dda35453dropServedPicklistOptionsinpackages/data-objectstack/src/index.tspackages/app-shell/src/views/metadata-admin/picklist-binding.tspresent (the old copy)dropServedPicklistOptions(updated)inEmbeddedItemEditor.tsxdropServedPicklistOptions(next)inPackageOwdOverviewPanel.tsxdropServedPicklistOptions(mergedObject)inMetadataFieldsPage.tsxClosures.
Fixes #11692closed this card as completed at the merge. No other issue closed in the landing window.pm:dispatchedcomes off in this stroke.The review's note, recorded here and not filed: if the server-published
fieldschema renders anoptionscontrol in the embedded-item drawer, an author'soptionsedit on a field that keeps itspicklistis dropped on save without a message. This is unmeasured. What would file it: open an object field that names a picklist in the metadata-admin drawer, and read whether anoptionscontrol renders.Generated by Claude Code
fix(studio): object writers seeded from a served read drop a picklist…