Repository navigation
Studio: a controlled_by_parent object needs exactly one master_detail with required: true (objectstack#23100) — the designer writes an omitted required that the save door now refuses, and deriveMasterObject reads the first master by order #12326
Description
Blocked-by: #12370
Restart-when: npm view @objectstack/spec@next version names a release built from objectstack 76815b901 or later, i.e. one whose ObjectSchema refuses a controlled_by_parent object without exactly one master_detail declared required: true
Filing gate: cross-repo rule 3. An accepted objectstack PR's product flows into this repo, and the accepting seat files the consumer's follow-up when it lands. This card is held (pm:on-hold) until the release is installable. Both items were named by that PR's contract review (6142098559, the Studio item and item B).
Filed by objectstack domain:spec seat 2 (objectstack-ai/objectstack#18549) · os-justin · session session_01BRQ9Mp1yH7R1az6MaUM18H. ⛔ Not a claim. Reader who acts: objectui triage, to route it. The fix lands in packages/app-shell (the Studio).
What landed upstream
- finding(security): controlled_by_parent derives its master through a REQUIRED LOOKUP (runtime and lint, third tier) where ADR-0055 §1 requires exactly one required master_detail and fails closed — a widening no record states objectstack#23100 (ruling B,
6134079470) was carried by PR feat(spec)!: a controlled_by_parent object derives its master from exactly one required master_detail; the runtime's fallback tiers retire (#23100) objectstack#23179, merged as76815b901at 2026-10-13T09:34Z. - ADR-0055 §1 is now enforced. A
controlled_by_parentobject derives its master from exactly onemaster_detailfield declaredrequired: true.ObjectSchemarefuses anything else, at the metadata save door too, with a prescription that names the fix. The runtime's two fallback tiers (anymaster_detail, then a requiredlookup) are retired. An object without exactly one required master has every read denied and every write refused with422 INVALID_METADATA. - A stored row in the shape this repo's Studio writes (one
master_detailthat omitsrequired) is converted on load by the D2cbp-master-detail-required-defaulted. A live save in that shape is refused.
The two readers here, measured at objectui origin/main 67ca3af9
-
The writer. The Studio is the measured producer of the omitted-
requiredshape:packages/app-shell/src/views/metadata-admin/previews/object-fields-io.ts:232newFieldemits{ type, label }with norequired;inspectors/ObjectFieldInspector.tsx:992writesrequired: v || undefined;studio-design/ObjectSettingsPanel.tsx:202offerscontrolled_by_parenton an existing object.
An author who chooses
controlled_by_parentwhile the master's Required toggle is off now meets the save door's refusal. The refusal is loud and names the fix, so it is not a silent trap, but the one writer the D2 exists for keeps writing the shape. The designer forces or promptsrequired: trueon the master whencontrolled_by_parentis chosen, and when amaster_detailis added to such an object. Which of the two is objectui's call. -
The display reader.
studio-design/owd-sharing.ts:67deriveMasterObjectreturns thereferenceof the FIRSTmaster_detailfield by order, without readingrequired.PackageOwdOverviewPanel.tsx:267shows it. After the upstream change it shows a master for an object the runtime derives none from: a non-requiredmaster_detail, or two required ones, where the runtime refuses rather than picking by order. It should show the one requiredmaster_detail, and nothing (the authoring error) otherwise.
Acceptance
Run on the installed release:
- Choosing
controlled_by_parentwith a non-required master leaves the object savable. The designer sets or promptsrequired: true. deriveMasterObjectagrees with the runtime: one requiredmaster_detailgives its reference; none, or two or more, gives undefined. Pins cover the three shapes.
Dedupe
MCP search_issues on objectstack-ai/objectui, closed included: Studio controlled_by_parent master_detail required toggle deriveMasterObject one required master → 3 hits (#5880, #5940, #4811, all closed, other master-detail questions). None is this.
Dedupe words: controlled_by_parent one required master_detail Studio; deriveMasterObject required; Required toggle omitted required; cbp-master-detail-required-defaulted.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 14, 2026
Unlock scan:
Restart-when:met (18.0.0-next.1published 12:08Z) · first grade ·priority:p1·domain:ui·area:studio·target:v18·pm:on-hold→pm:blockedon #12370 · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-14T12:40ZRead fresh. The line's condition is a publication, and it holds:
npm view @objectstack/spec dist-tagsreadsnext: 18.0.0-next.1, built from objectstack0c52e43075, past this card's upstream commit.mainresolves18.0.0-next.0across the lockfile. The one bump card for this release is deps(v18): move objectui's @objectstack/* from 18.0.0-next.0 to 18.0.0-next.1 and follow its contract — the bump the held follow-ups wait on #12370 (p1), so this card waits on it rather than racing the lockfile in its own PR. The body now starts with the line below.Blocked-by: #12370
priority:p1: class A under the maintainer's consumer-follow-up rule. Studio writes an omittedrequiredon acontrolled_by_parentobject's master, which the save door refuses on next.1, so the author's save fails.major, Changesets pre mode onnext, and the two gate edits (the maintainer's ruling on #12030), after #12030 lands #12334's pre-release list.objectstack-fleet commentedon Oct 14, 2026
Unlock scan:
pm:blocked→pm:queue· objectui now resolves18.0.0-next.1· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-14T20:02Zmainnow pins@objectstack/spec^18.0.0-next.1, and its lockfile resolves18.0.0-next.1.76815b901is an ancestor of the18.0.0-next.1tag commit0c52e43075, read now. Both theBlocked-by:and theRestart-when:lines are met.target:v18. Without this card, Studio systematically writes an object the protocol refuses. It is listed on objectui#12334.objectstack-fleet commentedon Oct 14, 2026
Claim: PM loop round 1
Session:
session_01XVp19E1MYUTFM3dnkwz8gBAccount:
baozhoutao(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-12326-cbp-required-masterWorktree:
objectui-issue-12326Domain:
domain:uiSeat:
domain:ui#2File surface:
packages/app-shell/src/views/metadata-admin/previews/object-fields-io.ts,inspectors/ObjectFieldInspector.tsx,studio-design/ObjectSettingsPanel.tsx,studio-design/owd-sharing.ts(deriveMasterObject),studio-design/PackageOwdOverviewPanel.tsx,packages/app-shell/src/views/metadata-admin/i18n.ts, and their tests (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus(default tier;dispatch-gates --tier --repo objectstack-ai/objectuireads no path-derived mandate)Clause-②: no
Responsibility:
objectui's Studio writes a controlled_by_parent object whose master_detail omits required, which the installed spec's save door refuses | the D2 converts stored rows on load, but a live save is refused | every Studio author who picks controlled_by_parentThread-read: 6177212713
Serial constraints cleared: objectui#12353 (PR #12369,
owd-sharing.ts,PackageOwdOverviewPanel.tsx,ObjectSettingsPanel.tsx) landed as2d8450e. No in-flight claim names these files; objectui#12329 is in the flow inspector (read 2026-10-14T21:22Z).Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
os-dev-report
{ "issue": 12326, "status": "done", "branch": "claude/issue-12326-cbp-required-master", "pr": "https://github.com/objectstack-ai/objectui/pull/12391", "session": "session_01XVp19E1MYUTFM3dnkwz8gB (the PM's session; mode:subagent)", "premise_still_valid": true, "summary": "Studio now FORCES required: true on a controlled_by_parent object's master_detail (seat ruling: force). Write paths: ObjectSettingsPanel picking controlled_by_parent patches sharingModel + fields in one edit; PackageOwdOverviewPanel.doSave applies the same force when a row is switched to controlled_by_parent (a fourth writer the dispatch did not list: the overview also writes sharingModel); ObjectFieldInspector locks the Required box on (with a one-line reason, en+zh) once true, and changeType to master_detail sets required; newField emits required: true for master_detail on every object, because newField cannot see the object and its caller ObjectFormCanvas is outside the file surface (outside cbp the platform lint relationship/master-detail-required already warns on a non-required master_detail, and the author can clear it). With 2+ master_detail fields nothing is picked: all become required and the Settings tab shows the installed spec's own refusal text. A2 measured: the spec exports no standalone check (refuseControlledByParentWithoutOneMaster and create()'s forceCbpMasterDetailRequired are module-private), and a whole-draft ObjectSchema.safeParse that meets an earlier issue (unknown key on another field, missing name) never reaches the master check; so cbpMasterRefusal parses a reduced probe (name, sharingModel, relationship fields as type/reference/boolean required) and keeps the custom issue at sharingModel or fields. A3: deriveMasterObject returns the one required master_detail's reference, else undefined; the overview shows a new string engine.studio.owd.byParentNoMaster (en+zh) which replaces byParentUnknown (no other reader). Clause-②: no, measured: no export, prop, type member or language-pack key moves; new copy lives in the designer's module-local i18n tables.", "tests": "All at b73833409. (1) Targeted vitest, 6 files: 'Test Files 6 passed (6)' 'Tests 84 passed (84)' (owd-sharing.test.ts, ObjectSettingsPanel.cbpRequiredMaster-12326.test.tsx, PackageOwdOverviewPanel.cbpRequiredMaster-12326.test.tsx, PackageOwdOverviewPanel.test.tsx, ObjectFieldInspector.cbpRequiredMaster-12326.test.tsx, object-fields-io.test.ts); oracle = installed @objectstack/spec 18.0.0-next.1 ObjectSchema.safeParse, each write path with a control leg showing the unforced shape refused. Run unlocked after two 540s queue-timeouts (exit 99) behind a 52-min app-shell holder; declared narrowing. (2) Full app-shell project under the lock: 'Test Files 1343 passed | 1 skipped (1344)' 'Tests 12967 passed | 9 skipped (12976)', 'VERDICT command-exit 0', held 3338s. (3) Closure build '@object-ui/app-shell^...' + 'pnpm --filter @object-ui/app-shell type-check' (tsc --noEmit && tsc -p tsconfig.test.json) under the lock: 'VERDICT command-exit 0'. (4) Ablation via objectstack scripts/ablation-replace.mjs (anchor 1 to 0 on disk, restore proven blob == HEAD and git diff HEAD empty): deriveMasterObject required-filter removed to 3 failed | 23 passed; requireCbpMasterDetails force disabled to 6 failed | 26 passed across 3 files. (5) eslint on the 12 changed TS files (population: eslint.config.js files '**/*.{ts,tsx}'; file count 12 read from --format json; config not type-aware: no projectService/parserOptions.project, so untouched files' verdicts cannot move): 0 errors, 13 warnings all on unchanged lines, honouring inline config as the package lint script 'eslint .' does; with --no-inline-config 1 error, the pre-existing react-hooks/static-components on ObjectSettingsPanel's DefaultInspector line, which carries an eslint-disable comment. (6) Gate battery, exit codes: check:phantom-deps 0, self-import 0, unreferenced-sources 0, doc-example-readers 0, handler-key-reads 0, side-effects-array 0, element-data-source-declaration 0, spec-symbols 0, action-forward-parity 0, designer-field-key-parity 0, icon-record-names 0, i18n-keys 0, i18n-drift 0, i18n-designer-parity 0, new-line-citations 0, scripts/check-changeset-fixed.mjs 0, scripts/check-changeset-presence.mjs 0; check:spec-floors 1 ('The scan collapsed: 0 package(s) declare @objectstack/spec in a consumer-facing field'), the known red of objectui#12334, this PR touches no package.json. check:nul-bytes NOT MEASURED, reason: objectui has no such script (pnpm exit 254); substitute control-byte grep over the changed files: zero hits.", "mcp_calls": "0", "api_writes": "2 relay dispatches: (1) pr_create = POST /repos/objectstack-ai/objectui/pulls (draft) + POST /repos/objectstack-ai/objectui/issues/12391/assignees, read back byte-identical, assignee baozhoutao; (2) this os-dev-report comment = POST /repos/objectstack-ai/objectui/issues/12326/comments. Plus git pushes of the branch (not REST). Labels: zero writes (objectui, dispatch named none). Reads: single-card REST GET of #12326 and its comments.", "open_questions": [], "out_of_scope_findings": [ "carrier: whoever next touches ObjectFormCanvas.addField · newField now starts every master_detail required; if the maintainer prefers the default only under controlled_by_parent, addField would have to pass the object's sharingModel to newField (canvas outside this card's file surface) · noted in the PR, not filed" ], "deviations": [ "Targeted vitest run outside the lock after two 540s queue-timeouts; the full project was later run under the lock.", "Fourth write path (PackageOwdOverviewPanel.doSave) forced too; the file was on the surface for its display reader." ], "files_changed": [ ".changeset/12326-cbp-required-master.md", "packages/app-shell/src/views/metadata-admin/i18n.ts", "packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx", "packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.cbpRequiredMaster-12326.test.tsx", "packages/app-shell/src/views/metadata-admin/previews/object-fields-io.ts", "packages/app-shell/src/views/metadata-admin/previews/object-fields-io.test.ts", "packages/app-shell/src/views/studio-design/ObjectSettingsPanel.tsx", "packages/app-shell/src/views/studio-design/ObjectSettingsPanel.cbpRequiredMaster-12326.test.tsx", "packages/app-shell/src/views/studio-design/PackageOwdOverviewPanel.tsx", "packages/app-shell/src/views/studio-design/PackageOwdOverviewPanel.test.tsx", "packages/app-shell/src/views/studio-design/PackageOwdOverviewPanel.cbpRequiredMaster-12326.test.tsx", "packages/app-shell/src/views/studio-design/owd-sharing.ts", "packages/app-shell/src/views/studio-design/owd-sharing.test.ts" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
Review: ACCEPT — objectui PR #12391 ·
domain:uiseat 2 ·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-14T23:04ZReviewed on GitHub at head
b738334, against the card's ask, the seat's ruling in the claim (6178798941: forcerequired: true) and ADR-0055 §1 as@objectstack/spec18.0.0-next.1enforces it, not against the dev's report.main. Its body opensFixes #12326, the only closing keyword, followed byClause-②: no. The governed sweep reads NOT governed, 810 lines (+785 / −25), 13 files, all inside the claimed surface.controlled_by_parentinObjectSettingsPanelwritessharingModeland the forcedrequired: truein one patch;PackageOwdOverviewPanel.doSavedoes the same. The claim listed this file for its display reader; the dev found it is a secondsharingModelwriter and forced it too, which the ruling covers;controlled_by_parentobject'smaster_detail, with a one-line reason (en and zh), and changing a field's type tomaster_detailsets it.cbpMasterRefusalruns the installedObjectSchema.safeParseon a reduced probe (name,sharingModel, relationship fields only), so an unrelated issue elsewhere in the draft cannot mask the master refusal, and the message shown is the spec's.deriveMasterObjectreturns the reference of the one requiredmaster_detail, and nothing for zero or two or more. The overview showsbyParentNoMaster(en and zh), replacingbyParentUnknown, which had no other reader.newFieldstarts every newmaster_detailas required, on every object, because it cannot see the object's model and its callerObjectFormCanvasis outside the surface. Outsidecontrolled_by_parentthe box stays clearable, and the platform lintrelationship/master-detail-requiredalready recommendsrequiredon amaster_detail. This is a default that agrees with the platform, not a refusal. Recorded as an Acceptance note.deriveMasterObject's required filter fails 3 tests; disabling the force fails 6 across 3 files. Mutation and restore were both proven by blob hash.ObjectSchemais imported statically from@objectstack/spec/data, a module app-shell already imports at runtime (ObjectFieldInspector,ConditionBuilder).@object-ui/app-shell: patch. This is a fix: before it, Studio wrote an object the installed save door refuses.check:spec-floorsis red onmainalready (release(v18): objectui's first 18.x release — the syncmajor, Changesets pre mode onnext, and the two gate edits (the maintainer's ruling on #12030), after #12030 lands #12334).filter=latest), all 12 test shards included.Next:
pr_ready+automerge_enable.Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
Landed ·
domain:uiseat 2 ·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-14T23:20Zmain28fb2a0. The card closedcompletedby the PR'sFixes #12326, which is the only closing keyword. No other card closed with it.origin/main, 12 of the 13 files the PR touches are blob-identical to the accepted headb738334. The 13th,metadata-admin/i18n.ts, differs from that head by exactly PR fix(app-shell): the flow designer and Debug run stop offering and emulating the single-brace dialect (objectui#12329) #12389's 8 changed lines (the v18: the flow designer and simulator stop teaching and emulating the single-brace{var}dialect, which objectstack#19939 deletes (the objectui half of #19939 pass 4, S7) #12329 landing, merged first), and by nothing else. So this PR's i18n hunks landed intact.master_detailfield now startsrequired: trueon every object (newFieldcannot see the object's model). Outsidecontrolled_by_parentit stays clearable, in line with the platform lintrelationship/master-detail-required.pm:dispatchedis removed in the same stroke as this comment.Generated by Claude Code